From 1de15bdf4d74af1101c4317d7d1516596f6cb792 Mon Sep 17 00:00:00 2001 From: WeiQing Chen <40507679+david6666666@users.noreply.github.com> Date: Fri, 22 May 2026 09:46:36 +0800 Subject: [PATCH 001/112] Update WeChat group QR code (#3806) Signed-off-by: David Chen <530634352@qq.com> --- docs/assets/WeChat.jpg | Bin 267149 -> 411750 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/WeChat.jpg b/docs/assets/WeChat.jpg index 18a0c53ce4adceb0a9d63dfe2a8b5010ee6113d7..e795b3873632952d0c50b4a320c4f15f9625bfc5 100644 GIT binary patch literal 411750 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

k${VkU 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

H0%0CE3Idi@k;b-~Cx|7xW4t>pZBuh`_? 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 HKV$y}F4qV7 From 7d8816b46b77b84a3340432b6ba7ba0fbee88e18 Mon Sep 17 00:00:00 2001 From: Chenguang Zheng <645327136@qq.com> Date: Fri, 22 May 2026 10:17:59 +0800 Subject: [PATCH 002/112] [Bugfix] Fix Hunyuan worker device context (#3768) Signed-off-by: Chenguang ZHENG <645327136@qq.com> Signed-off-by: Canlin Guo <961750412@qq.com> Co-authored-by: Canlin Guo <961750412@qq.com> --- vllm_omni/diffusion/distributed/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vllm_omni/diffusion/distributed/utils.py b/vllm_omni/diffusion/distributed/utils.py index 92d00ef4b..851328e62 100644 --- a/vllm_omni/diffusion/distributed/utils.py +++ b/vllm_omni/diffusion/distributed/utils.py @@ -10,5 +10,7 @@ from vllm_omni.platforms import current_omni_platform def get_local_device() -> torch.device: """Return the torch device for the current rank based on detected device type.""" + if current_omni_platform.is_initialized(): + return current_omni_platform.get_torch_device(current_omni_platform.current_device()) local_rank = int(os.environ.get("LOCAL_RANK", 0)) return current_omni_platform.get_torch_device(local_rank) From 076c34abfbef61584c20d29e42e13a4138d26b9d Mon Sep 17 00:00:00 2001 From: baonudesifeizhai <85092850+baonudesifeizhai@users.noreply.github.com> Date: Thu, 21 May 2026 22:39:10 -0400 Subject: [PATCH 003/112] =?UTF-8?q?=EF=BC=88Phase=202=EF=BC=89Add=20ModelO?= =?UTF-8?q?pt=20mixed=20FP8/NVFP4=20support=20for=20image=20generation=20(?= =?UTF-8?q?#3570)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: roG0d Signed-off-by: baonudesifeizhai <85092850+baonudesifeizhai@users.noreply.github.com> --- docs/user_guide/quantization/modelopt.md | 107 ++++++-- mkdocs.yml | 4 +- recipes/Qwen/Qwen-Image.md | 105 ++++++++ recipes/README.md | 3 +- recipes/Tencent/HunyuanImage-3.0-Instruct.md | 132 +++++++++- tests/e2e/accuracy/test_hunyuan_image3.py | 236 +++++++++++++++++- .../models/test_encoder_quant_config.py | 5 +- vllm_omni/diffusion/data.py | 18 +- .../checkpoint_adapters/__init__.py | 14 +- .../checkpoint_adapters/modelopt.py | 23 +- .../models/helios/helios_transformer.py | 124 ++++++++- .../hunyuan_image3_transformer.py | 2 +- .../qwen_image/qwen_image_transformer.py | 45 +++- .../models/hunyuan_image3/hunyuan_image3.py | 2 +- vllm_omni/quantization/component_config.py | 4 +- vllm_omni/quantization/factory.py | 12 + 16 files changed, 768 insertions(+), 68 deletions(-) diff --git a/docs/user_guide/quantization/modelopt.md b/docs/user_guide/quantization/modelopt.md index 8a4389015..66e8427bb 100644 --- a/docs/user_guide/quantization/modelopt.md +++ b/docs/user_guide/quantization/modelopt.md @@ -6,34 +6,46 @@ ModelOpt quantization loads checkpoints produced by NVIDIA ModelOpt. The quantized weights and scale tensors are generated before serving, so inference does not run online calibration or convert a BF16 checkpoint at startup. -vLLM-Omni currently validates the ModelOpt FP8 checkpoint path for diffusion -transformers. The loader auto-detects supported ModelOpt FP8 checkpoint configs -and keeps non-transformer components, such as the tokenizer, scheduler, text +vLLM-Omni validates ModelOpt FP8, ModelOpt NVFP4, and ModelOpt mixed +FP8/NVFP4 checkpoint loading for diffusion transformer stages. The loader +auto-detects supported ModelOpt checkpoint configs and keeps non-transformer +components, such as the tokenizer, scheduler, text encoder, vision/audio encoder, and VAE, on the base checkpoint unless a model-specific guide says otherwise. !!! note - `--force-cutlass-fp8` is an explicit runtime override for diffusion - checkpoints that already carry a supported ModelOpt FP8 config. It does not - quantize BF16 checkpoints and it does not apply to online `--quantization - fp8`. The flag only takes effect for ModelOpt FP8 diffusion stages on CUDA - SM89+ devices; other platforms and non-ModelOpt FP8 paths fall back to the - normal vLLM kernel selection. + ModelOpt checkpoints are pre-quantized checkpoints. Do not pass + `--quantization fp8` for these checkpoints. The checkpoint + `quantization_config` selects the ModelOpt path. + +!!! note + `--force-cutlass-fp8`, `--linear-backend cutlass`, and + `--moe-backend cutlass` are runtime backend selections for checkpoints that + already carry supported ModelOpt quantized weights and scales. They do not + quantize BF16 checkpoints at startup. ## Supported ModelOpt Checkpoint Formats vLLM-Omni treats ModelOpt checkpoints as pre-quantized checkpoints. The checkpoint config must identify ModelOpt as the quantization method or producer, -and the quantization algorithm must be one of the validated FP8 algorithms. +and the quantization algorithm must be one of the validated algorithms below. | Checkpoint field | Supported value | |------------------|-----------------| -| `method` / `quant_method` | `modelopt` | +| `method` / `quant_method` | `modelopt`, `modelopt_fp4`, `modelopt_mixed` | | `producer.name` | `modelopt` | -| `quant_algo` | `FP8`, `FP8_PER_CHANNEL_PER_TOKEN` | +| `quant_algo` | `FP8`, `FP8_PER_CHANNEL_PER_TOKEN`, `NVFP4`, `MIXED_PRECISION` | -Other ModelOpt algorithms, such as NVFP4, are not enabled by this diffusion -FP8 path until they have separate model and quality validation. +| `quant_algo` | Runtime method | Typical use | +|--------------|----------------|-------------| +| `FP8`, `FP8_PER_CHANNEL_PER_TOKEN` | `modelopt` | FP8 diffusion transformer checkpoints | +| `NVFP4` | `modelopt_fp4` | NVFP4 diffusion transformer checkpoints | +| `MIXED_PRECISION` | `modelopt_mixed` | Mixed FP8/NVFP4 checkpoints with a ModelOpt per-layer policy | + +For multi-component diffusion or omni models, only the checkpoint component +that contains ModelOpt quantized weights should use the ModelOpt quantization +method. Encoders, decoders, tokenizers, schedulers, and other BF16 components +stay unquantized unless the model-specific recipe validates otherwise. ## Hardware Support @@ -47,7 +59,10 @@ FP8 path until they have separate model and quality validation. | Ascend NPU | ❌ | Legend: `✅` supported, `❌` unsupported, `⭕` not verified in this guide. -The optional CUTLASS FP8 runtime override requires CUDA SM89+. +The optional CUTLASS FP8 runtime override requires CUDA SM89+. ModelOpt NVFP4 +and mixed FP8/NVFP4 diffusion checkpoints are currently validated on Blackwell +CUDA systems in the recipes below; other CUDA generations require separate +backend and quality validation. ## Model Type Support @@ -56,12 +71,19 @@ The optional CUTLASS FP8 runtime override requires CUDA SM89+. | Model | HF checkpoint | Scope | Status | |-------|---------------|-------|--------| | Qwen-Image 2512 | `feizhai123/qwen-image-2512-modelopt-fp8-dynamic-all` | Diffusion transformer | Validated for ModelOpt FP8 checkpoints | +| Qwen-Image 2512 | `feizhai123/qwen-image-2512-modelopt-mixed-fp8-sensitive-nvfp4-heavy` | Diffusion transformer | Validated for ModelOpt mixed FP8/NVFP4 checkpoints | | Z-Image | `feizhai123/z-image-modelopt-fp8-conservative` | Diffusion transformer | Validated for ModelOpt FP8 checkpoints | | FLUX.2-dev | `feizhai123/flux2-dev-modelopt-fp8` | Diffusion transformer | Validated for ModelOpt FP8 checkpoints | | FLUX.2-klein 4B | `feizhai123/flux2-klein-4b-modelopt-fp8` | Diffusion transformer | Validated for ModelOpt FP8 checkpoints | | HunyuanImage-3.0 | `feizhai123/hunyuan-image3-modelopt-fp8` | MoE diffusion transformer | Validated for ModelOpt FP8 checkpoints | +| HunyuanImage-3.0 | `feizhai123/hunyuan-image3-modelopt-mixed-experts-nvfp4-dense-fp8` | MoE diffusion transformer | Validated for ModelOpt mixed FP8/NVFP4 checkpoints | | Wan2.2 | Not available | Diffusion transformer | Not validated | +For full serving commands and benchmark context, see +[`recipes/Qwen/Qwen-Image.md`](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Qwen/Qwen-Image.md) +and +[`recipes/Tencent/HunyuanImage-3.0-Instruct.md`](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Tencent/HunyuanImage-3.0-Instruct.md). + ### Multi-Stage Omni/TTS Model | Model | Scope | Status | @@ -80,15 +102,28 @@ ModelOpt targets yet. ## Configuration -For pre-quantized ModelOpt FP8 checkpoints, no `--quantization fp8` flag is -needed. The checkpoint config selects the ModelOpt path. +For pre-quantized ModelOpt checkpoints, no `--quantization fp8` flag is needed. +The checkpoint config selects the ModelOpt path. Online serving: ```bash -vllm serve \ +vllm serve \ --omni \ --tensor-parallel-size \ + --linear-backend cutlass \ + --force-cutlass-fp8 +``` + +For mixed FP8/NVFP4 MoE checkpoints, also select the validated MoE backend: + +```bash +vllm serve \ + --omni \ + --tensor-parallel-size \ + --enable-expert-parallel \ + --linear-backend cutlass \ + --moe-backend cutlass \ --force-cutlass-fp8 ``` @@ -96,14 +131,14 @@ Offline inference: ```bash python examples/offline_inference/text_to_image/text_to_image.py \ - --model \ + --model \ --tensor-parallel-size \ --prompt "a red ceramic teapot on a wooden table" \ --height 1024 \ --width 1024 \ --num-inference-steps 20 \ --seed 42 \ - --output outputs/modelopt_fp8.png + --output outputs/modelopt.png ``` Python API: @@ -112,7 +147,7 @@ Python API: from vllm_omni import Omni omni = Omni( - model="", + model="", tensor_parallel_size=2, force_cutlass_fp8=True, ) @@ -123,16 +158,34 @@ omni = Omni( | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `force_cutlass_fp8` / `--force-cutlass-fp8` | bool | `False` | Force CUTLASS FP8 linear kernels for supported ModelOpt FP8 diffusion stages on CUDA SM89+ | +| `--linear-backend cutlass` | str | auto | Select the validated CUTLASS linear backend for supported ModelOpt NVFP4 or mixed FP8/NVFP4 diffusion stages | +| `--moe-backend cutlass` | str | auto | Select the validated CUTLASS MoE backend for supported ModelOpt mixed MoE checkpoints | ## Validation and Notes -1. Compare the ModelOpt FP8 checkpoint against the BF16 baseline with the same +1. Compare the ModelOpt checkpoint against the BF16 baseline with the same prompt, resolution, seed, and inference steps. 2. Use `tests/diffusion/quantization/test_quantization_quality.py` with `VLLM_OMNI_QUALITY_CONFIGS` to validate local baseline and quantized model paths. -3. Report LPIPS, PSNR, MAE, throughput, latency, and peak memory when adding a - new validated ModelOpt diffusion checkpoint. -4. Keep `--quantization fp8` for online FP8 from BF16 checkpoints; use this - ModelOpt path only when the checkpoint already contains ModelOpt FP8 weights - and scales. +3. For HunyuanImage-3.0 quantized DiT checkpoints, the opt-in accuracy check is: + + ```bash + CUDA_VISIBLE_DEVICES=2,3 \ + HUNYUAN_IMAGE3_RUN_QUANT_ACCURACY=1 \ + HUNYUAN_IMAGE3_QUANT_DEVICES=0,1 \ + HUNYUAN_IMAGE3_QUANT_TP=2 \ + HUNYUAN_IMAGE3_BF16_MODEL=/path/to/hunyuan-image3-bf16 \ + HUNYUAN_IMAGE3_FP8_MODEL=/path/to/hunyuan-image3-modelopt-fp8 \ + HUNYUAN_IMAGE3_NVFP4_MODEL=/path/to/hunyuan-image3-modelopt-mixed-experts-nvfp4-dense-fp8 \ + PYTHONPATH=/path/to/vllm-omni:${PYTHONPATH:-} \ + python -m pytest -s -v \ + tests/e2e/accuracy/test_hunyuan_image3.py \ + -k quantized_dit_matches_bf16_accuracy + ``` + +4. Report CLIP score deltas, SSIM, PSNR, throughput, latency, and peak memory + when adding a new validated ModelOpt diffusion checkpoint. +5. Keep `--quantization fp8` for online FP8 from BF16 checkpoints; use this + ModelOpt path only when the checkpoint already contains ModelOpt quantized + weights and scales. diff --git a/mkdocs.yml b/mkdocs.yml index 6461c65f2..d19483002 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -120,7 +120,9 @@ plugins: inventories: - https://docs.python.org/3/objects.inv - https://typing-extensions.readthedocs.io/en/latest/objects.inv - - https://docs.aiohttp.org/en/stable/objects.inv + # Disabled because Read the Docs strict builds fail if this + # external inventory intermittently resets the connection. + # - https://docs.aiohttp.org/en/stable/objects.inv - https://pillow.readthedocs.io/en/stable/objects.inv - https://numpy.org/doc/stable/objects.inv # Temporarily disabled due to decompression errors diff --git a/recipes/Qwen/Qwen-Image.md b/recipes/Qwen/Qwen-Image.md index 327160973..c40d71c1e 100644 --- a/recipes/Qwen/Qwen-Image.md +++ b/recipes/Qwen/Qwen-Image.md @@ -130,3 +130,108 @@ and compare the output JSON or terminal metrics. - Key flags: `--step-execution` enables the step-wise runtime; `--max-num-seqs` controls how many compatible requests may stay active together. - Keep `--max-num-seqs 1` when you want the more conservative path, when traffic is mostly single-request, or when you are debugging correctness before measuring throughput. - Current batching is still shape-sensitive: different step progress can co-batch, but different resolutions do not yet co-batch. + +### 2x B200 ModelOpt mixed FP8/NVFP4 + +#### Environment + +- OS: Linux +- Python: 3.12 +- Driver / runtime: NVIDIA CUDA environment with two B200 GPUs +- vLLM version: Match the vLLM-Omni checkout used for deployment +- vLLM-Omni version or commit: Use the commit that contains ModelOpt mixed + FP8/NVFP4 checkpoint loading for diffusion models + +#### Command + +This configuration uses a pre-quantized ModelOpt mixed checkpoint for +Qwen-Image-2512. The validated checkpoint keeps boundary, modulation, +normalization, and output layers in BF16, uses FP8 for sensitive attention +projections, and uses NVFP4 for heavier MLP / attention-output linear layers. + +Validated checkpoint: + +```text +feizhai123/qwen-image-2512-modelopt-mixed-fp8-sensitive-nvfp4-heavy +``` + +Start the server with the CUTLASS quantized linear backend: + +```bash +CUDA_VISIBLE_DEVICES=2,3 \ +vllm serve feizhai123/qwen-image-2512-modelopt-mixed-fp8-sensitive-nvfp4-heavy \ + --omni \ + --host 0.0.0.0 \ + --port 8091 \ + --trust-remote-code \ + --tensor-parallel-size 2 \ + --linear-backend cutlass \ + --force-cutlass-fp8 +``` + +#### Verification + +Run a direct image generation request after the server is ready: + +```bash +curl http://localhost:8091/v1/images/generations \ + -H "Content-Type: application/json" \ + -d '{ + "model": "feizhai123/qwen-image-2512-modelopt-mixed-fp8-sensitive-nvfp4-heavy", + "prompt": "A cinematic Warhammer 40,000 Ultramarine warrior in blue power armor, armor stained with alien ichor, swinging a chainsword at a Tyranid creature, gothic battlefield, dramatic lighting, high detail, no text, no watermark", + "negative_prompt": "blurry, low quality, text, watermark, logo, malformed armor, bad hands", + "size": "1024x1024", + "num_inference_steps": 20, + "true_cfg_scale": 4.0, + "guidance_scale": 4.0, + "seed": 48 + }' +``` + +Check that: + +- The server responds on `http://localhost:8091/health`. +- The response contains one generated image. +- Logs show CUTLASS selection for ModelOpt FP8 / mixed-precision linear + layers instead of falling back to the default automatic backend choice. + +#### Benchmark + +The numbers below are loaded-once request-level measurements, not an HTTP +concurrency benchmark. Each case loads the model once, runs warmup requests, +then measures 100 sequential requests. + +```text +Hardware: 2x B200 +Tensor parallel size: 2 +Requests: 100 +Concurrency: 1 +Resolution: 1024x1024 +Denoising steps: 20 +``` + +| Config | Mean | P90 | P99 | Peak VRAM | +| --- | ---: | ---: | ---: | ---: | +| BF16 | 2.696s | 2.700s | 2.707s | 99,000 MiB | +| ModelOpt FP8 CUTLASS | 2.663s | 2.671s | 2.676s | 85,760 MiB | +| Mixed FP8/NVFP4 CUTLASS | 2.584s | 2.590s | 2.594s | 84,412 MiB | + +The same 2-GPU setup was also used to sweep NVFP4 linear backends. Direct +CUTLASS was the fastest validated backend for this checkpoint. + +| Backend | Mean | P90 | P99 | Peak VRAM | +| --- | ---: | ---: | ---: | ---: | +| auto | 2.927s | 2.948s | 3.535s | 85,340 MiB | +| flashinfer-cudnn | 3.523s | 3.528s | 3.766s | 84,500 MiB | +| flashinfer-trtllm | 2.711s | 2.727s | 3.010s | 84,996 MiB | +| flashinfer-cutlass | 2.935s | 2.950s | 3.011s | 85,340 MiB | +| cutlass | 2.578s | 2.584s | 2.592s | 84,412 MiB | + +#### Notes + +- Use `--linear-backend cutlass` to select the validated quantized linear + backend from the CLI. +- `--force-cutlass-fp8` keeps ModelOpt FP8 layers on the CUTLASS FP8 path. +- The benchmark table intentionally reports Qwen-Image-2512 as + concurrency-1 request-level data; do not compare it directly with an online + HTTP concurrency benchmark. diff --git a/recipes/README.md b/recipes/README.md index 6a49e6449..8ddb72943 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -33,13 +33,14 @@ recipes/ | [`inclusionAI/Ming-flash-omni-2.0.md`](./inclusionAI/Ming-flash-omni-2.0.md) | Online serving for multimodal chat + standalone TTS | 4x H100 / 1x H100 80GB | | [`LTX/LTX-2.md`](./LTX/LTX-2.md) | Text-to-video and image-to-video serving | 1x H200 141GB | | [`LTX/LTX-2.3.md`](./LTX/LTX-2.3.md) | Text-to-video with audio generation (22B) | 1x GPU (96GB VRAM) | +| [`Qwen/Qwen-Image.md`](./Qwen/Qwen-Image.md) | Text-to-image serving with step-wise continuous batching replay and ModelOpt mixed FP8/NVFP4 | 1x A100 80GB / 2x B200 | | [`Qwen/Qwen-Image.md`](./Qwen/Qwen-Image.md) | Text-to-image serving with step-wise continuous batching replay | 1x A100 80GB | | [`Qwen/Qwen-Image-Edit.md`](./Qwen/Qwen-Image-Edit.md) | Text-guided single-image editing | 1x or 2x H200 141GB | | [`Qwen/Qwen3-Omni.md`](./Qwen/Qwen3-Omni.md) | Online serving for multimodal chat | 1x A100 80GB | | [`Qwen/Qwen3-TTS.md`](./Qwen/Qwen3-TTS.md) | Text-to-speech serving (CustomVoice / VoiceDesign / Base) | 1x H100/A100 80GB | | [`SenseNova/SenseNova-U1.md`](./SenseNova/SenseNova-U1.md) | Unified image generation and understanding | 1x H200 (144GB) | | [`Tencent/Covo-Audio-Chat.md`](./Tencent/Covo-Audio-Chat.md) | Online serving for audio chat | 1x A100 80GB | -| [`Tencent/HunyuanImage-3.0-Instruct.md`](./Tencent/HunyuanImage-3.0-Instruct.md) | DiT-only text-to-image serving and benchmark | 4x H100/H800 80GB | +| [`Tencent/HunyuanImage-3.0-Instruct.md`](./Tencent/HunyuanImage-3.0-Instruct.md) | DiT-only text-to-image serving and benchmark, including ModelOpt mixed FP8/NVFP4 | 4x H100/H800 80GB / 2x B200 | | [`Wan-AI/Wan2.2-I2V.md`](./Wan-AI/Wan2.2-I2V.md) | Image-to-video serving (Wan2.2 14B) | 8x Ascend NPU (A2/A3) | | [`Wan-AI/Wan2.2-S2V.md`](./Wan-AI/Wan2.2-S2V.md) | Speech-to-video serving (Wan2.2 14B) | 2x A100/H100 80GB | diff --git a/recipes/Tencent/HunyuanImage-3.0-Instruct.md b/recipes/Tencent/HunyuanImage-3.0-Instruct.md index 9ccb37bed..3e6adf130 100644 --- a/recipes/Tencent/HunyuanImage-3.0-Instruct.md +++ b/recipes/Tencent/HunyuanImage-3.0-Instruct.md @@ -1,6 +1,8 @@ # HunyuanImage-3.0-Instruct -> DiT-only text-to-image serving and benchmark +> DiT-only text-to-image serving and benchmark with FP8, tensor parallelism, +> sequence parallelism, CFG parallelism, and ModelOpt mixed FP8/NVFP4 +> checkpoints. ## Summary @@ -16,13 +18,15 @@ Use this recipe when you want to run or benchmark the HunyuanImage-3.0 DiT stage directly. This is the recommended first setup when validating DiT throughput, memory, FP8 kernels, sequence parallelism, or CFG parallelism. -The recipe covers three 4-GPU configurations: +The recipe covers three 4-GPU FP8 configurations and one 2-GPU ModelOpt mixed +FP8/NVFP4 configuration: | Configuration | Parallelism | Notes | | --- | --- | --- | | `tp4_fp8` | TP=4 | Lowest per-GPU memory, higher communication overhead | | `tp2_fp8_sp2` | TP=2, SP=2, Ulysses=2 | Splits sequence work across two GPUs per TP group | | `tp2_fp8_cfgp2` | TP=2, CFG=2 | Runs CFG branches in parallel; fastest validated DiT setup | +| `tp2_mixed_fp8_nvfp4` | TP=2, EP enabled | Uses FP8 dense layers and NVFP4 routed experts on B200 | ## References @@ -222,3 +226,127 @@ of: - If you see OOM on 80GB GPUs, reduce image size, request concurrency, or use the TP=4 configuration before increasing batch size. GPU memory utilization is not a useful primary tuning knob for this DiT-only recipe. + +### 2x B200 ModelOpt mixed FP8/NVFP4 + +#### Environment + +- OS: Linux +- Python: 3.12 +- Driver / runtime: NVIDIA CUDA environment with two B200 GPUs +- vLLM version: Match the vLLM-Omni checkout used for deployment +- vLLM-Omni version or commit: Use the commit that contains ModelOpt mixed + FP8/NVFP4 checkpoint loading for diffusion models + +#### Command + +This configuration uses a pre-quantized ModelOpt mixed checkpoint for the +HunyuanImage-3.0 DiT stage. Dense attention and shared dense projections use +FP8, while routed MoE expert projections use NVFP4. Embeddings, norms, +routers, output layers, and non-DiT components stay in BF16. + +Validated checkpoint: + +```text +feizhai123/hunyuan-image3-modelopt-mixed-experts-nvfp4-dense-fp8 +``` + +Start the server with CUTLASS selected from the CLI for quantized linear and +MoE kernels: + +```bash +CUDA_VISIBLE_DEVICES=2,3 \ +vllm serve feizhai123/hunyuan-image3-modelopt-mixed-experts-nvfp4-dense-fp8 \ + --omni \ + --host 0.0.0.0 \ + --port 8091 \ + --trust-remote-code \ + --tensor-parallel-size 2 \ + --enable-expert-parallel \ + --linear-backend cutlass \ + --moe-backend cutlass \ + --force-cutlass-fp8 +``` + +#### Verification + +Run a direct image generation request after the server is ready: + +```bash +curl -s http://localhost:8091/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "feizhai123/hunyuan-image3-modelopt-mixed-experts-nvfp4-dense-fp8", + "messages": [ + {"role": "user", "content": "A cinematic portrait of an Adeptus Custodes warrior standing on Terra before the Imperial Palace, ornate golden armor, guardian spear, crimson cloak, marble steps, gothic architecture, holy golden light, highly detailed, no text, no watermark"} + ], + "extra_body": { + "height": 1024, + "width": 1024, + "num_inference_steps": 20, + "guidance_scale": 4.0, + "true_cfg_scale": 4.0, + "seed": 48 + } + }' | jq -r '.choices[0].message.content[0].image_url.url' \ + | cut -d',' -f2- \ + | base64 -d > hunyuan_image3_mixed_nvfp4.png +``` + +Check that: + +- The server responds on `http://localhost:8091/health`. +- The response contains one generated image. +- Logs show CUTLASS selection for ModelOpt FP8 dense layers and ModelOpt + NVFP4 routed expert layers. + +#### Benchmark + +The numbers below are online HTTP measurements with 16 concurrent requests. +Each case serves 100 image-generation requests on the same 2-GPU B200 setup. + +```text +Hardware: 2x B200 +Tensor parallel size: 2 +Requests: 100 +Concurrency: 16 +Resolution: 1024x1024 +Denoising steps: 20 +``` + +| Config | Mean | P90 | P99 | Peak VRAM | +| --- | ---: | ---: | ---: | ---: | +| BF16 | 70.729s | 77.716s | 77.758s | 191,920 MiB | +| ModelOpt FP8 | 58.603s | 64.435s | 64.469s | 115,816 MiB | +| Mixed FP8/NVFP4 | 54.739s | 60.244s | 60.317s | 83,560 MiB | + +#### Accuracy + +Run the HunyuanImage-3.0 quantized DiT accuracy check with the BF16, FP8, and +mixed FP8/NVFP4 checkpoints: + +```bash +CUDA_VISIBLE_DEVICES=2,3 \ +HUNYUAN_IMAGE3_QUANT_DEVICES=0,1 \ +HUNYUAN_IMAGE3_QUANT_TP=2 \ +HUNYUAN_IMAGE3_BF16_MODEL=/path/to/hunyuan-image3-bf16 \ +HUNYUAN_IMAGE3_FP8_MODEL=/path/to/hunyuan-image3-modelopt-fp8 \ +HUNYUAN_IMAGE3_NVFP4_MODEL=/path/to/hunyuan-image3-modelopt-mixed-experts-nvfp4-dense-fp8 \ +PYTHONPATH=/path/to/vllm-omni:/path/to/vllm:${PYTHONPATH:-} \ +python -m pytest -s -v \ + tests/e2e/accuracy/test_hunyuan_image3.py \ + -k quantized_dit_matches_bf16_accuracy +``` + +The test compares quantized images with the BF16 image using CLIP-based image +and text scores, plus structural SSIM / PSNR checks. The current structural +thresholds are `SSIM >= 0.20` and `PSNR >= 10.0`. + +#### Notes + +- Use `--linear-backend cutlass` to select the validated quantized linear + backend from the CLI. +- Use `--moe-backend cutlass` for the routed expert path. +- `--force-cutlass-fp8` keeps the FP8 dense layers on the CUTLASS FP8 path. +- The benchmark table is an online HTTP concurrency-16 result; do not compare + it directly with loaded-once request-level benchmarks. diff --git a/tests/e2e/accuracy/test_hunyuan_image3.py b/tests/e2e/accuracy/test_hunyuan_image3.py index 6541866d4..2145028b4 100644 --- a/tests/e2e/accuracy/test_hunyuan_image3.py +++ b/tests/e2e/accuracy/test_hunyuan_image3.py @@ -6,9 +6,11 @@ from __future__ import annotations import copy import gc import importlib +import json import os import tempfile import time +from dataclasses import dataclass from pathlib import Path import pytest @@ -19,16 +21,17 @@ from PIL import Image from tests.e2e.accuracy.helpers import ( CLIPScorer, SemanticSimilarityScorer, + assert_similarity, compute_image_ssim_psnr, download_images, model_output_dir, ) -from tests.helpers.mark import hardware_marks +from tests.helpers.mark import hardware_test from vllm_omni.diffusion.models.hunyuan_image3.prompt_utils import build_prompt_tokens, resolve_stop_token_ids os.environ["DIFFUSION_ATTENTION_BACKEND"] = "TORCH_SDPA" -pytestmark = [pytest.mark.local_model, pytest.mark.diffusion, *hardware_marks(res={"cuda": "H100"}, num_cards=8)] +pytestmark = [pytest.mark.local_model, pytest.mark.diffusion] # ============================================================================ # Configurable Parameters @@ -76,6 +79,18 @@ THRESHOLDS = { "ssim": 0.20, # Structural similarity "psnr": 11.0, # Peak signal-to-noise ratio (dB) } + +QUANT_PROMPT = "A brown and white dog is running on the grass." +QUANT_HEIGHT, QUANT_WIDTH = 1024, 1024 +QUANT_PSNR_THRESHOLD = 10.0 +QUANT_SSIM_THRESHOLD = 0.20 +QUANT_CLIP_SCORE_THRESHOLD = 20.0 +QUANT_CLIP_SCORE_DROP_THRESHOLD = float(os.environ.get("HUNYUAN_IMAGE3_QUANT_CLIP_SCORE_DROP_THRESHOLD", "5.0")) +QUANT_RUN_ENV = "HUNYUAN_IMAGE3_RUN_QUANT_ACCURACY" +QUANT_BF16_ENV = "HUNYUAN_IMAGE3_BF16_MODEL" +QUANT_FP8_ENV = "HUNYUAN_IMAGE3_FP8_MODEL" +QUANT_NVFP4_ENV = "HUNYUAN_IMAGE3_NVFP4_MODEL" +_TRUE_ENV_VALUES = {"1", "true", "yes", "on"} # fmt: off _BASE_CONFIG = { "stage_args": [ @@ -124,6 +139,64 @@ _BASE_CONFIG = { } # fmt: on +_QUANT_DIT_CONFIG = { + "stage_args": [ + { + "stage_id": 0, + "stage_type": "diffusion", + "runtime": { + "devices": "0,1", + "max_batch_size": 1, + }, + "engine_args": { + "model_stage": "dit", + "enforce_eager": True, + "trust_remote_code": True, + "distributed_executor_backend": "mp", + "force_cutlass_fp8": True, + "moe_backend": "cutlass", + "parallel_config": { + "tensor_parallel_size": 2, + "enable_expert_parallel": True, + }, + "omni_kv_config": {"need_recv_cache": True}, + }, + "final_output": True, + "final_output_type": "image", + "is_comprehension": False, + "default_sampling_params": {"seed": SEED}, + } + ], + "runtime": {"enabled": True}, +} + + +@dataclass(frozen=True) +class _QuantAccuracyCase: + name: str + model_env: str + nvfp4_backend: str | None = None + + +def _quant_accuracy_cases() -> list[pytest.ParameterSet]: + cases = [ + _QuantAccuracyCase(name="fp8", model_env=QUANT_FP8_ENV), + _QuantAccuracyCase(name="mixed_nvfp4", model_env=QUANT_NVFP4_ENV, nvfp4_backend="cutlass"), + ] + params: list[pytest.ParameterSet] = [] + run_quant_accuracy = os.environ.get(QUANT_RUN_ENV, "").lower() in _TRUE_ENV_VALUES + for case in cases: + marks = [] + if not run_quant_accuracy: + marks.append(pytest.mark.skip(reason=f"Set {QUANT_RUN_ENV}=1 to run HunyuanImage3 quant accuracy.")) + if not os.environ.get(QUANT_BF16_ENV): + marks.append(pytest.mark.skip(reason=f"Set {QUANT_BF16_ENV} to run HunyuanImage3 quant accuracy.")) + if not os.environ.get(case.model_env): + marks.append(pytest.mark.skip(reason=f"Set {case.model_env} to run this quant accuracy case.")) + params.append(pytest.param(case, id=case.name, marks=marks)) + return params + + # fmt: off COT_REF = ("首先,我分析所有输入图像:图像1是一个圆形的logo,设计现代且抽象。它由不同色调的蓝色(深蓝、中蓝、浅蓝)和白色构成,这些色块以流畅的曲线相互交织,形成一个动态的、类似旋涡或波浪的图案。整个logo是扁平化的矢量图形,背景为纯黑色。图像2展示了四个并排摆放的卡通动物造型冰箱贴," "它们被放置在灰色的织物背景上。这些冰箱贴的关键特征是其材质:它们具有光滑、高光的珐琅或烤漆质感,边缘有明显的金属包边,整体呈现出一种立体的、有厚度的实体感。用户的指令是“基于图一的logo,参考图二中冰箱贴的材质,制作一个新的冰箱贴”。这个指令要求将一个二维的平面设计(logo)" @@ -144,6 +217,21 @@ def _make_config(enable_kv_reuse: bool, path: Path) -> None: path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False)) +def _quant_devices() -> str: + return os.environ.get("HUNYUAN_IMAGE3_QUANT_DEVICES", "0,1") + + +def _quant_tensor_parallel_size() -> int: + return int(os.environ.get("HUNYUAN_IMAGE3_QUANT_TP", str(len(_quant_devices().split(","))))) + + +def _make_quant_dit_config(path: Path) -> None: + config = copy.deepcopy(_QUANT_DIT_CONFIG) + config["stage_args"][0]["runtime"]["devices"] = _quant_devices() + config["stage_args"][0]["engine_args"]["parallel_config"]["tensor_parallel_size"] = _quant_tensor_parallel_size() + path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False)) + + def _run_offline(stage_configs_path: str, output_path: Path) -> tuple[Image.Image, str, float]: from transformers import AutoTokenizer @@ -220,6 +308,65 @@ def _run_offline(stage_configs_path: str, output_path: Path) -> tuple[Image.Imag return image, cot_text, elapsed +def _extract_image(outputs) -> Image.Image: + assert outputs, "Pipeline produced no outputs" + for output in outputs: + images = getattr(output, "images", None) + request_output = getattr(output, "request_output", None) + if not images and request_output is not None: + images = getattr(request_output, "images", None) + if images: + image = images[0].convert("RGB") + image.load() + return image + raise AssertionError("Pipeline output had no images") + + +def _run_dit_model( + model: str, + stage_config_path: str, + output_path: Path, + *, + nvfp4_backend: str | None = None, +) -> tuple[Image.Image, float]: + from tests.helpers.runtime import OmniRunner + from vllm_omni.inputs.data import OmniDiffusionSamplingParams + from vllm_omni.platforms import current_omni_platform + + old_backend = os.environ.get("VLLM_NVFP4_GEMM_BACKEND") + if nvfp4_backend is not None: + os.environ["VLLM_NVFP4_GEMM_BACKEND"] = nvfp4_backend + + try: + with OmniRunner(model, stage_configs_path=stage_config_path, mode="text-to-image") as runner: + generator = torch.Generator(device=current_omni_platform.device_type or "cuda").manual_seed(SEED) + params = OmniDiffusionSamplingParams( + height=QUANT_HEIGHT, + width=QUANT_WIDTH, + seed=SEED, + generator=generator, + num_inference_steps=20, + guidance_scale=4.0, + guidance_scale_provided=True, + ) + t0 = time.perf_counter() + outputs = runner.omni.generate({"prompt": QUANT_PROMPT}, params) + elapsed = time.perf_counter() - t0 + image = _extract_image(outputs) + image.save(output_path) + return image, elapsed + finally: + if nvfp4_backend is not None: + if old_backend is None: + os.environ.pop("VLLM_NVFP4_GEMM_BACKEND", None) + else: + os.environ["VLLM_NVFP4_GEMM_BACKEND"] = old_backend + gc.collect() + if torch.accelerator.is_available(): + torch.accelerator.empty_cache() + + +@hardware_test(res={"cuda": "H100"}, num_cards=8) @pytest.mark.skipif( torch.accelerator.device_count() < AR_TP_SIZE + DIT_TP_SIZE, reason=f"Needs {AR_TP_SIZE + DIT_TP_SIZE}+ GPUs ({AR_TP_SIZE} AR + {DIT_TP_SIZE} DiT)", @@ -265,3 +412,88 @@ def test_image_to_image_alignment(accuracy_artifact_root: Path, accuracy_assets_ ) assert ssim_value >= THRESHOLDS["ssim"], f"SSIM {ssim_value:.4f} is below threshold {THRESHOLDS['ssim']}" assert psnr_value >= THRESHOLDS["psnr"], f"PSNR {psnr_value:.2f} dB is below threshold {THRESHOLDS['psnr']} dB" + + +@pytest.mark.parametrize("case", _quant_accuracy_cases()) +@pytest.mark.skipif(torch.accelerator.device_count() < 2, reason="Needs 2+ GPUs for HunyuanImage3 DiT") +def test_quantized_dit_matches_bf16_accuracy( + case: _QuantAccuracyCase, + accuracy_artifact_root: Path, +) -> None: + """Quantized DiT checkpoints should preserve prompt-aligned image quality.""" + output_dir = model_output_dir(accuracy_artifact_root, MODEL_NAME + "-quant") + bf16_model = os.environ[QUANT_BF16_ENV] + quant_model = os.environ[case.model_env] + + with tempfile.TemporaryDirectory() as tmpdir: + stage_config_path = Path(tmpdir) / "hunyuan_image3_quant_dit.yaml" + _make_quant_dit_config(stage_config_path) + + bf16_image, bf16_time = _run_dit_model( + bf16_model, + str(stage_config_path), + output_dir / "bf16.png", + ) + quant_image, quant_time = _run_dit_model( + quant_model, + str(stage_config_path), + output_dir / f"{case.name}.png", + nvfp4_backend=case.nvfp4_backend, + ) + + ssim_score, psnr_score = compute_image_ssim_psnr( + prediction=quant_image, + reference=bf16_image, + ) + assert_similarity( + model_name=f"{MODEL_NAME} {case.name} vs bf16", + vllm_image=quant_image, + diffusers_image=bf16_image, + ssim_threshold=QUANT_SSIM_THRESHOLD, + psnr_threshold=QUANT_PSNR_THRESHOLD, + width=QUANT_WIDTH, + height=QUANT_HEIGHT, + ) + + clip_scorer = CLIPScorer() + bf16_clip_score = clip_scorer.score(bf16_image, QUANT_PROMPT) + quant_clip_score = clip_scorer.score(quant_image, QUANT_PROMPT) + clip_score_drop = bf16_clip_score - quant_clip_score + + metrics = { + "case": case.name, + "bf16_model": bf16_model, + "quant_model": quant_model, + "prompt": QUANT_PROMPT, + "seed": SEED, + "height": QUANT_HEIGHT, + "width": QUANT_WIDTH, + "num_inference_steps": 20, + "guidance_scale": 4.0, + "bf16_elapsed_s": bf16_time, + "quant_elapsed_s": quant_time, + "ssim": ssim_score, + "psnr": psnr_score, + "bf16_clip_score": bf16_clip_score, + "quant_clip_score": quant_clip_score, + "clip_score_drop": clip_score_drop, + } + metrics_path = output_dir / f"{case.name}_metrics.json" + metrics_path.write_text(json.dumps(metrics, indent=2, sort_keys=True), encoding="utf-8") + + print(f"\nHunyuanImage3 quant accuracy ({case.name})") + print(f" bf16 model: {bf16_model}") + print(f" quant model: {quant_model}") + print(f" BF16 CLIP score: {bf16_clip_score:.4f}") + print(f" quant CLIP score: {quant_clip_score:.4f} threshold>={QUANT_CLIP_SCORE_THRESHOLD:.4f}") + print(f" CLIP score drop: {clip_score_drop:.4f} threshold<={QUANT_CLIP_SCORE_DROP_THRESHOLD:.4f}") + print(f" metrics: {metrics_path}") + + assert quant_clip_score >= QUANT_CLIP_SCORE_THRESHOLD, ( + f"{case.name} CLIP score below threshold: got {quant_clip_score:.4f}, " + f"expected >= {QUANT_CLIP_SCORE_THRESHOLD:.4f}" + ) + assert clip_score_drop <= QUANT_CLIP_SCORE_DROP_THRESHOLD, ( + f"{case.name} CLIP score drop too large: got {clip_score_drop:.4f}, " + f"expected <= {QUANT_CLIP_SCORE_DROP_THRESHOLD:.4f}" + ) diff --git a/tests/model_executor/models/test_encoder_quant_config.py b/tests/model_executor/models/test_encoder_quant_config.py index 47c7c69c7..28a1efa71 100644 --- a/tests/model_executor/models/test_encoder_quant_config.py +++ b/tests/model_executor/models/test_encoder_quant_config.py @@ -58,7 +58,8 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu] @pytest.mark.parametrize("method", sorted(PRE_QUANTIZED_METHODS)) def test_pre_quantized_returns_none(method: str) -> None: """visual_quant_config and audio_quant_config must be None for - pre-quantized methods (modelopt, modelopt_fp4, modelopt_mxfp8).""" + pre-quantized methods (modelopt, modelopt_fp4, modelopt_mxfp8, + modelopt_mixed).""" mock_config = MagicMock() mock_config.get_name.return_value = method @@ -100,5 +101,5 @@ def test_component_config_passed_through() -> None: def test_pre_quantized_methods_contains_expected() -> None: """Guard against accidental removal of a known pre-quantized method.""" - expected = {"modelopt", "modelopt_fp4", "modelopt_mxfp8"} + expected = {"modelopt", "modelopt_fp4", "modelopt_mxfp8", "modelopt_mixed"} assert PRE_QUANTIZED_METHODS == expected diff --git a/vllm_omni/diffusion/data.py b/vllm_omni/diffusion/data.py index c45ee63e7..98674ee2d 100644 --- a/vllm_omni/diffusion/data.py +++ b/vllm_omni/diffusion/data.py @@ -745,8 +745,11 @@ class OmniDiffusionConfig: return is_checkpoint_fp8 = bool(getattr(tf_config.quant_config, "is_checkpoint_fp8_serialized", False)) - should_use_checkpoint_config = self.quantization_config is None or ( - is_checkpoint_fp8 and self._is_generic_fp8_quant_config(self.quantization_config) + is_checkpoint_nvfp4 = bool(getattr(tf_config.quant_config, "is_checkpoint_nvfp4_serialized", False)) + should_use_checkpoint_config = ( + self.quantization_config is None + or (is_checkpoint_fp8 and self._is_generic_fp8_quant_config(self.quantization_config)) + or (is_checkpoint_nvfp4 and self._is_generic_nvfp4_quant_config(self.quantization_config)) ) if should_use_checkpoint_config: self.quantization_config = tf_config.quant_config @@ -766,6 +769,17 @@ class OmniDiffusionConfig: return quant_config.get_name() == "fp8" return False + @staticmethod + def _is_generic_nvfp4_quant_config(quant_config: object) -> bool: + if isinstance(quant_config, str): + return quant_config.lower() in {"fp4", "nvfp4", "modelopt_fp4"} + if isinstance(quant_config, Mapping): + method = quant_config.get("method", quant_config.get("quant_method")) + return isinstance(method, str) and method.lower() in {"fp4", "nvfp4", "modelopt_fp4"} + if hasattr(quant_config, "get_name"): + return quant_config.get_name() == "modelopt_fp4" + return False + def set_tf_model_config(self, tf_config: "TransformerConfig") -> None: """Assign `tf_model_config` and propagate quantization if detected. diff --git a/vllm_omni/diffusion/model_loader/checkpoint_adapters/__init__.py b/vllm_omni/diffusion/model_loader/checkpoint_adapters/__init__.py index cc20b2da4..584562193 100644 --- a/vllm_omni/diffusion/model_loader/checkpoint_adapters/__init__.py +++ b/vllm_omni/diffusion/model_loader/checkpoint_adapters/__init__.py @@ -2,7 +2,11 @@ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from torch import nn -from .modelopt import ModelOptFp8CheckpointAdapter +from .modelopt import ( + ModelOptFp8CheckpointAdapter, + ModelOptMixedPrecisionCheckpointAdapter, + ModelOptNvFp4CheckpointAdapter, +) def get_checkpoint_adapter( @@ -10,13 +14,19 @@ def get_checkpoint_adapter( source: object, quant_config: object | None, use_safetensors: bool, -) -> ModelOptFp8CheckpointAdapter | None: +) -> ModelOptFp8CheckpointAdapter | ModelOptNvFp4CheckpointAdapter | ModelOptMixedPrecisionCheckpointAdapter | None: if ModelOptFp8CheckpointAdapter.is_compatible(source, quant_config, use_safetensors): return ModelOptFp8CheckpointAdapter(model, source) + if ModelOptNvFp4CheckpointAdapter.is_compatible(source, quant_config, use_safetensors): + return ModelOptNvFp4CheckpointAdapter(model, source) + if ModelOptMixedPrecisionCheckpointAdapter.is_compatible(source, quant_config, use_safetensors): + return ModelOptMixedPrecisionCheckpointAdapter(model, source) return None __all__ = [ "ModelOptFp8CheckpointAdapter", + "ModelOptMixedPrecisionCheckpointAdapter", + "ModelOptNvFp4CheckpointAdapter", "get_checkpoint_adapter", ] diff --git a/vllm_omni/diffusion/model_loader/checkpoint_adapters/modelopt.py b/vllm_omni/diffusion/model_loader/checkpoint_adapters/modelopt.py index 6102a55e6..04fc8ea32 100644 --- a/vllm_omni/diffusion/model_loader/checkpoint_adapters/modelopt.py +++ b/vllm_omni/diffusion/model_loader/checkpoint_adapters/modelopt.py @@ -11,7 +11,7 @@ from vllm.model_executor.utils import get_packed_modules_mapping logger = init_logger(__name__) -MODEL_OPT_SCALE_SUFFIXES = (".input_scale", ".weight_scale", ".weight_scale_inv") +MODEL_OPT_SCALE_SUFFIXES = (".input_scale", ".weight_scale", ".weight_scale_2", ".weight_scale_inv") DEFAULT_PACKED_MODULES_MAPPING = { "to_qkv": ("to_q", "to_k", "to_v"), "add_kv_proj": ("add_q_proj", "add_k_proj", "add_v_proj"), @@ -249,3 +249,24 @@ class ModelOptFp8CheckpointAdapter: self._check_pending_weights(state) self._log_adaptation_summary(state) + + +class ModelOptNvFp4CheckpointAdapter(ModelOptFp8CheckpointAdapter): + @staticmethod + def _is_checkpoint_quant_config(quant_config: object | None) -> bool: + return ( + quant_config is not None + and hasattr(quant_config, "get_name") + and quant_config.get_name() == "modelopt_fp4" + and bool(getattr(quant_config, "is_checkpoint_nvfp4_serialized", False)) + ) + + +class ModelOptMixedPrecisionCheckpointAdapter(ModelOptFp8CheckpointAdapter): + @staticmethod + def _is_checkpoint_quant_config(quant_config: object | None) -> bool: + return ( + quant_config is not None + and hasattr(quant_config, "get_name") + and quant_config.get_name() == "modelopt_mixed" + ) diff --git a/vllm_omni/diffusion/models/helios/helios_transformer.py b/vllm_omni/diffusion/models/helios/helios_transformer.py index 5e7934c3b..ed0a50fee 100644 --- a/vllm_omni/diffusion/models/helios/helios_transformer.py +++ b/vllm_omni/diffusion/models/helios/helios_transformer.py @@ -3,6 +3,7 @@ # Adapted from Helios (https://github.com/BestWishYsh/Helios) import math +from collections import OrderedDict from collections.abc import Iterable from functools import lru_cache from typing import TYPE_CHECKING, Any @@ -447,21 +448,32 @@ class HeliosCrossAttention(nn.Module): causal=False, ) - def forward( - self, - hidden_states: torch.Tensor, - encoder_hidden_states: torch.Tensor, - ) -> torch.Tensor: - query = self.to_q(hidden_states) - query = self.norm_q(query) - + def project_kv(self, encoder_hidden_states: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: key = self.to_k(encoder_hidden_states) value = self.to_v(encoder_hidden_states) key = self.norm_k(key) - query = query.unflatten(2, (self.num_heads, self.head_dim)) key = key.unflatten(2, (self.num_heads, self.head_dim)) value = value.unflatten(2, (self.num_heads, self.head_dim)) + return key, value + + def forward( + self, + hidden_states: torch.Tensor, + encoder_hidden_states: torch.Tensor | None = None, + encoder_key_value: tuple[torch.Tensor, torch.Tensor] | None = None, + ) -> torch.Tensor: + query = self.to_q(hidden_states) + query = self.norm_q(query) + + if encoder_key_value is None: + if encoder_hidden_states is None: + raise ValueError("encoder_hidden_states is required when encoder_key_value is not provided.") + key, value = self.project_kv(encoder_hidden_states) + else: + key, value = encoder_key_value + + query = query.unflatten(2, (self.num_heads, self.head_dim)) hidden_states = self.attn(query, key, value) hidden_states = hidden_states.flatten(2, 3) @@ -526,6 +538,7 @@ class HeliosTransformerBlock(nn.Module): temb: torch.Tensor, rotary_emb: torch.Tensor, original_context_length: int | None = None, + cross_attn_key_value: tuple[torch.Tensor, torch.Tensor] | None = None, ) -> torch.Tensor: if temb.ndim == 4: shift_msa, scale_msa, gate_msa, c_shift_msa, c_scale_msa, c_gate_msa = ( @@ -556,12 +569,20 @@ class HeliosTransformerBlock(nn.Module): hidden_states[:, history_seq_len:], ) norm_hidden_states = self.norm2(current_hidden_states.float()).type_as(current_hidden_states) - attn_output = self.attn2(norm_hidden_states, encoder_hidden_states) + attn_output = self.attn2( + norm_hidden_states, + encoder_hidden_states, + encoder_key_value=cross_attn_key_value, + ) current_hidden_states = current_hidden_states + attn_output hidden_states = torch.cat([history_hidden_states, current_hidden_states], dim=1) else: norm_hidden_states = self.norm2(hidden_states.float()).type_as(hidden_states) - attn_output = self.attn2(norm_hidden_states, encoder_hidden_states) + attn_output = self.attn2( + norm_hidden_states, + encoder_hidden_states, + encoder_key_value=cross_attn_key_value, + ) hidden_states = hidden_states + attn_output # 3. Feed-forward @@ -723,11 +744,80 @@ class HeliosTransformer3DModel(nn.Module): # 5. Output norm & projection self.norm_out = HeliosOutputNorm(inner_dim, eps) self.proj_out = nn.Linear(inner_dim, out_channels * math.prod(patch_size)) + self._projected_encoder_cache: OrderedDict[tuple, torch.Tensor] = OrderedDict() + self._cross_attn_kv_cache: OrderedDict[tuple, list[tuple[torch.Tensor, torch.Tensor]]] = OrderedDict() + self._cross_attn_cache_size = 2 @property def dtype(self) -> torch.dtype: return next(self.parameters()).dtype + @staticmethod + def _tensor_cache_key(tensor: torch.Tensor) -> tuple: + try: + version = tensor._version + except RuntimeError: + version = None + + return ( + tensor.data_ptr(), + tuple(tensor.shape), + tuple(tensor.stride()), + tensor.dtype, + tensor.device.type, + tensor.device.index, + version, + ) + + @staticmethod + def _get_from_lru(cache: OrderedDict, key: tuple): + value = cache.get(key) + if value is not None: + cache.move_to_end(key) + return value + + def _put_lru(self, cache: OrderedDict, key: tuple, value) -> None: + cache[key] = value + cache.move_to_end(key) + while len(cache) > self._cross_attn_cache_size: + cache.popitem(last=False) + + def clear_cross_attention_cache(self) -> None: + self._projected_encoder_cache.clear() + self._cross_attn_kv_cache.clear() + + def _cache_enabled(self) -> bool: + return not self.training and not torch.is_grad_enabled() and not torch.compiler.is_compiling() + + def _project_encoder_hidden_states(self, encoder_hidden_states: torch.Tensor) -> torch.Tensor: + if not self._cache_enabled(): + return self.condition_embedder.text_embedder(encoder_hidden_states) + + cache_key = self._tensor_cache_key(encoder_hidden_states) + cached = self._get_from_lru(self._projected_encoder_cache, cache_key) + if cached is not None: + return cached + + projected = self.condition_embedder.text_embedder(encoder_hidden_states) + self._put_lru(self._projected_encoder_cache, cache_key, projected) + return projected + + def _get_cross_attn_key_values( + self, + encoder_hidden_states: torch.Tensor, + ) -> list[tuple[torch.Tensor, torch.Tensor]] | None: + if not self._cache_enabled(): + return None + + cache_key = self._tensor_cache_key(encoder_hidden_states) + cached = self._get_from_lru(self._cross_attn_kv_cache, cache_key) + if cached is not None: + return cached + + key_values = [block.attn2.project_kv(encoder_hidden_states) for block in self.blocks] + self._put_lru(self._cross_attn_kv_cache, cache_key, key_values) + return key_values + def forward( self, hidden_states: torch.Tensor, @@ -836,7 +926,12 @@ class HeliosTransformer3DModel(nn.Module): .expand(batch_size, -1, history_context_length, -1) ) - temb, timestep_proj, encoder_hidden_states = self.condition_embedder(timestep, encoder_hidden_states) + temb, timestep_proj, _ = self.condition_embedder( + timestep, + encoder_hidden_states, + is_return_encoder_hidden_states=False, + ) + encoder_hidden_states = self._project_encoder_hidden_states(encoder_hidden_states) timestep_proj = timestep_proj.unflatten(-1, (6, -1)) if indices_hidden_states is not None and not self.zero_history_timestep: @@ -857,14 +952,17 @@ class HeliosTransformer3DModel(nn.Module): hidden_states = hidden_states.contiguous() encoder_hidden_states = encoder_hidden_states.contiguous() rotary_emb = rotary_emb.contiguous() + cross_attn_key_values = self._get_cross_attn_key_values(encoder_hidden_states) - for block in self.blocks: + for block_idx, block in enumerate(self.blocks): + cross_attn_key_value = None if cross_attn_key_values is None else cross_attn_key_values[block_idx] hidden_states = block( hidden_states, encoder_hidden_states, timestep_proj, rotary_emb, original_context_length, + cross_attn_key_value, ) # 7. Output normalization diff --git a/vllm_omni/diffusion/models/hunyuan_image3/hunyuan_image3_transformer.py b/vllm_omni/diffusion/models/hunyuan_image3/hunyuan_image3_transformer.py index c39965f67..eaf0c2afe 100644 --- a/vllm_omni/diffusion/models/hunyuan_image3/hunyuan_image3_transformer.py +++ b/vllm_omni/diffusion/models/hunyuan_image3/hunyuan_image3_transformer.py @@ -2073,7 +2073,7 @@ class HunyuanImage3Model(nn.Module): return False def is_scalar_quant_scale(name: str, tensor: torch.Tensor) -> bool: - return tensor.numel() == 1 and name.endswith((".input_scale", ".weight_scale")) + return tensor.numel() == 1 and name.endswith((".input_scale", ".weight_scale", ".weight_scale_2")) def load_split_param( name: str, diff --git a/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py b/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py index 88a66d7f6..dac724162 100644 --- a/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py +++ b/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py @@ -49,6 +49,10 @@ from vllm_omni.diffusion.layers.rope import RotaryEmbedding logger = init_logger(__name__) +def _join_prefix(prefix: str, suffix: str) -> str: + return f"{prefix}.{suffix}" if prefix else suffix + + class ImageRopePrepare(nn.Module): """Prepares image hidden_states and RoPE embeddings for sequence parallel. @@ -471,7 +475,12 @@ class FeedForward(nn.Module): layers: list[nn.Module] = [ ColumnParallelApproxGELU( - dim, inner_dim, approximate="tanh", bias=bias, quant_config=quant_config, prefix=prefix + dim, + inner_dim, + approximate="tanh", + bias=bias, + quant_config=quant_config, + prefix=_join_prefix(prefix, "net.0.proj"), ), nn.Identity(), # placeholder for weight loading RowParallelLinear( @@ -480,7 +489,7 @@ class FeedForward(nn.Module): input_is_parallel=True, return_bias=False, quant_config=quant_config, - prefix=prefix, + prefix=_join_prefix(prefix, "net.2"), ), ] @@ -507,6 +516,7 @@ class QwenImageCrossAttention(nn.Module): context_pre_only: bool = False, out_dim: int | None = None, quant_config: QuantizationConfig | None = None, + prefix: str = "", ) -> None: super().__init__() assert dim % num_heads == 0 @@ -523,7 +533,7 @@ class QwenImageCrossAttention(nn.Module): head_size=self.head_dim, total_num_heads=num_heads, quant_config=quant_config, - prefix="to_qkv", + prefix=_join_prefix(prefix, "to_qkv"), ) self.query_num_heads = self.to_qkv.num_heads self.kv_num_heads = self.to_qkv.num_kv_heads @@ -539,7 +549,7 @@ class QwenImageCrossAttention(nn.Module): head_size=head_dim, total_num_heads=num_heads, quant_config=quant_config, - prefix="add_kv_proj", + prefix=_join_prefix(prefix, "add_kv_proj"), ) self.add_query_num_heads = self.add_kv_proj.num_heads self.add_kv_num_heads = self.add_kv_proj.num_kv_heads @@ -552,7 +562,7 @@ class QwenImageCrossAttention(nn.Module): input_is_parallel=True, return_bias=False, quant_config=quant_config, - prefix="to_add_out", + prefix=_join_prefix(prefix, "to_add_out"), ) assert not pre_only @@ -563,7 +573,7 @@ class QwenImageCrossAttention(nn.Module): input_is_parallel=True, return_bias=False, quant_config=quant_config, - prefix="to_out", + prefix=_join_prefix(prefix, "to_out"), ) self.norm_added_q = RMSNorm(head_dim, eps=eps) @@ -697,6 +707,7 @@ class QwenImageTransformerBlock(nn.Module): eps: float = 1e-6, zero_cond_t: bool = False, quant_config: QuantizationConfig | None = None, + prefix: str = "", ): super().__init__() @@ -716,7 +727,7 @@ class QwenImageTransformerBlock(nn.Module): bias=True, return_bias=False, quant_config=None, - prefix="img_mod.1", + prefix=_join_prefix(prefix, "img_mod.1"), ), ) self.img_norm1 = AdaLayerNorm(dim, elementwise_affine=False, eps=eps) @@ -727,9 +738,15 @@ class QwenImageTransformerBlock(nn.Module): context_pre_only=False, head_dim=attention_head_dim, quant_config=quant_config, + prefix=_join_prefix(prefix, "attn"), ) self.img_norm2 = AdaLayerNorm(dim, elementwise_affine=False, eps=eps) - self.img_mlp = FeedForward(dim=dim, dim_out=dim, quant_config=quant_config, prefix="img_mlp") + self.img_mlp = FeedForward( + dim=dim, + dim_out=dim, + quant_config=quant_config, + prefix=_join_prefix(prefix, "img_mlp"), + ) # Text processing modules. self.txt_mod = nn.Sequential( @@ -740,13 +757,18 @@ class QwenImageTransformerBlock(nn.Module): bias=True, return_bias=False, quant_config=None, - prefix="txt_mod.1", + prefix=_join_prefix(prefix, "txt_mod.1"), ), ) self.txt_norm1 = AdaLayerNorm(dim, elementwise_affine=False, eps=eps) # Text doesn't need separate attention - it's handled by img_attn joint computation self.txt_norm2 = AdaLayerNorm(dim, elementwise_affine=False, eps=eps) - self.txt_mlp = FeedForward(dim=dim, dim_out=dim, quant_config=quant_config, prefix="txt_mlp") + self.txt_mlp = FeedForward( + dim=dim, + dim_out=dim, + quant_config=quant_config, + prefix=_join_prefix(prefix, "txt_mlp"), + ) self.zero_cond_t = zero_cond_t @@ -996,8 +1018,9 @@ class QwenImageTransformer2DModel(CachedTransformer): attention_head_dim=attention_head_dim, zero_cond_t=zero_cond_t, quant_config=quant_config, + prefix=f"transformer_blocks.{i}", ) - for _ in range(num_layers) + for i in range(num_layers) ] ) diff --git a/vllm_omni/model_executor/models/hunyuan_image3/hunyuan_image3.py b/vllm_omni/model_executor/models/hunyuan_image3/hunyuan_image3.py index 3ed3e8f89..03e150409 100644 --- a/vllm_omni/model_executor/models/hunyuan_image3/hunyuan_image3.py +++ b/vllm_omni/model_executor/models/hunyuan_image3/hunyuan_image3.py @@ -84,7 +84,7 @@ from vllm.multimodal.processing import ( PromptUpdateDetails, ) from vllm.sequence import IntermediateTensors -from vllm.transformers_utils.tokenizer import get_tokenizer +from vllm.tokenizers import get_tokenizer from vllm.utils.tensor_schema import TensorSchema from vllm.v1.outputs import SamplerOutput from vllm.v1.sample.metadata import SamplingMetadata diff --git a/vllm_omni/quantization/component_config.py b/vllm_omni/quantization/component_config.py index f9286079b..25d1abf1e 100644 --- a/vllm_omni/quantization/component_config.py +++ b/vllm_omni/quantization/component_config.py @@ -23,10 +23,10 @@ if TYPE_CHECKING: ) -# Pre-quantized checkpoints (modelopt FP8/FP4/MXFP8) only quantize the +# Pre-quantized checkpoints (modelopt FP8/FP4/MXFP8/mixed) only quantize the # Thinker LM. Vision and audio encoder weights remain in BF16 with no # corresponding scale tensors in the checkpoint. -PRE_QUANTIZED_METHODS: frozenset[str] = frozenset({"modelopt", "modelopt_fp4", "modelopt_mxfp8"}) +PRE_QUANTIZED_METHODS: frozenset[str] = frozenset({"modelopt", "modelopt_fp4", "modelopt_mxfp8", "modelopt_mixed"}) def resolve_encoder_quant_config( diff --git a/vllm_omni/quantization/factory.py b/vllm_omni/quantization/factory.py index 3766e4596..19eafab22 100644 --- a/vllm_omni/quantization/factory.py +++ b/vllm_omni/quantization/factory.py @@ -138,11 +138,19 @@ def _normalize_quant_method_alias(method: str | None) -> str | None: _MODEL_OPT_METHODS = { "modelopt", + "modelopt_fp4", + "modelopt_mixed", } _MODEL_OPT_FP8_ALGOS = { "FP8", "FP8_PER_CHANNEL_PER_TOKEN", } +_MODEL_OPT_NVFP4_ALGOS = { + "NVFP4", +} +_MODEL_OPT_MIXED_ALGOS = { + "MIXED_PRECISION", +} def _normalize_method_name(method: Any) -> str: @@ -170,6 +178,10 @@ def _detect_modelopt_method(config: Mapping[str, Any]) -> str | None: if quant_algo: if quant_algo in _MODEL_OPT_FP8_ALGOS: return "modelopt" + if quant_algo in _MODEL_OPT_NVFP4_ALGOS: + return "modelopt_fp4" + if quant_algo in _MODEL_OPT_MIXED_ALGOS: + return "modelopt_mixed" return None if method is not None: From 8e937400dcc567e7a1af63b9d989cadb616115af Mon Sep 17 00:00:00 2001 From: Samit <285365963@qq.com> Date: Fri, 22 May 2026 11:12:29 +0800 Subject: [PATCH 004/112] Fix OmniDiffusionConfig master_port selection for parallel launches (#3803) --- .../test_omni_diffusion_config_master_port.py | 49 +++++++++++++++++++ vllm_omni/diffusion/data.py | 23 +++++++-- 2 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 tests/diffusion/test_omni_diffusion_config_master_port.py diff --git a/tests/diffusion/test_omni_diffusion_config_master_port.py b/tests/diffusion/test_omni_diffusion_config_master_port.py new file mode 100644 index 000000000..d65586021 --- /dev/null +++ b/tests/diffusion/test_omni_diffusion_config_master_port.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Unit tests for OmniDiffusionConfig master_port resolution (issue #3794).""" + +import socket + +import pytest + +from vllm_omni.diffusion.data import OmniDiffusionConfig + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + + +class TestOmniDiffusionConfigMasterPort: + def test_honors_master_port_env(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("MASTER_PORT", "43210") + config = OmniDiffusionConfig(model="test") + assert config.master_port == 43210 + + def test_honors_explicit_master_port(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("MASTER_PORT", raising=False) + config = OmniDiffusionConfig(model="test", master_port=40000) + assert config.master_port == 40000 + + def test_master_port_env_takes_precedence_over_kwarg(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("MASTER_PORT", "43210") + config = OmniDiffusionConfig(model="test", master_port=40000) + assert config.master_port == 43210 + + def test_explicit_master_port_is_stable_without_jitter(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("MASTER_PORT", raising=False) + ports = {OmniDiffusionConfig(model="test", master_port=40123).master_port for _ in range(5)} + assert ports == {40123} + + def test_default_master_port_is_available(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("MASTER_PORT", raising=False) + config = OmniDiffusionConfig(model="test") + assert config.master_port is not None + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(("", config.master_port)) + + def test_settle_port_steps_when_port_is_busy(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("MASTER_PORT", raising=False) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("", 0)) + busy_port = sock.getsockname()[1] + config = OmniDiffusionConfig(model="test", master_port=busy_port) + assert config.master_port == busy_port + 37 diff --git a/vllm_omni/diffusion/data.py b/vllm_omni/diffusion/data.py index 98674ee2d..9841aa2e3 100644 --- a/vllm_omni/diffusion/data.py +++ b/vllm_omni/diffusion/data.py @@ -606,6 +606,25 @@ class OmniDiffusionConfig: return False + def _resolve_master_port(self) -> int: + """Resolve torch.distributed master port without unnecessary random jitter. + + Precedence: + 1. ``MASTER_PORT`` environment variable (set by orchestrators for multi-replica launch). + 2. Explicit ``master_port`` passed at construction time. + 3. An OS-assigned ephemeral port when neither is provided. + """ + from vllm.utils.network_utils import get_open_port + + from vllm_omni.diffusion import envs + + env_port = envs.MASTER_PORT + if env_port is not None: + return self.settle_port(env_port, port_inc=37) + if self.master_port is not None: + return self.settle_port(self.master_port, port_inc=37) + return self.settle_port(get_open_port(), port_inc=37) + def settle_port(self, port: int, port_inc: int = 42, max_attempts: int = 100) -> int: """ Find an available port with retry logic. @@ -642,9 +661,7 @@ class OmniDiffusionConfig: ) def __post_init__(self): - # TODO: remove hard code - initial_master_port = (self.master_port or 30005) + random.randint(0, 100) - self.master_port = self.settle_port(initial_master_port, 37) + self.master_port = self._resolve_master_port() if isinstance(self.profiler_config, dict): from vllm.config import ProfilerConfig From 0ed3186db114700e058687eceaca45e9dee13604 Mon Sep 17 00:00:00 2001 From: qiye Date: Fri, 22 May 2026 11:32:14 +0800 Subject: [PATCH 005/112] [Bugfix] Remove stale OmniStage import and type annotation (#3541) Signed-off-by: Jianliang Qi --- vllm_omni/entrypoints/pd_utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vllm_omni/entrypoints/pd_utils.py b/vllm_omni/entrypoints/pd_utils.py index 413d5d6b4..bdc721ab0 100644 --- a/vllm_omni/entrypoints/pd_utils.py +++ b/vllm_omni/entrypoints/pd_utils.py @@ -13,8 +13,6 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from vllm import SamplingParams - from vllm_omni.entrypoints.omni_stage import OmniStage - _DEFAULT_MOONCAKE_BOOTSTRAP_PORT = 25201 logger = logging.getLogger(__name__) @@ -131,7 +129,7 @@ class PDDisaggregationMixin: p_stage = self.stage_configs[p_id] d_stage = self.stage_configs[d_id] - def _get_kv_cfg(stage: "OmniStage") -> dict[str, Any]: + def _get_kv_cfg(stage: Any) -> dict[str, Any]: ea = stage.engine_args cfg = getattr(ea, "kv_transfer_config", None) if cfg is None: From f0af3ab2005df0e1de743a23c7e473cce37db93d Mon Sep 17 00:00:00 2001 From: Zeng Chuang Date: Fri, 22 May 2026 14:29:46 +0800 Subject: [PATCH 006/112] [BugFix] Fix prefer_model_sampler token history in async scheduling (#3681) Signed-off-by: zengchuang Co-authored-by: Claude Opus 4.7 --- tests/e2e/accuracy/test_hunyuan_image3.py | 141 +++++++++--------- .../test_cosyvoice3_model_helpers.py | 19 ++- vllm_omni/worker/gpu_ar_model_runner.py | 2 +- 3 files changed, 90 insertions(+), 72 deletions(-) diff --git a/tests/e2e/accuracy/test_hunyuan_image3.py b/tests/e2e/accuracy/test_hunyuan_image3.py index 2145028b4..bc3fd7f4d 100644 --- a/tests/e2e/accuracy/test_hunyuan_image3.py +++ b/tests/e2e/accuracy/test_hunyuan_image3.py @@ -92,82 +92,87 @@ QUANT_FP8_ENV = "HUNYUAN_IMAGE3_FP8_MODEL" QUANT_NVFP4_ENV = "HUNYUAN_IMAGE3_NVFP4_MODEL" _TRUE_ENV_VALUES = {"1", "true", "yes", "on"} # fmt: off -_BASE_CONFIG = { - "stage_args": [ +_DEPLOY_CONFIG = { + "pipeline": "hunyuan_image_3_moe", + "async_chunk": False, + "trust_remote_code": True, + "connectors": { + "shared_memory_connector": { + "name": "SharedMemoryConnector", + "extra": {"shm_threshold_bytes": 65536}, + }, + }, + "stages": [ { - "stage_id": 0, "stage_type": "llm", - "runtime": {"process": True, "devices": AR_DEVICES, "max_batch_size": 1, "requires_multimodal_data": True}, - "engine_args": { - "model_stage": "AR", "model_arch": "HunyuanImage3ForCausalMM", - "worker_cls": "vllm_omni.worker.gpu_ar_worker.GPUARWorker", - "scheduler_cls": "vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler", - "gpu_memory_utilization": 0.95, "enforce_eager": True, "trust_remote_code": True, - "engine_output_type": "latent", "enable_prefix_caching": False, - "max_num_batched_tokens": 32768, "tensor_parallel_size": AR_TP_SIZE, "pipeline_parallel_size": 1, - "hf_overrides": {"rope_parameters": {"mrope_section": [0, 32, 32], "rope_type": "default"}}, - }, - "is_comprehension": False, "final_output": True, "final_output_type": "text", - "default_sampling_params": { - "temperature": 0.0, "top_p": 1, "top_k": -1, "max_tokens": 8192, - "stop_token_ids": [128025], "detokenize": True, "skip_special_tokens": False, + "stage_id": 0, + "is_comprehension": False, + "final_output": True, + "final_output_type": "text", + "max_num_seqs": 1, + "gpu_memory_utilization": 0.95, + "enforce_eager": True, + "trust_remote_code": True, + "max_num_batched_tokens": 32768, + "devices": AR_DEVICES, + "tensor_parallel_size": AR_TP_SIZE, + "hf_overrides": { + "rope_parameters": {"mrope_section": [0, 32, 32], "rope_type": "default"}, }, "output_connectors": {"to_stage_1": "shared_memory_connector"}, + "default_sampling_params": { + "temperature": 0.0, + "top_p": 1, + "top_k": -1, + "max_tokens": 8192, + "stop_token_ids": [128025], + "detokenize": True, + "skip_special_tokens": False, + }, }, { - "stage_id": 1, "stage_type": "diffusion", - "runtime": {"process": True, "devices": DIT_DEVICES, "max_batch_size": 1, "requires_multimodal_data": True}, - "engine_args": { - "model_stage": "dit", "model_arch": "HunyuanImage3ForCausalMM", - "enforce_eager": True, "trust_remote_code": True, "distributed_executor_backend": "mp", - "parallel_config": {"tensor_parallel_size": DIT_TP_SIZE, "enable_expert_parallel": True}, - }, - "engine_input_source": [0], - "custom_process_input_func": "vllm_omni.model_executor.stage_input_processors.hunyuan_image3.ar2diffusion", - "final_output": True, "final_output_type": "image", - "default_sampling_params": {"num_inference_steps": NUM_INFERENCE_STEPS, "guidance_scale": GUIDANCE_SCALE}, + "stage_id": 1, + "max_num_seqs": 1, + "enforce_eager": True, + "trust_remote_code": True, + "devices": DIT_DEVICES, + "distributed_executor_backend": "mp", + "parallel_config": {"tensor_parallel_size": DIT_TP_SIZE, "enable_expert_parallel": True}, "input_connectors": {"from_stage_0": "shared_memory_connector"}, + "default_sampling_params": { + "num_inference_steps": NUM_INFERENCE_STEPS, + "guidance_scale": GUIDANCE_SCALE, + }, }, ], - "runtime": { - "enabled": True, - "connectors": {"shared_memory_connector": { - "name": "SharedMemoryConnector", - "extra": {"shm_threshold_bytes": 65536} - }}, - "edges": [{"from": 0, "to": 1}], - }, + "edges": [{"from": 0, "to": 1}], } # fmt: on _QUANT_DIT_CONFIG = { - "stage_args": [ + "pipeline": "hunyuan_image_3_moe", + "async_chunk": False, + "trust_remote_code": True, + "stages": [ { "stage_id": 0, - "stage_type": "diffusion", - "runtime": { - "devices": "0,1", - "max_batch_size": 1, - }, - "engine_args": { - "model_stage": "dit", - "enforce_eager": True, - "trust_remote_code": True, - "distributed_executor_backend": "mp", - "force_cutlass_fp8": True, - "moe_backend": "cutlass", - "parallel_config": { - "tensor_parallel_size": 2, - "enable_expert_parallel": True, - }, - "omni_kv_config": {"need_recv_cache": True}, + "model_stage": "dit", + "enforce_eager": True, + "trust_remote_code": True, + "devices": "0,1", + "distributed_executor_backend": "mp", + "force_cutlass_fp8": True, + "moe_backend": "cutlass", + "parallel_config": { + "tensor_parallel_size": 2, + "enable_expert_parallel": True, }, + "omni_kv_config": {"need_recv_cache": True}, "final_output": True, "final_output_type": "image", "is_comprehension": False, "default_sampling_params": {"seed": SEED}, } ], - "runtime": {"enabled": True}, } @@ -211,9 +216,9 @@ COT_REF = ("首先,我分析所有输入图像:图像1是一个圆形的logo def _make_config(enable_kv_reuse: bool, path: Path) -> None: - config = copy.deepcopy(_BASE_CONFIG) - config["stage_args"][0]["engine_args"]["omni_kv_config"] = {"need_send_cache": enable_kv_reuse} - config["stage_args"][1]["engine_args"]["omni_kv_config"] = {"need_recv_cache": enable_kv_reuse} + config = copy.deepcopy(_DEPLOY_CONFIG) + config["stages"][0]["omni_kv_config"] = {"need_send_cache": enable_kv_reuse} + config["stages"][1]["omni_kv_config"] = {"need_recv_cache": enable_kv_reuse} path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False)) @@ -227,12 +232,12 @@ def _quant_tensor_parallel_size() -> int: def _make_quant_dit_config(path: Path) -> None: config = copy.deepcopy(_QUANT_DIT_CONFIG) - config["stage_args"][0]["runtime"]["devices"] = _quant_devices() - config["stage_args"][0]["engine_args"]["parallel_config"]["tensor_parallel_size"] = _quant_tensor_parallel_size() + config["stages"][0]["devices"] = _quant_devices() + config["stages"][0]["parallel_config"]["tensor_parallel_size"] = _quant_tensor_parallel_size() path.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False)) -def _run_offline(stage_configs_path: str, output_path: Path) -> tuple[Image.Image, str, float]: +def _run_offline(deploy_config_path: str, output_path: Path) -> tuple[Image.Image, str, float]: from transformers import AutoTokenizer from tests.helpers.runtime import OmniRunner @@ -251,7 +256,7 @@ def _run_offline(stage_configs_path: str, output_path: Path) -> tuple[Image.Imag system_prompt_type = result.system_prompt_type ar_stop_token_ids = resolve_stop_token_ids(task="it2i", bot_task="think_recaption", tokenizer=tokenizer) - with OmniRunner(MODEL_PATH, stage_configs_path=stage_configs_path) as runner: + with OmniRunner(MODEL_PATH, deploy_config=deploy_config_path) as runner: params_list = list(runner.omni.default_sampling_params_list) for sp in params_list: if isinstance(sp, OmniDiffusionSamplingParams): @@ -324,7 +329,7 @@ def _extract_image(outputs) -> Image.Image: def _run_dit_model( model: str, - stage_config_path: str, + deploy_config_path: str, output_path: Path, *, nvfp4_backend: str | None = None, @@ -338,7 +343,7 @@ def _run_dit_model( os.environ["VLLM_NVFP4_GEMM_BACKEND"] = nvfp4_backend try: - with OmniRunner(model, stage_configs_path=stage_config_path, mode="text-to-image") as runner: + with OmniRunner(model, deploy_config=deploy_config_path, mode="text-to-image") as runner: generator = torch.Generator(device=current_omni_platform.device_type or "cuda").manual_seed(SEED) params = OmniDiffusionSamplingParams( height=QUANT_HEIGHT, @@ -426,17 +431,17 @@ def test_quantized_dit_matches_bf16_accuracy( quant_model = os.environ[case.model_env] with tempfile.TemporaryDirectory() as tmpdir: - stage_config_path = Path(tmpdir) / "hunyuan_image3_quant_dit.yaml" - _make_quant_dit_config(stage_config_path) + deploy_config_path = Path(tmpdir) / "hunyuan_image3_quant_dit.yaml" + _make_quant_dit_config(deploy_config_path) bf16_image, bf16_time = _run_dit_model( bf16_model, - str(stage_config_path), + str(deploy_config_path), output_dir / "bf16.png", ) quant_image, quant_time = _run_dit_model( quant_model, - str(stage_config_path), + str(deploy_config_path), output_dir / f"{case.name}.png", nvfp4_backend=case.nvfp4_backend, ) diff --git a/tests/model_executor/models/cosyvoice3/test_cosyvoice3_model_helpers.py b/tests/model_executor/models/cosyvoice3/test_cosyvoice3_model_helpers.py index bc6a26466..956d32528 100644 --- a/tests/model_executor/models/cosyvoice3/test_cosyvoice3_model_helpers.py +++ b/tests/model_executor/models/cosyvoice3/test_cosyvoice3_model_helpers.py @@ -406,6 +406,9 @@ def test_gpu_ar_model_runner_prefers_model_sampler_when_opted_in(): self.updated = False def update_async_output_token_ids(self): + # After PR 3681 fix, update_async_output_token_ids is called + # BEFORE model sampler path to ensure async placeholder repair + # runs for all sampling paths self.updated = True _, GPUARModelRunner = _cosyvoice3_model_and_runner() @@ -420,7 +423,7 @@ def test_gpu_ar_model_runner_prefers_model_sampler_when_opted_in(): out = runner._sample(torch.tensor([[0.1, 0.2]], dtype=torch.float32), spec_decode_metadata=None) assert out is expected - assert runner.input_batch.updated is False + assert runner.input_batch.updated is True assert len(calls) == 1 @@ -436,9 +439,13 @@ def test_gpu_ar_model_runner_supplies_req_output_history_to_model_sampler(): self.sampled_token_ids_cpu = None self.async_copy_ready_event = None self.prev_req_id_to_index = None + self.update_async_called = False def update_async_output_token_ids(self): - raise AssertionError("fallback async repair should not run for model sampler path") + # After PR 3681 fix, update_async_output_token_ids is called + # BEFORE model sampler path to ensure async placeholder repair + # runs for all sampling paths + self.update_async_called = True _, GPUARModelRunner = _cosyvoice3_model_and_runner() runner = object.__new__(GPUARModelRunner) @@ -454,6 +461,7 @@ def test_gpu_ar_model_runner_supplies_req_output_history_to_model_sampler(): runner._sample(torch.tensor([[0.1, 0.2]], dtype=torch.float32), spec_decode_metadata=None) + assert runner.input_batch.update_async_called is True assert seen_histories == [[[1, 2, 3]]] @@ -476,9 +484,13 @@ def test_gpu_ar_model_runner_repairs_async_placeholders_for_model_sampler(): self.sampled_token_ids_cpu = torch.tensor([[29]], dtype=torch.int32) self.async_copy_ready_event = _ReadyEvent() self.prev_req_id_to_index = {"rid-1": 0} + self.update_async_called = False def update_async_output_token_ids(self): - raise AssertionError("fallback async repair should not run for model sampler path") + # After PR 3681 fix, update_async_output_token_ids is called + # BEFORE model sampler path to ensure async placeholder repair + # runs for all sampling paths (model sampler + fallback sampler) + self.update_async_called = True _, GPUARModelRunner = _cosyvoice3_model_and_runner() runner = object.__new__(GPUARModelRunner) @@ -495,4 +507,5 @@ def test_gpu_ar_model_runner_repairs_async_placeholders_for_model_sampler(): runner._sample(torch.tensor([[0.1, 0.2]], dtype=torch.float32), spec_decode_metadata=None) assert runner.input_batch.async_copy_ready_event.synced is True + assert runner.input_batch.update_async_called is True assert seen_histories == [[[11, 29]]] diff --git a/vllm_omni/worker/gpu_ar_model_runner.py b/vllm_omni/worker/gpu_ar_model_runner.py index c4ba29a35..2bcd4ba01 100644 --- a/vllm_omni/worker/gpu_ar_model_runner.py +++ b/vllm_omni/worker/gpu_ar_model_runner.py @@ -709,6 +709,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin): sampling_metadata = self.input_batch.sampling_metadata if spec_decode_metadata is None: model_sample = getattr(self.model, "sample", None) + self.input_batch.update_async_output_token_ids() if logits is not None and callable(model_sample) and getattr(self.model, "prefer_model_sampler", False): # Apply logit bias (min_tokens, allowed_token_ids) before # the custom model sampler — the standard GPU sampler does @@ -726,7 +727,6 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin): ) if sampler_output is not None: return sampler_output - self.input_batch.update_async_output_token_ids() return self.sampler( logits=logits, sampling_metadata=sampling_metadata, From 28467dc19a8c453105ce30c8e97537d2d265dd00 Mon Sep 17 00:00:00 2001 From: ANHDY <49194355+ANHDY@users.noreply.github.com> Date: Fri, 22 May 2026 15:23:03 +0800 Subject: [PATCH 007/112] [feature]: support Hidream-I1-Full model (#2572) Signed-off-by: wangmengdi <160612767@qq.com> Co-authored-by: wangmengdi --- docs/models/supported_models.md | 2 + docs/user_guide/diffusion_features.md | 1 + .../offline_inference/text_to_image/README.md | 20 + .../text_to_image/text_to_image.py | 7 + pyproject.toml | 2 + recipes/HiDream/HiDream-I1-Image.md | 106 ++ .../test_hidream_i1_full_expansion.py | 69 ++ vllm_omni/diffusion/data.py | 5 +- .../models/hidream_image/__init__.py | 17 + .../hidream_image_transformer.py | 1042 +++++++++++++++++ .../pipeline_hidream_i1_image.py | 1027 ++++++++++++++++ .../hidream_image/pipeline_hidream_image.py | 1027 ++++++++++++++++ vllm_omni/diffusion/registry.py | 6 + vllm_omni/engine/async_omni_engine.py | 4 + vllm_omni/entrypoints/cli/serve.py | 10 + 15 files changed, 3344 insertions(+), 1 deletion(-) create mode 100644 recipes/HiDream/HiDream-I1-Image.md create mode 100644 tests/e2e/online_serving/test_hidream_i1_full_expansion.py create mode 100644 vllm_omni/diffusion/models/hidream_image/__init__.py create mode 100644 vllm_omni/diffusion/models/hidream_image/hidream_image_transformer.py create mode 100644 vllm_omni/diffusion/models/hidream_image/pipeline_hidream_i1_image.py create mode 100644 vllm_omni/diffusion/models/hidream_image/pipeline_hidream_image.py diff --git a/docs/models/supported_models.md b/docs/models/supported_models.md index f30f34758..b0637a5d7 100644 --- a/docs/models/supported_models.md +++ b/docs/models/supported_models.md @@ -71,4 +71,6 @@ th { | `CovoAudioForConditionalGeneration` | Covo-Audio-Chat | `tencent/Covo-Audio-Chat` | ✅︎ | | | | |`DyninOmniForConditionalGeneration` | Dynin-Omni | `snu-aidas/Dynin-Omni` | ✅︎ | | | | | `ErnieImagePipeline` | ERNIE-Image | `baidu/ERNIE-Image`, `baidu/ERNIE-Image-Turbo` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | +|`HiDreamImagePipeline` | HiDream-I1-Full | `HiDream-ai/HiDream-I1-Full` | ✅︎ | ✅︎ | | | + ✅︎ indicates the model is supported on that backend. Empty cells mean not listed as supported on that backend. diff --git a/docs/user_guide/diffusion_features.md b/docs/user_guide/diffusion_features.md index 61694ac9d..2a296a059 100644 --- a/docs/user_guide/diffusion_features.md +++ b/docs/user_guide/diffusion_features.md @@ -115,6 +115,7 @@ The following tables show which models support each feature: | **FLUX.1-Kontext-dev** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | | **FLUX.2-dev** | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | | **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| **Hidream-I1-Full** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | | **LongCat-Image** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | | **LongCat-Image-Edit** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | diff --git a/examples/offline_inference/text_to_image/README.md b/examples/offline_inference/text_to_image/README.md index c71773972..02538b40f 100644 --- a/examples/offline_inference/text_to_image/README.md +++ b/examples/offline_inference/text_to_image/README.md @@ -35,6 +35,7 @@ This folder provides several entrypoints for experimenting with text-to-image di | `black-forest-labs/FLUX.2-klein-9B` | 1024 x 1024 | 37.1 | 32.3 | | `black-forest-labs/FLUX.2-dev` | 1024 x 1024 | 65.7 | >80 (CPU offload required) | | `HunyuanImage-3.0` | 1024 x 1024 | 80.0 (TP≥3) | 160 | +| `HiDream-I1-Full` | 1024 x 1024 | 63.7 | 57.7 | !!! info *Peak VRAM: based on basic single-card usage, batch size =1, without any acceleration/optimization features. FLUX.2-dev requires `--enable-cpu-offload` on a single 80 GiB GPU. @@ -94,6 +95,7 @@ python text_to_image.py \ | `--lora-scale` | float | `1.0` | Scale factor for LoRA weights | | `--use-system-prompt` | str | `None` | System prompt preset: `en_unified`, `en_vanilla`, `en_recaption`, `en_think_recaption`, `dynamic`, `None`, or custom text. Recommended: `en_unified`. Only for HunyuanImage-3.0.| | `--system-prompt` | str | `None` | Custom system prompt text. Only used when `--use-system-prompt` is set to `custom`. Only for HunyuanImage-3.0.| +| `--auxiliary-text-encoder` | str | `None` | Supplementary auxiliary text encoder parameters model name or path (especially for Hidream-l1-full). | **NextStep-1.1 specific arguments:** @@ -177,6 +179,24 @@ python examples/offline_inference/text_to_image/text_to_image.py \ --output flux2-dev.png ``` + +### HiDream-I1-Full Models + +The `--auxiliary-text-encoder` parameter is required when running HiDream‑I1‑Full: + +```bash +python examples/offline_inference/text_to_image/text_to_image.py \ + --model HiDream-ai/HiDream-I1-Full \ + --prompt "The setting sun of late autumn dyes the riverside with a warm orange hue" \ + --seed 42 \ + --guidance-scale 5.0 \ + --tensor-parallel-size 1 \ + --num-images-per-prompt 1 \ + --num-inference-steps 50 \ + --auxiliary-text-encoder meta-llama/Meta-Llama-3.1-8B-Instruct \ + --output /output.png +``` + ### Batch Requests (Multiple Prompts) You can pass multiple prompts in a single `generate` call. diff --git a/examples/offline_inference/text_to_image/text_to_image.py b/examples/offline_inference/text_to_image/text_to_image.py index c0fd337bd..443b21acc 100644 --- a/examples/offline_inference/text_to_image/text_to_image.py +++ b/examples/offline_inference/text_to_image/text_to_image.py @@ -300,6 +300,12 @@ def parse_args() -> argparse.Namespace: default=None, help=("Custom system prompt. Used when --use-system-prompt is custom. "), ) + parser.add_argument( + "--auxiliary-text-encoder", + type=str, + default=None, + help="Supplementary auxiliary text encoder parameters model name or path (especially for Hidream-l1-full).", + ) current_omni_platform.pre_register_and_update(parser) from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults @@ -392,6 +398,7 @@ def main(): "profiler_config": args.profiler_config, "init_timeout": args.init_timeout, "stage_init_timeout": args.stage_init_timeout, + "auxiliary_text_encoder": args.auxiliary_text_encoder, **lora_args, **quant_kwargs, } diff --git a/pyproject.toml b/pyproject.toml index bb9cdc688..e2c9242b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -241,6 +241,8 @@ extend-ignore-identifiers-re = [ ".*MoBA", ".*temperal_downsample", ".*_nd", + ".*nd$", + ".*EmbedND$", ".*nothink.*", ".*NOTHINK.*", ".*nin.*", diff --git a/recipes/HiDream/HiDream-I1-Image.md b/recipes/HiDream/HiDream-I1-Image.md new file mode 100644 index 000000000..958d03d6d --- /dev/null +++ b/recipes/HiDream/HiDream-I1-Image.md @@ -0,0 +1,106 @@ +# HiDream-I1-Full + +> Text-to-image serving and benchmark + +## Summary + +- Vendor: HiDream.ai +- Model: `HiDream-ai/HiDream-I1-Full` +- Task: Text-to-image generation +- Mode: Online serving with the OpenAI-compatible API +- Maintainer: Community + +## When to use this recipe + +Use this recipe when you want a known-good starting point for serving +`HiDream-ai/HiDream-I1-Full` on a single 80 GB A800, validate the normal online-serving path. + +## References + +- Model: +- Upstream or canonical docs: + [`docs/user_guide/examples/online_serving/text_to_image.md`](../../docs/user_guide/examples/online_serving/text_to_image.md) +- Related example under `examples/`: + [`examples/online_serving/text_to_image/README.md`](../../examples/online_serving/text_to_image/README.md) + + +## Hardware Support + +This recipe currently documents one CUDA GPU serving configuration. + +## GPU + +### 1x A800 80GB + +#### Environment + +- OS: Linux +- Python: 3.10+ +- Driver / runtime: NVIDIA CUDA environment with an A100 80 GB GPU +- vLLM version: Match the repository requirements for your checkout +- vLLM-Omni version or commit: Use the commit you are deploying from + +#### Command + +Start the baseline server: + +```bash +export MODEL_NAME_OR_PATH=HiDream-ai/HiDream-I1-Full +vllm serve ${MODEL_NAME_OR_PATH} \ + --omni \ + --port 8092 \ + --auxiliary-text-encoder meta-llama/Llama-3.1-8B-Instruct \ + --tensor-parallel-size 1 \ + --vae_use_slicing \ + --vae_use_tiling +``` + + +You can also use the example launcher and pass the extra flags through: + +```bash +python examples/offline_inference/text_to_image/text_to_image.py \ + --model HiDream-ai/HiDream-I1-Full \ + --prompt "The setting sun of late autumn dyes the riverside with a warm orange hue" \ + --seed 42 \ + --guidance-scale 5.0 \ + --tensor-parallel-size 1 \ + --num-images-per-prompt 1 \ + --num-inference-steps 50 \ + --auxiliary-text-encoder meta-llama/Llama-3.1-8B-Instruct \ + --output /workspace/mnt/cmss-wmd/result/output.png + +``` + +#### Verification + +Run the existing client example after the server is ready: + +```bash +python examples/online_serving/text_to_image/openai_chat_client.py \ + --server http://localhost:8092 \ + --prompt "The setting sun of late autumn dyes the riverside with a warm orange hue" \ + --output /tmp/hidream_i1_recipe.png +``` + +For a direct API smoke test: + +```bash +curl -s http://localhost:8092/v1/chat/completions -H "Content-Type: application/json" -d '{ + "messages": [ + {"role": "user", "content": "The setting sun of late autumn dyes the riverside with a warm orange hue"} + ], + "extra_body": { + "num_inference_steps": 50, + "seed": 42, + "height": 1024, + "width": 1024 + } + }' | jq -r '.choices[0].message.content[0].image_url.url' | cut -d',' -f2- | base64 -d > output_online.png +``` + + + +#### Notes + +- Key flags: `--auxiliary-text-encoder` designates the path for the auxiliary text model meta-llama/Llama-3.1-8B-Instruct. For HiDream-I1-Full, unspecified use defaults meta-llama/Llama-3.1-8B-Instruct (downloaded from official Hugging Face), and custom paths are supported. diff --git a/tests/e2e/online_serving/test_hidream_i1_full_expansion.py b/tests/e2e/online_serving/test_hidream_i1_full_expansion.py new file mode 100644 index 000000000..f84cc08ad --- /dev/null +++ b/tests/e2e/online_serving/test_hidream_i1_full_expansion.py @@ -0,0 +1,69 @@ +""" +End-to-end diffusion coverage for HiDream-I1-Full in online serving mode. + +This test verifies that HiDream-I1-Full can be launched, +accepts text-to-image requests through the OpenAI-compatible API, and returns +valid generated images with the requested resolution. + +assert_diffusion_response validates successful generation and the expected +image resolution. +""" + +import pytest + +from tests.helpers.mark import hardware_marks +from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data + +pytestmark = [pytest.mark.diffusion, pytest.mark.full_model] + +MODEL = "HiDream-ai/HiDream-I1-Full" +PROMPT = "A cinematic mountain landscape at sunrise, dramatic clouds, ultra-detailed, realistic photography." +NEGATIVE_PROMPT = "low quality, blurry, distorted, deformed, watermark" + +SINGLE_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"}) + + +def _get_hidream_i1_image_feature_cases(model: str): + """Return HiDream-I1-Full diffusion feature cases.""" + + return [ + pytest.param( + OmniServerParams( + model=model, + server_args=[ + "--auxiliary-text-encoder", + "meta-llama/Meta-Llama-3.1-8B-Instruct", + ], + ), + id="default", + marks=SINGLE_CARD_FEATURE_MARKS, + ), + ] + + +@pytest.mark.parametrize( + "omni_server", + _get_hidream_i1_image_feature_cases(MODEL), + indirect=True, +) +def test_hidream_i1_image( + omni_server: OmniServer, + openai_client: OpenAIClientHandler, +): + """Validate HiDream-I1-Full online serving with CPU offload.""" + + messages = dummy_messages_from_mix_data(content_text=PROMPT) + + request_config = { + "model": omni_server.model, + "messages": messages, + "extra_body": { + "height": 512, + "width": 512, + "num_inference_steps": 2, + "negative_prompt": NEGATIVE_PROMPT, + "seed": 42, + }, + } + + openai_client.send_diffusion_request(request_config) diff --git a/vllm_omni/diffusion/data.py b/vllm_omni/diffusion/data.py index 9841aa2e3..a6fe1e4e9 100644 --- a/vllm_omni/diffusion/data.py +++ b/vllm_omni/diffusion/data.py @@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any import diffusers import torch from PIL import Image -from pydantic import model_validator +from pydantic import Field, model_validator from typing_extensions import Self from vllm.config.utils import config from vllm.logger import init_logger @@ -593,6 +593,9 @@ class OmniDiffusionConfig: # Maximum number of sequences to generate in a batch max_num_seqs: int = 1 + # Supplementary model specific parameters + extras: dict[str, Any] = Field(default_factory=dict) + @property def is_moe(self) -> bool: num_experts = self.tf_model_config.get("num_experts", None) diff --git a/vllm_omni/diffusion/models/hidream_image/__init__.py b/vllm_omni/diffusion/models/hidream_image/__init__.py new file mode 100644 index 000000000..d8c52afdd --- /dev/null +++ b/vllm_omni/diffusion/models/hidream_image/__init__.py @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""HiDream Image diffusion model components.""" + +from vllm_omni.diffusion.models.hidream_image.hidream_image_transformer import ( + HiDreamImageTransformer2DModel, +) +from vllm_omni.diffusion.models.hidream_image.pipeline_hidream_image import ( + HiDreamImagePipeline, + get_hidream_image_post_process_func, +) + +__all__ = [ + "HiDreamImageTransformer2DModel", + "HiDreamImagePipeline", + "get_hidream_image_post_process_func", +] diff --git a/vllm_omni/diffusion/models/hidream_image/hidream_image_transformer.py b/vllm_omni/diffusion/models/hidream_image/hidream_image_transformer.py new file mode 100644 index 000000000..5c12df14e --- /dev/null +++ b/vllm_omni/diffusion/models/hidream_image/hidream_image_transformer.py @@ -0,0 +1,1042 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + + +from collections.abc import Iterable +from typing import TYPE_CHECKING + +import torch +import torch.nn as nn +import torch.nn.functional as F + +if TYPE_CHECKING: + from vllm.model_executor.layers.quantization.base_config import ( + QuantizationConfig, + ) + +from diffusers.models.embeddings import TimestepEmbedding, Timesteps +from diffusers.models.modeling_outputs import Transformer2DModelOutput +from diffusers.utils import deprecate, logging +from vllm.distributed import ( + get_tensor_model_parallel_rank, + get_tensor_model_parallel_world_size, + tensor_model_parallel_all_reduce, +) +from vllm.model_executor.layers.linear import ( + QKVParallelLinear, + RowParallelLinear, +) +from vllm.model_executor.model_loader.weight_utils import default_weight_loader + +from vllm_omni.diffusion.data import OmniDiffusionConfig +from vllm_omni.diffusion.layers.rope import RotaryEmbedding + +logger = logging.get_logger(__name__) + + +class HiDreamImageFeedForwardSwiGLU(nn.Module): + def __init__( + self, + dim: int, + hidden_dim: int, + multiple_of: int = 256, + ffn_dim_multiplier: float | None = None, + ): + super().__init__() + hidden_dim = int(2 * hidden_dim / 3) + # custom dim factor multiplier + if ffn_dim_multiplier is not None: + hidden_dim = int(ffn_dim_multiplier * hidden_dim) + hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of) + + self.w1 = nn.Linear(dim, hidden_dim, bias=False) + self.w2 = nn.Linear(hidden_dim, dim, bias=False) + self.w3 = nn.Linear(dim, hidden_dim, bias=False) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.w2(torch.nn.functional.silu(self.w1(x)) * self.w3(x)) + + +class HiDreamImagePooledEmbed(nn.Module): + def __init__(self, text_emb_dim, hidden_size): + super().__init__() + self.pooled_embedder = TimestepEmbedding(in_channels=text_emb_dim, time_embed_dim=hidden_size) + + def forward(self, pooled_embed: torch.Tensor) -> torch.Tensor: + # + return self.pooled_embedder(pooled_embed.to(dtype=torch.bfloat16)) + + +class HiDreamImageTimestepEmbed(nn.Module): + def __init__(self, hidden_size, frequency_embedding_size=256): + super().__init__() + self.time_proj = Timesteps(num_channels=frequency_embedding_size, flip_sin_to_cos=True, downscale_freq_shift=0) + self.timestep_embedder = TimestepEmbedding(in_channels=frequency_embedding_size, time_embed_dim=hidden_size) + + def forward(self, timesteps: torch.Tensor, wdtype: torch.dtype | None = None) -> torch.Tensor: + t_emb = self.time_proj(timesteps).to(dtype=torch.bfloat16) + t_emb = self.timestep_embedder(t_emb) + return t_emb + + +class HiDreamImageOutEmbed(nn.Module): + def __init__(self, hidden_size, patch_size, out_channels): + super().__init__() + self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.linear = nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True) + self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size, bias=True)) + + def forward(self, hidden_states: torch.Tensor, temb: torch.Tensor) -> torch.Tensor: + shift, scale = self.adaLN_modulation(temb).chunk(2, dim=1) + hidden_states = self.norm_final(hidden_states) * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1) + hidden_states = self.linear(hidden_states) + return hidden_states + + +class HiDreamImagePatchEmbed(nn.Module): + def __init__( + self, + patch_size=2, + in_channels=4, + out_channels=1024, + ): + super().__init__() + self.patch_size = patch_size + self.out_channels = out_channels + self.proj = nn.Linear(in_channels * patch_size * patch_size, out_channels, bias=True) + + def forward(self, latent) -> torch.Tensor: + latent = self.proj(latent.to(dtype=torch.bfloat16)) + return latent + + +def rope(pos: torch.Tensor, dim: int, theta: int) -> torch.Tensor: + assert dim % 2 == 0, "The dimension must be even." + + is_mps = pos.device.type == "mps" + is_npu = pos.device.type == "npu" + + dtype = torch.float32 if (is_mps or is_npu) else torch.float64 + + scale = torch.arange(0, dim, 2, dtype=dtype, device=pos.device) / dim + omega = 1.0 / (theta**scale) + + batch_size, seq_length = pos.shape + out = torch.einsum("...n,d->...nd", pos, omega) + cos_out = torch.cos(out) + sin_out = torch.sin(out) + + stacked_out = torch.stack([cos_out, -sin_out, sin_out, cos_out], dim=-1) + out = stacked_out.view(batch_size, -1, dim // 2, 2, 2) + return out.float() + + +class HiDreamImageEmbedND(nn.Module): + def __init__(self, theta: int, axes_dim: list[int]): + super().__init__() + self.theta = theta + self.axes_dim = axes_dim + + def forward(self, ids: torch.Tensor) -> torch.Tensor: + n_axes = ids.shape[-1] + emb = torch.cat( + [rope(ids[..., i], self.axes_dim[i], self.theta) for i in range(n_axes)], + dim=-3, + ) + return emb.unsqueeze(2) + + +class DistributedRMSNorm(nn.Module): + """RMSNorm that computes global RMS across tensor parallel ranks.""" + + def __init__(self, hidden_size: int, eps: float = 1e-6): + super().__init__() + self.eps = eps + self.weight = nn.Parameter(torch.ones(hidden_size)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + tp_size = get_tensor_model_parallel_world_size() + + input_dtype = x.dtype + x_float = x.float() + local_sum_sq = (x_float**2).sum(dim=-1, keepdim=True) + local_count = x.shape[-1] + + if tp_size > 1: + global_sum_sq = local_sum_sq.clone() + tensor_model_parallel_all_reduce(global_sum_sq) + global_count = local_count * tp_size + else: + global_sum_sq = local_sum_sq + global_count = local_count + + rms = torch.sqrt(global_sum_sq / global_count + self.eps) + + output = (x_float / rms) * self.weight.float() + return output.to(input_dtype) + + +class HiDreamAttention(nn.Module): + def __init__( + self, + query_dim: int, + heads: int = 8, + dim_head: int = 64, + upcast_attention: bool = False, + upcast_softmax: bool = False, + scale_qk: bool = True, + eps: float = 1e-5, + processor=None, + out_dim: int = None, + single: bool = False, + quant_config: "QuantizationConfig | None" = None, + ): + super().__init__() + self.head_dim = dim_head + + self.inner_dim = out_dim if out_dim is not None else dim_head * heads + self.query_dim = query_dim + self.upcast_attention = upcast_attention + self.upcast_softmax = upcast_softmax + self.out_dim = out_dim if out_dim is not None else query_dim + + self.scale_qk = scale_qk + self.scale = dim_head**-0.5 if self.scale_qk else 1.0 + + self.heads = out_dim // dim_head if out_dim is not None else heads + self.sliceable_head_dim = heads + self.single = single + + # Fused QKV projection using vLLM's optimized layer + self.to_qkv = QKVParallelLinear( + hidden_size=query_dim, + head_size=self.head_dim, + total_num_heads=self.heads, + quant_config=quant_config, + prefix="to_qkv", + ) + self.query_num_heads = self.to_qkv.num_heads + self.kv_num_heads = self.to_qkv.num_kv_heads + + self.to_out = RowParallelLinear( + self.inner_dim, + self.out_dim, + bias=True, + input_is_parallel=True, + return_bias=False, + quant_config=quant_config, + prefix="to_out", + ) + + self.tp_inner_dim = self.query_num_heads * dim_head + if get_tensor_model_parallel_world_size() > 1: + self.q_rms_norm = DistributedRMSNorm(self.tp_inner_dim, eps=eps) + self.k_rms_norm = DistributedRMSNorm(self.tp_inner_dim, eps=eps) + else: + self.q_rms_norm = nn.RMSNorm(self.tp_inner_dim, eps) + self.k_rms_norm = nn.RMSNorm(self.tp_inner_dim, eps) + + if not single: + self.to_qkv_t = QKVParallelLinear( + hidden_size=query_dim, + head_size=self.head_dim, + total_num_heads=self.heads, + quant_config=quant_config, + prefix="to_qkv_t", + ) + self.to_out_t = RowParallelLinear( + self.inner_dim, + self.out_dim, + bias=True, + input_is_parallel=True, + return_bias=False, + quant_config=quant_config, + prefix="to_out_t", + ) + if get_tensor_model_parallel_world_size() > 1: + self.q_rms_norm_t = DistributedRMSNorm(self.tp_inner_dim, eps=eps) + self.k_rms_norm_t = DistributedRMSNorm(self.tp_inner_dim, eps=eps) + else: + self.q_rms_norm_t = nn.RMSNorm(self.tp_inner_dim, eps) + self.k_rms_norm_t = nn.RMSNorm(self.tp_inner_dim, eps) + self.rope = RotaryEmbedding(is_neox_style=False) + + def forward( + self, + hidden_states: torch.Tensor, + hidden_states_masks: torch.Tensor | None = None, + encoder_hidden_states: torch.Tensor | None = None, + image_rotary_emb: tuple[torch.Tensor, torch.Tensor] | None = None, + ) -> torch.Tensor: + dtype = hidden_states.dtype + batch_size = hidden_states.shape[0] + + qkv, _ = self.to_qkv(hidden_states) + q_size = self.to_qkv.num_heads * self.head_dim + kv_size = self.to_qkv.num_kv_heads * self.head_dim + query, key, value = qkv.split([q_size, kv_size, kv_size], dim=-1) + + query_i = self.q_rms_norm(query).to(dtype=dtype) + key_i = self.k_rms_norm(key).to(dtype=dtype) + value_i = value + + inner_dim = key_i.shape[-1] + head_dim = inner_dim // self.query_num_heads + + query_i = query_i.view(batch_size, -1, self.query_num_heads, head_dim) + key_i = key_i.view(batch_size, -1, self.kv_num_heads, head_dim) + value_i = value_i.view(batch_size, -1, self.kv_num_heads, head_dim) + if hidden_states_masks is not None: + key_i = key_i * hidden_states_masks.view(batch_size, -1, 1, 1) + + if not self.single: + qkv_t, _ = self.to_qkv_t(encoder_hidden_states) + q_size_t = self.to_qkv_t.num_heads * self.head_dim + kv_size_t = self.to_qkv_t.num_kv_heads * self.head_dim + query_t, key_t, value_t = qkv_t.split([q_size_t, kv_size_t, kv_size_t], dim=-1) + query_t = self.q_rms_norm_t(query_t).to(dtype=dtype) + key_t = self.k_rms_norm_t(key_t).to(dtype=dtype) + value_t = value_t + + query_t = query_t.view(batch_size, -1, self.to_qkv_t.num_heads, head_dim) + key_t = key_t.view(batch_size, -1, self.to_qkv_t.num_kv_heads, head_dim) + value_t = value_t.view(batch_size, -1, self.to_qkv_t.num_kv_heads, head_dim) + + num_image_tokens = query_i.shape[1] + num_text_tokens = query_t.shape[1] + query = torch.cat([query_i, query_t], dim=1) + key = torch.cat([key_i, key_t], dim=1) + value = torch.cat([value_i, value_t], dim=1) + else: + query = query_i + key = key_i + value = value_i + + cos, sin = image_rotary_emb + cos = cos.squeeze(2).to(query.dtype) + sin = sin.squeeze(2).to(query.dtype) + if query.shape[-1] == image_rotary_emb[0].shape[-1] * 2: + query = self.rope(query, cos, sin) + key = self.rope(key, cos, sin) + else: + query_1, query_2 = query.chunk(2, dim=-1) + key_1, key_2 = key.chunk(2, dim=-1) + key_1 = self.rope(query_1, cos, sin) + key_1 = self.rope(key_1, cos, sin) + query = torch.cat([query_1, query_2], dim=-1) + key = torch.cat([key_1, key_2], dim=-1) + + hidden_states = F.scaled_dot_product_attention( + query.transpose(1, 2), key.transpose(1, 2), value.transpose(1, 2), dropout_p=0.0, is_causal=False + ) + + hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, self.query_num_heads * head_dim) + hidden_states = hidden_states.to(query.dtype) + + if not self.single: + hidden_states_i, hidden_states_t = torch.split(hidden_states, [num_image_tokens, num_text_tokens], dim=1) + hidden_states_i = self.to_out(hidden_states_i) + hidden_states_t = self.to_out_t(hidden_states_t) + return hidden_states_i, hidden_states_t + else: + hidden_states = self.to_out(hidden_states) + return hidden_states + + +# Modified from https://github.com/deepseek-ai/DeepSeek-V3/blob/main/inference/model.py +class MoEGate(nn.Module): + def __init__( + self, + embed_dim, + num_routed_experts=4, + num_activated_experts=2, + aux_loss_alpha=0.01, + _force_inference_output=False, + ): + super().__init__() + self.top_k = num_activated_experts + self.n_routed_experts = num_routed_experts + + self.scoring_func = "softmax" + self.alpha = aux_loss_alpha + self.seq_aux = False + + # topk selection algorithm + self.norm_topk_prob = False + self.gating_dim = embed_dim + self.weight = nn.Parameter(torch.randn(self.n_routed_experts, self.gating_dim) / embed_dim**0.5) + + self._force_inference_output = _force_inference_output + + def forward(self, hidden_states): + bsz, seq_len, h = hidden_states.shape + ### compute gating score + hidden_states = hidden_states.view(-1, h) + logits = F.linear(hidden_states, self.weight, None) + if self.scoring_func == "softmax": + scores = logits.softmax(dim=-1) + else: + raise NotImplementedError(f"insupportable scoring function for MoE gating: {self.scoring_func}") + + ### select top-k experts + topk_weight, topk_idx = torch.topk(scores, k=self.top_k, dim=-1, sorted=False) + + ### norm gate to sum 1 + if self.top_k > 1 and self.norm_topk_prob: + denominator = topk_weight.sum(dim=-1, keepdim=True) + 1e-20 + topk_weight = topk_weight / denominator + + ### expert-level computation auxiliary loss + if self.training and self.alpha > 0.0 and not self._force_inference_output: + scores_for_aux = scores + aux_topk = self.top_k + # always compute aux loss based on the naive greedy topk method + topk_idx_for_aux_loss = topk_idx.view(bsz, -1) + if self.seq_aux: + scores_for_seq_aux = scores_for_aux.view(bsz, seq_len, -1) + ce = torch.zeros(bsz, self.n_routed_experts, device=hidden_states.device) + ce.scatter_add_( + 1, topk_idx_for_aux_loss, torch.ones(bsz, seq_len * aux_topk, device=hidden_states.device) + ).div_(seq_len * aux_topk / self.n_routed_experts) + aux_loss = (ce * scores_for_seq_aux.mean(dim=1)).sum(dim=1).mean() * self.alpha + else: + mask_ce = F.one_hot(topk_idx_for_aux_loss.view(-1), num_classes=self.n_routed_experts) + ce = mask_ce.float().mean(0) + + Pi = scores_for_aux.mean(0) + fi = ce * self.n_routed_experts + aux_loss = (Pi * fi).sum() * self.alpha + else: + aux_loss = None + return topk_idx, topk_weight, aux_loss + + +# Modified from https://github.com/deepseek-ai/DeepSeek-V3/blob/main/inference/model.py +class MOEFeedForwardSwiGLU(nn.Module): + def __init__( + self, + dim: int, + hidden_dim: int, + num_routed_experts: int, + num_activated_experts: int, + _force_inference_output: bool = False, + ): + super().__init__() + self.shared_experts = HiDreamImageFeedForwardSwiGLU(dim, hidden_dim // 2) + self.experts = nn.ModuleList( + [HiDreamImageFeedForwardSwiGLU(dim, hidden_dim) for i in range(num_routed_experts)] + ) + self._force_inference_output = _force_inference_output + self.gate = MoEGate( + embed_dim=dim, + num_routed_experts=num_routed_experts, + num_activated_experts=num_activated_experts, + _force_inference_output=_force_inference_output, + ) + self.num_activated_experts = num_activated_experts + + def forward(self, x): + wtype = x.dtype + identity = x + orig_shape = x.shape + topk_idx, topk_weight, aux_loss = self.gate(x) + x = x.view(-1, x.shape[-1]) + flat_topk_idx = topk_idx.view(-1) + if self.training and not self._force_inference_output: + x = x.repeat_interleave(self.num_activated_experts, dim=0) + y = torch.empty_like(x, dtype=wtype) + for i, expert in enumerate(self.experts): + y[flat_topk_idx == i] = expert(x[flat_topk_idx == i]).to(dtype=wtype) + y = (y.view(*topk_weight.shape, -1) * topk_weight.unsqueeze(-1)).sum(dim=1) + y = y.view(*orig_shape).to(dtype=wtype) + else: + y = self.moe_infer(x, flat_topk_idx, topk_weight.view(-1, 1)).view(*orig_shape) + y = y + self.shared_experts(identity) + return y + + @torch.no_grad() + def moe_infer(self, x, flat_expert_indices, flat_expert_weights): + expert_cache = torch.zeros_like(x) + idxs = flat_expert_indices.argsort() + tokens_per_expert = flat_expert_indices.bincount().cpu().numpy().cumsum(0) + token_idxs = idxs // self.num_activated_experts + + # TODO: This MoE expert routing is sequential and can be a bottleneck for models + # with MoE in every transformer block. Consider optimizing this in the future + # using techniques like grouped GEMM or batched expert execution. + for i, end_idx in enumerate(tokens_per_expert): + start_idx = 0 if i == 0 else tokens_per_expert[i - 1] + if start_idx == end_idx: + continue + expert = self.experts[i] + exp_token_idx = token_idxs[start_idx:end_idx] + expert_tokens = x[exp_token_idx] + expert_out = expert(expert_tokens) + expert_out.mul_(flat_expert_weights[idxs[start_idx:end_idx]]) + + # for fp16 and other dtype + expert_cache = expert_cache.to(expert_out.dtype) + expert_cache.scatter_reduce_(0, exp_token_idx.view(-1, 1).repeat(1, x.shape[-1]), expert_out, reduce="sum") + return expert_cache + + +class TextProjection(nn.Module): + def __init__(self, in_features, hidden_size): + super().__init__() + self.linear = nn.Linear(in_features=in_features, out_features=hidden_size, bias=False) + + def forward(self, caption): + hidden_states = self.linear(caption) + return hidden_states + + +class HiDreamImageSingleTransformerBlock(nn.Module): + def __init__( + self, + dim: int, + num_attention_heads: int, + attention_head_dim: int, + num_routed_experts: int = 4, + num_activated_experts: int = 2, + _force_inference_output: bool = False, + ): + super().__init__() + self.num_attention_heads = num_attention_heads + self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(dim, 6 * dim, bias=True)) + + # 1. Attention + self.norm1_i = nn.LayerNorm(dim, eps=1e-06, elementwise_affine=False) + self.attn1 = HiDreamAttention( + query_dim=dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + single=True, + ) + + # 3. Feed-forward + self.norm3_i = nn.LayerNorm(dim, eps=1e-06, elementwise_affine=False) + if num_routed_experts > 0: + self.ff_i = MOEFeedForwardSwiGLU( + dim=dim, + hidden_dim=4 * dim, + num_routed_experts=num_routed_experts, + num_activated_experts=num_activated_experts, + _force_inference_output=_force_inference_output, + ) + else: + self.ff_i = HiDreamImageFeedForwardSwiGLU(dim=dim, hidden_dim=4 * dim) + + def forward( + self, + hidden_states: torch.Tensor, + hidden_states_masks: torch.Tensor | None = None, + encoder_hidden_states: torch.Tensor | None = None, + temb: torch.Tensor | None = None, + image_rotary_emb: tuple[torch.Tensor, torch.Tensor] | None = None, + ) -> torch.Tensor: + wtype = hidden_states.dtype + shift_msa_i, scale_msa_i, gate_msa_i, shift_mlp_i, scale_mlp_i, gate_mlp_i = self.adaLN_modulation(temb)[ + :, None + ].chunk(6, dim=-1) + + # 1. MM-Attention + norm_hidden_states = self.norm1_i(hidden_states).to(dtype=wtype) + norm_hidden_states = norm_hidden_states * (1 + scale_msa_i) + shift_msa_i + attn_output_i = self.attn1( + norm_hidden_states, + hidden_states_masks, + image_rotary_emb=image_rotary_emb, + ) + hidden_states = gate_msa_i * attn_output_i + hidden_states + + # 2. Feed-forward + norm_hidden_states = self.norm3_i(hidden_states).to(dtype=wtype) + norm_hidden_states = norm_hidden_states * (1 + scale_mlp_i) + shift_mlp_i + ff_output_i = gate_mlp_i * self.ff_i(norm_hidden_states.to(dtype=wtype)) + hidden_states = ff_output_i + hidden_states + return hidden_states + + +class HiDreamImageTransformerBlock(nn.Module): + def __init__( + self, + dim: int, + num_attention_heads: int, + attention_head_dim: int, + num_routed_experts: int = 4, + num_activated_experts: int = 2, + _force_inference_output: bool = False, + ): + super().__init__() + self.num_attention_heads = num_attention_heads + self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(dim, 12 * dim, bias=True)) + + # 1. Attention + self.norm1_i = nn.LayerNorm(dim, eps=1e-06, elementwise_affine=False) + self.norm1_t = nn.LayerNorm(dim, eps=1e-06, elementwise_affine=False) + self.attn1 = HiDreamAttention( + query_dim=dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + single=False, + ) + + # 3. Feed-forward + self.norm3_i = nn.LayerNorm(dim, eps=1e-06, elementwise_affine=False) + if num_routed_experts > 0: + self.ff_i = MOEFeedForwardSwiGLU( + dim=dim, + hidden_dim=4 * dim, + num_routed_experts=num_routed_experts, + num_activated_experts=num_activated_experts, + _force_inference_output=_force_inference_output, + ) + else: + self.ff_i = HiDreamImageFeedForwardSwiGLU(dim=dim, hidden_dim=4 * dim) + self.norm3_t = nn.LayerNorm(dim, eps=1e-06, elementwise_affine=False) + self.ff_t = HiDreamImageFeedForwardSwiGLU(dim=dim, hidden_dim=4 * dim) + + def forward( + self, + hidden_states: torch.Tensor, + hidden_states_masks: torch.Tensor | None = None, + encoder_hidden_states: torch.Tensor | None = None, + temb: torch.Tensor | None = None, + image_rotary_emb: tuple[torch.Tensor, torch.Tensor] | None = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + wtype = hidden_states.dtype + ( + shift_msa_i, + scale_msa_i, + gate_msa_i, + shift_mlp_i, + scale_mlp_i, + gate_mlp_i, + shift_msa_t, + scale_msa_t, + gate_msa_t, + shift_mlp_t, + scale_mlp_t, + gate_mlp_t, + ) = self.adaLN_modulation(temb)[:, None].chunk(12, dim=-1) + + # 1. MM-Attention + norm_hidden_states = self.norm1_i(hidden_states).to(dtype=wtype) + norm_hidden_states = norm_hidden_states * (1 + scale_msa_i) + shift_msa_i + norm_encoder_hidden_states = self.norm1_t(encoder_hidden_states).to(dtype=wtype) + norm_encoder_hidden_states = norm_encoder_hidden_states * (1 + scale_msa_t) + shift_msa_t + + attn_output_i, attn_output_t = self.attn1( + norm_hidden_states, + hidden_states_masks, + norm_encoder_hidden_states, + image_rotary_emb=image_rotary_emb, + ) + + hidden_states = gate_msa_i * attn_output_i + hidden_states + encoder_hidden_states = gate_msa_t * attn_output_t + encoder_hidden_states + + # 2. Feed-forward + norm_hidden_states = self.norm3_i(hidden_states).to(dtype=wtype) + norm_hidden_states = norm_hidden_states * (1 + scale_mlp_i) + shift_mlp_i + norm_encoder_hidden_states = self.norm3_t(encoder_hidden_states).to(dtype=wtype) + norm_encoder_hidden_states = norm_encoder_hidden_states * (1 + scale_mlp_t) + shift_mlp_t + + ff_output_i = gate_mlp_i * self.ff_i(norm_hidden_states) + ff_output_t = gate_mlp_t * self.ff_t(norm_encoder_hidden_states) + hidden_states = ff_output_i + hidden_states + encoder_hidden_states = ff_output_t + encoder_hidden_states + return hidden_states, encoder_hidden_states + + +class HiDreamBlock(nn.Module): + def __init__(self, block: HiDreamImageTransformerBlock | HiDreamImageSingleTransformerBlock): + super().__init__() + self.block = block + + def forward( + self, + hidden_states: torch.Tensor, + hidden_states_masks: torch.Tensor | None = None, + encoder_hidden_states: torch.Tensor | None = None, + temb: torch.Tensor | None = None, + image_rotary_emb: tuple[torch.Tensor, torch.Tensor] | None = None, + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]: + return self.block( + hidden_states=hidden_states, + hidden_states_masks=hidden_states_masks, + encoder_hidden_states=encoder_hidden_states, + temb=temb, + image_rotary_emb=image_rotary_emb, + ) + + +class HiDreamImageTransformer2DModel(nn.Module): + def __init__( + self, + od_config: OmniDiffusionConfig = None, + patch_size: int | None = None, + in_channels: int = 64, + out_channels: int | None = None, + num_layers: int = 16, + num_single_layers: int = 32, + attention_head_dim: int = 128, + num_attention_heads: int = 20, + caption_channels: list[int] = None, + text_emb_dim: int = 2048, + num_routed_experts: int = 4, + num_activated_experts: int = 2, + axes_dims_rope: tuple[int, int] = (32, 32), + max_resolution: tuple[int, int] = (128, 128), + llama_layers: list[int] = None, + force_inference_output: bool = False, + quant_config: "QuantizationConfig | None" = None, + ): + super().__init__() + self.parallel_config = od_config.parallel_config + + self.in_channels = in_channels + self.patch_size = patch_size + self.force_inference_output = force_inference_output + self.llama_layers = llama_layers + + self.out_channels = out_channels or in_channels + self.inner_dim = num_attention_heads * attention_head_dim + + self.t_embedder = HiDreamImageTimestepEmbed(self.inner_dim) + self.p_embedder = HiDreamImagePooledEmbed(text_emb_dim, self.inner_dim) + self.x_embedder = HiDreamImagePatchEmbed( + patch_size=patch_size, + in_channels=in_channels, + out_channels=self.inner_dim, + ) + self.pe_embedder = HiDreamImageEmbedND(theta=10000, axes_dim=axes_dims_rope) + + self.double_stream_blocks = nn.ModuleList( + [ + HiDreamBlock( + HiDreamImageTransformerBlock( + dim=self.inner_dim, + num_attention_heads=num_attention_heads, + attention_head_dim=attention_head_dim, + num_routed_experts=num_routed_experts, + num_activated_experts=num_activated_experts, + _force_inference_output=force_inference_output, + ) + ) + for _ in range(num_layers) + ] + ) + + self.single_stream_blocks = nn.ModuleList( + [ + HiDreamBlock( + HiDreamImageSingleTransformerBlock( + dim=self.inner_dim, + num_attention_heads=num_attention_heads, + attention_head_dim=attention_head_dim, + num_routed_experts=num_routed_experts, + num_activated_experts=num_activated_experts, + _force_inference_output=force_inference_output, + ) + ) + for _ in range(num_single_layers) + ] + ) + + self.final_layer = HiDreamImageOutEmbed(self.inner_dim, patch_size, self.out_channels) + + num_llama_projections = len(self.llama_layers) if self.llama_layers is not None else 0 + caption_channels = [caption_channels[1]] * num_llama_projections + [caption_channels[0]] + caption_projection = [] + for caption_channel in caption_channels: + caption_projection.append(TextProjection(in_features=caption_channel, hidden_size=self.inner_dim)) + self.caption_projection = nn.ModuleList(caption_projection) + self.max_seq = max_resolution[0] * max_resolution[1] // (patch_size * patch_size) + + def unpatchify(self, x: torch.Tensor, img_sizes: list[tuple[int, int]], is_training: bool) -> list[torch.Tensor]: + if is_training and not self.force_inference_output: + B, S, F = x.shape + C = F // (self.patch_size * self.patch_size) + x = ( + x.reshape(B, S, self.patch_size, self.patch_size, C) + .permute(0, 4, 1, 2, 3) + .reshape(B, C, S, self.patch_size * self.patch_size) + ) + else: + x_arr = [] + p1 = self.patch_size + p2 = self.patch_size + for i, img_size in enumerate(img_sizes): + pH, pW = img_size + t = x[i, : pH * pW].reshape(1, pH, pW, -1) + F_token = t.shape[-1] + C = F_token // (p1 * p2) + t = t.reshape(1, pH, pW, p1, p2, C) + t = t.permute(0, 5, 1, 3, 2, 4) + t = t.reshape(1, C, pH * p1, pW * p2) + x_arr.append(t) + x = torch.cat(x_arr, dim=0) + return x + + def patchify(self, hidden_states): + batch_size, channels, height, width = hidden_states.shape + patch_size = self.patch_size + patch_height, patch_width = height // patch_size, width // patch_size + device = hidden_states.device + dtype = hidden_states.dtype + + # create img_sizes + img_sizes = torch.tensor([patch_height, patch_width], dtype=torch.int64, device=device).reshape(-1) + img_sizes = img_sizes.unsqueeze(0).repeat(batch_size, 1) + + # create hidden_states_masks + if hidden_states.shape[-2] != hidden_states.shape[-1]: + hidden_states_masks = torch.zeros((batch_size, self.max_seq), dtype=dtype, device=device) + hidden_states_masks[:, : patch_height * patch_width] = 1.0 + else: + hidden_states_masks = None + + # create img_ids + img_ids = torch.zeros(patch_height, patch_width, 3, device=device) + row_indices = torch.arange(patch_height, device=device)[:, None] + col_indices = torch.arange(patch_width, device=device)[None, :] + img_ids[..., 1] = img_ids[..., 1] + row_indices + img_ids[..., 2] = img_ids[..., 2] + col_indices + img_ids = img_ids.reshape(patch_height * patch_width, -1) + + if hidden_states.shape[-2] != hidden_states.shape[-1]: + # Handle non-square latents + img_ids_pad = torch.zeros(self.max_seq, 3, device=device) + img_ids_pad[: patch_height * patch_width, :] = img_ids + img_ids = img_ids_pad.unsqueeze(0).repeat(batch_size, 1, 1) + else: + img_ids = img_ids.unsqueeze(0).repeat(batch_size, 1, 1) + + # patchify hidden_states + if hidden_states.shape[-2] != hidden_states.shape[-1]: + # Handle non-square latents + out = torch.zeros( + (batch_size, channels, self.max_seq, patch_size * patch_size), + dtype=dtype, + device=device, + ) + hidden_states = hidden_states.reshape( + batch_size, channels, patch_height, patch_size, patch_width, patch_size + ) + hidden_states = hidden_states.permute(0, 1, 2, 4, 3, 5) + hidden_states = hidden_states.reshape( + batch_size, channels, patch_height * patch_width, patch_size * patch_size + ) + out[:, :, 0 : patch_height * patch_width] = hidden_states + hidden_states = out + hidden_states = hidden_states.permute(0, 2, 3, 1).reshape( + batch_size, self.max_seq, patch_size * patch_size * channels + ) + + else: + # Handle square latents + hidden_states = hidden_states.reshape( + batch_size, channels, patch_height, patch_size, patch_width, patch_size + ) + hidden_states = hidden_states.permute(0, 2, 4, 3, 5, 1) + hidden_states = hidden_states.reshape( + batch_size, patch_height * patch_width, patch_size * patch_size * channels + ) + + return hidden_states, hidden_states_masks, img_sizes, img_ids + + def forward( + self, + hidden_states: torch.Tensor, + timesteps: torch.LongTensor = None, + encoder_hidden_states_t5: torch.Tensor = None, + encoder_hidden_states_llama3: torch.Tensor = None, + pooled_embeds: torch.Tensor = None, + img_ids: torch.Tensor | None = None, + img_sizes: list[tuple[int, int]] | None = None, + hidden_states_masks: torch.Tensor | None = None, + return_dict: bool = True, + **kwargs, + ) -> tuple[torch.Tensor] | Transformer2DModelOutput: + encoder_hidden_states = kwargs.get("encoder_hidden_states", None) + + if encoder_hidden_states is not None: + deprecation_message = ( + "The `encoder_hidden_states` argument is deprecated." + "Please use `encoder_hidden_states_t5` and `encoder_hidden_states_llama3` instead." + ) + deprecate("encoder_hidden_states", "0.35.0", deprecation_message) + encoder_hidden_states_t5 = encoder_hidden_states[0] + encoder_hidden_states_llama3 = encoder_hidden_states[1] + + if img_ids is not None and img_sizes is not None and hidden_states_masks is None: + deprecation_message = ( + "Passing `img_ids` and `img_sizes` with unpachified `hidden_states` is deprecated and will be ignored." + ) + deprecate("img_ids", "0.35.0", deprecation_message) + + if hidden_states_masks is not None and (img_ids is None or img_sizes is None): + raise ValueError("if `hidden_states_masks` is passed, `img_ids` and `img_sizes` must also be passed.") + elif hidden_states_masks is not None and hidden_states.ndim != 3: + raise ValueError( + "if `hidden_states_masks` is passed, `hidden_states` must be a 3D tensors with shape" + "(batch_size, patch_height * patch_width, patch_size * patch_size * channels)" + ) + + # spatial forward + batch_size = hidden_states.shape[0] + hidden_states_type = hidden_states.dtype + + # Patchify the input + if hidden_states_masks is None: + hidden_states, hidden_states_masks, img_sizes, img_ids = self.patchify(hidden_states) + + # Embed the hidden states + hidden_states = self.x_embedder(hidden_states) + + # 0. time + timesteps = self.t_embedder(timesteps, hidden_states_type) + p_embedder = self.p_embedder(pooled_embeds) + temb = timesteps + p_embedder + + encoder_hidden_states = [encoder_hidden_states_llama3[k] for k in self.llama_layers] + + if self.caption_projection is not None: + new_encoder_hidden_states = [] + for i, enc_hidden_state in enumerate(encoder_hidden_states): + enc_hidden_state = self.caption_projection[i](enc_hidden_state) + enc_hidden_state = enc_hidden_state.view(batch_size, -1, hidden_states.shape[-1]) + new_encoder_hidden_states.append(enc_hidden_state) + encoder_hidden_states = new_encoder_hidden_states + encoder_hidden_states_t5 = self.caption_projection[-1](encoder_hidden_states_t5) + encoder_hidden_states_t5 = encoder_hidden_states_t5.view(batch_size, -1, hidden_states.shape[-1]) + encoder_hidden_states.append(encoder_hidden_states_t5) + + txt_ids = torch.zeros( + batch_size, + encoder_hidden_states[-1].shape[1] + encoder_hidden_states[-2].shape[1] + encoder_hidden_states[0].shape[1], + 3, + device=img_ids.device, + dtype=img_ids.dtype, + ) + image_rotary_emb = self.pe_embedder(img_ids) + text_rotary_emb = self.pe_embedder(txt_ids) + concat_rotary_emb = ( + torch.cat([image_rotary_emb[..., 0, 0], text_rotary_emb[..., 0, 0]], dim=1), + torch.cat([image_rotary_emb[..., 1, 0], text_rotary_emb[..., 1, 0]], dim=1), + ) + + # 2. Blocks + block_id = 0 + initial_encoder_hidden_states = torch.cat([encoder_hidden_states[-1], encoder_hidden_states[-2]], dim=1) + initial_encoder_hidden_states_seq_len = initial_encoder_hidden_states.shape[1] + for bid, block in enumerate(self.double_stream_blocks): + cur_llama31_encoder_hidden_states = encoder_hidden_states[block_id] + cur_encoder_hidden_states = torch.cat( + [initial_encoder_hidden_states, cur_llama31_encoder_hidden_states], dim=1 + ) + hidden_states, initial_encoder_hidden_states = block( + hidden_states=hidden_states, + hidden_states_masks=hidden_states_masks, + encoder_hidden_states=cur_encoder_hidden_states, + temb=temb, + image_rotary_emb=concat_rotary_emb, + ) + initial_encoder_hidden_states = initial_encoder_hidden_states[:, :initial_encoder_hidden_states_seq_len] + block_id += 1 + + image_tokens_seq_len = hidden_states.shape[1] + hidden_states = torch.cat([hidden_states, initial_encoder_hidden_states], dim=1) + hidden_states_seq_len = hidden_states.shape[1] + if hidden_states_masks is not None: + encoder_attention_mask_ones = torch.ones( + (batch_size, initial_encoder_hidden_states.shape[1] + cur_llama31_encoder_hidden_states.shape[1]), + device=hidden_states_masks.device, + dtype=hidden_states_masks.dtype, + ) + hidden_states_masks = torch.cat([hidden_states_masks, encoder_attention_mask_ones], dim=1) + + for bid, block in enumerate(self.single_stream_blocks): + cur_llama31_encoder_hidden_states = encoder_hidden_states[block_id] + hidden_states = torch.cat([hidden_states, cur_llama31_encoder_hidden_states], dim=1) + + hidden_states = block( + hidden_states=hidden_states, + hidden_states_masks=hidden_states_masks, + encoder_hidden_states=None, + temb=temb, + image_rotary_emb=concat_rotary_emb, + ) + hidden_states = hidden_states[:, :hidden_states_seq_len] + block_id += 1 + + hidden_states = hidden_states[:, :image_tokens_seq_len, ...] + output = self.final_layer(hidden_states, temb) + output = self.unpatchify(output, img_sizes, self.training) + if hidden_states_masks is not None: + hidden_states_masks = hidden_states_masks[:, :image_tokens_seq_len] + + if not return_dict: + return (output,) + return Transformer2DModelOutput(sample=output) + + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: + tp_rank = get_tensor_model_parallel_rank() + tp_size = get_tensor_model_parallel_world_size() + + stacked_params_mapping = [ + # (param_name, shard_name, shard_id) + # self-attn + (".to_qkv", ".to_q", "q"), + (".to_qkv", ".to_k", "k"), + (".to_qkv", ".to_v", "v"), + # self-attn no single + (".to_qkv_t", ".to_q_t", "q"), + (".to_qkv_t", ".to_k_t", "k"), + (".to_qkv_t", ".to_v_t", "v"), + # cross-attn + (".add_kv_proj", ".add_q_proj", "q"), + (".add_kv_proj", ".add_k_proj", "k"), + (".add_kv_proj", ".add_v_proj", "v"), + ] + + params_dict = dict(self.named_parameters()) + + # we need to load the buffers for beta and eps (XIELU) + for name, buffer in self.named_buffers(): + if name.endswith(".beta") or name.endswith(".eps"): + params_dict[name] = buffer + + loaded_params: set[str] = set() + for name, loaded_weight in weights: + original_name = name + lookup_name = name + for param_name, weight_name, shard_id in stacked_params_mapping: + if weight_name not in original_name: + continue + lookup_name = original_name.replace(weight_name, param_name) + param = params_dict[lookup_name] + weight_loader = param.weight_loader + weight_loader(param, loaded_weight, shard_id) + break + else: + if lookup_name not in params_dict and ".to_out.0." in lookup_name: + lookup_name = lookup_name.replace(".to_out.0.", ".to_out.") + if lookup_name not in params_dict: + continue + param = params_dict[lookup_name] + if tp_size > 1 and any( + norm_name in lookup_name + for norm_name in [ + ".attn1.q_rms_norm", + ".attn1.k_rms_norm", + ] + ): + shard_size = loaded_weight.shape[0] // tp_size + loaded_weight = loaded_weight[tp_rank * shard_size : (tp_rank + 1) * shard_size] + weight_loader = getattr(param, "weight_loader", default_weight_loader) + weight_loader(param, loaded_weight) + loaded_params.add(original_name) + loaded_params.add(lookup_name) + return loaded_params diff --git a/vllm_omni/diffusion/models/hidream_image/pipeline_hidream_i1_image.py b/vllm_omni/diffusion/models/hidream_image/pipeline_hidream_i1_image.py new file mode 100644 index 000000000..39869a00d --- /dev/null +++ b/vllm_omni/diffusion/models/hidream_image/pipeline_hidream_i1_image.py @@ -0,0 +1,1027 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +import inspect +import json +import math +import os +from collections.abc import Callable, Iterable +from typing import Any + +import numpy as np +import torch +from diffusers.image_processor import VaeImageProcessor +from diffusers.models import AutoencoderKL +from diffusers.schedulers import FlowMatchEulerDiscreteScheduler +from diffusers.utils import deprecate, logging +from diffusers.utils.torch_utils import randn_tensor +from torch import nn +from transformers import ( + CLIPTextModelWithProjection, + CLIPTokenizer, + LlamaForCausalLM, + PreTrainedTokenizerFast, + T5EncoderModel, + T5Tokenizer, +) +from vllm.model_executor.models.utils import AutoWeightsLoader + +from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig +from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin +from vllm_omni.diffusion.distributed.parallel_state import get_classifier_free_guidance_world_size +from vllm_omni.diffusion.distributed.utils import get_local_device +from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader +from vllm_omni.diffusion.models.hidream_image import HiDreamImageTransformer2DModel +from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin +from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin +from vllm_omni.diffusion.request import OmniDiffusionRequest +from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs +from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific + +logger = logging.get_logger(__name__) + + +def get_hidream_image_post_process_func( + od_config: OmniDiffusionConfig, +): + model_name = od_config.model + if os.path.exists(model_name): + model_path = model_name + else: + model_path = download_weights_from_hf_specific(model_name, None, ["*"]) + vae_config_path = os.path.join(model_path, "vae/config.json") + if not os.path.exists(vae_config_path): + raise FileNotFoundError( + f"VAE config not found at {vae_config_path}. " + "Please ensure the model path contains a valid VAE configuration." + ) + with open(vae_config_path) as f: + vae_config = json.load(f) + vae_scale_factor = 2 ** (len(vae_config["block_out_channels"]) - 1) if "block_out_channels" in vae_config else 8 + + image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor * 2) + + def post_process_func( + images: torch.Tensor, + ): + return image_processor.postprocess(images) + + return post_process_func + + +# Copied from diffusers.pipelines.flux.pipeline_flux.calculate_shift +def calculate_shift( + image_seq_len, + base_seq_len: int = 256, + max_seq_len: int = 4096, + base_shift: float = 0.5, + max_shift: float = 1.15, +): + m = (max_shift - base_shift) / (max_seq_len - base_seq_len) + b = base_shift - m * base_seq_len + mu = image_seq_len * m + b + return mu + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps +def retrieve_timesteps( + scheduler, + num_inference_steps: int | None = None, + device: str | torch.device | None = None, + timesteps: list[int] | None = None, + sigmas: list[float] | None = None, + **kwargs, +): + r""" + Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles + custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`. + + Args: + scheduler (`SchedulerMixin`): + The scheduler to get timesteps from. + num_inference_steps (`int`): + The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps` + must be `None`. + device (`str` or `torch.device`, *optional*): + The device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + timesteps (`list[int]`, *optional*): + Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed, + `num_inference_steps` and `sigmas` must be `None`. + sigmas (`list[float]`, *optional*): + Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed, + `num_inference_steps` and `timesteps` must be `None`. + + Returns: + timesteps (`torch.Tensor`): The timestep schedule from the scheduler. + num_inference_steps (`int`): The number of inference steps. + """ + if timesteps is not None and sigmas is not None: + raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values") + if timesteps is not None: + accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys()) + if not accepts_timesteps: + raise ValueError( + f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom" + f" timestep schedules. Please check whether you are using the correct scheduler." + ) + scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs) + timesteps = scheduler.timesteps + num_inference_steps = len(timesteps) + elif sigmas is not None: + accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys()) + if not accept_sigmas: + raise ValueError( + f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom" + f" sigmas schedules. Please check whether you are using the correct scheduler." + ) + scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs) + timesteps = scheduler.timesteps + num_inference_steps = len(timesteps) + else: + scheduler.set_timesteps(num_inference_steps, device=device, **kwargs) + timesteps = scheduler.timesteps + return timesteps, num_inference_steps + + +class HiDreamI1ImagePipeline(nn.Module, CFGParallelMixin, DiffusionPipelineProfilerMixin, ProgressBarMixin): + def __init__( + self, + *, + od_config: OmniDiffusionConfig, + prefix: str = "", + ): + super().__init__() + self.od_config = od_config + dtype = getattr(od_config, "dtype", torch.bfloat16) + llama_path = self.od_config.extras["auxiliary_text_encoder"] + if llama_path is None: + logger.warning( + f"auxiliary_text_encoder is not provided. " + f"Attempting to load default LLaMA model: {self.od_config.extras['default_llama_model_id']}" + ) + llama_path = self.od_config.extras["default_llama_model_id"] + logger.info(f"Loading LLAMA model from: {llama_path}") + + self.weights_sources = [ + DiffusersPipelineLoader.ComponentSource( + model_or_path=od_config.model, + subfolder="transformer", + revision=None, + prefix="transformer.", + fall_back_to_pt=True, + ), + ] + + self.device = get_local_device() + model = od_config.model + # Check if model is a local path + local_files_only = os.path.exists(model) + + self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained( + model, subfolder="scheduler", local_files_only=local_files_only + ) + self.vae = AutoencoderKL.from_pretrained(model, subfolder="vae", local_files_only=local_files_only).to( + self.device + ) + self.text_encoder = CLIPTextModelWithProjection.from_pretrained( + model, subfolder="text_encoder", local_files_only=local_files_only + ) + self.tokenizer = CLIPTokenizer.from_pretrained(model, subfolder="tokenizer", local_files_only=local_files_only) + self.text_encoder_2 = CLIPTextModelWithProjection.from_pretrained( + model, subfolder="text_encoder_2", local_files_only=local_files_only + ) + self.tokenizer_2 = CLIPTokenizer.from_pretrained( + model, subfolder="tokenizer_2", local_files_only=local_files_only + ) + self.text_encoder_3 = T5EncoderModel.from_pretrained( + model, subfolder="text_encoder_3", local_files_only=local_files_only + ) + self.tokenizer_3 = T5Tokenizer.from_pretrained( + model, subfolder="tokenizer_3", local_files_only=local_files_only + ) + self.text_encoder_4 = LlamaForCausalLM.from_pretrained(llama_path, output_hidden_states=True, dtype=dtype).to( + self.device + ) + self.tokenizer_4 = PreTrainedTokenizerFast.from_pretrained(llama_path, use_fast=False) + transformer_kwargs = get_transformer_config_kwargs(od_config.tf_model_config, HiDreamImageTransformer2DModel) + self.transformer = HiDreamImageTransformer2DModel( + od_config=od_config, quant_config=od_config.quantization_config, **transformer_kwargs + ) + + self.stage = None + + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) if getattr(self, "vae", None) else 8 + # HiDreamImage latents are turned into 2x2 patches and packed. + # This means the latent width and height has to be divisible + # by the patch size. So the vae scale factor is multiplied by the patch size to account for this + self.default_sample_size = 128 + if getattr(self, "tokenizer_4", None) is not None: + self.tokenizer_4.pad_token = self.tokenizer_4.eos_token + + self.setup_diffusion_pipeline_profiler( + enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler + ) + + def check_inputs( + self, + prompt, + prompt_2, + prompt_3, + prompt_4, + negative_prompt=None, + negative_prompt_2=None, + negative_prompt_3=None, + negative_prompt_4=None, + prompt_embeds_t5=None, + prompt_embeds_llama3=None, + negative_prompt_embeds_t5=None, + negative_prompt_embeds_llama3=None, + pooled_prompt_embeds=None, + negative_pooled_prompt_embeds=None, + callback_on_step_end_tensor_inputs=None, + ): + if prompt is not None and pooled_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `pooled_prompt_embeds`: {pooled_prompt_embeds}. " + "Please make sure to only forward one of the two." + ) + elif prompt_2 is not None and pooled_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt_2`: {prompt_2} and `pooled_prompt_embeds`: {pooled_prompt_embeds}." + "Please make sure to only forward one of the two." + ) + elif prompt_3 is not None and prompt_embeds_t5 is not None: + raise ValueError( + f"Cannot forward both `prompt_3`: {prompt_3} and `prompt_embeds_t5`: {prompt_embeds_t5}." + "Please make sure to only forward one of the two." + ) + elif prompt_4 is not None and prompt_embeds_llama3 is not None: + raise ValueError( + f"Cannot forward both `prompt_4`: {prompt_4} and `prompt_embeds_llama3`: {prompt_embeds_llama3}." + "Please make sure to only forward one of the two." + ) + elif prompt is None and pooled_prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `pooled_prompt_embeds`." + "Cannot leave both `prompt` and `pooled_prompt_embeds` undefined." + ) + elif prompt is None and prompt_embeds_t5 is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds_t5`." + "Cannot leave both `prompt` and `prompt_embeds_t5` undefined." + ) + elif prompt is None and prompt_embeds_llama3 is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds_llama3`." + "Cannot leave both `prompt` and `prompt_embeds_llama3` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + elif prompt_2 is not None and (not isinstance(prompt_2, str) and not isinstance(prompt_2, list)): + raise ValueError(f"`prompt_2` has to be of type `str` or `list` but is {type(prompt_2)}") + elif prompt_3 is not None and (not isinstance(prompt_3, str) and not isinstance(prompt_3, list)): + raise ValueError(f"`prompt_3` has to be of type `str` or `list` but is {type(prompt_3)}") + elif prompt_4 is not None and (not isinstance(prompt_4, str) and not isinstance(prompt_4, list)): + raise ValueError(f"`prompt_4` has to be of type `str` or `list` but is {type(prompt_4)}") + + if negative_prompt is not None and negative_pooled_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_pooled_prompt_embeds`:" + f" {negative_pooled_prompt_embeds}. Please make sure to only forward one of the two." + ) + elif negative_prompt_2 is not None and negative_pooled_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt_2`: {negative_prompt_2} and `negative_pooled_prompt_embeds`:" + f" {negative_pooled_prompt_embeds}. Please make sure to only forward one of the two." + ) + elif negative_prompt_3 is not None and negative_prompt_embeds_t5 is not None: + raise ValueError( + f"Cannot forward both `negative_prompt_3`: {negative_prompt_3} and `negative_prompt_embeds_t5`:" + f" {negative_prompt_embeds_t5}. Please make sure to only forward one of the two." + ) + elif negative_prompt_4 is not None and negative_prompt_embeds_llama3 is not None: + raise ValueError( + f"Cannot forward both `negative_prompt_4`: {negative_prompt_4} and `negative_prompt_embeds_llama3`:" + f" {negative_prompt_embeds_llama3}. Please make sure to only forward one of the two." + ) + + if pooled_prompt_embeds is not None and negative_pooled_prompt_embeds is not None: + if pooled_prompt_embeds.shape != negative_pooled_prompt_embeds.shape: + raise ValueError( + "`pooled_prompt_embeds` and `negative_pooled_prompt_embeds`" + " must have the same shape when passed directly, but" + f" got: `pooled_prompt_embeds` {pooled_prompt_embeds.shape} != `negative_pooled_prompt_embeds`" + f" {negative_pooled_prompt_embeds.shape}." + ) + if prompt_embeds_t5 is not None and negative_prompt_embeds_t5 is not None: + if prompt_embeds_t5.shape != negative_prompt_embeds_t5.shape: + raise ValueError( + "`prompt_embeds_t5` and `negative_prompt_embeds_t5`" + " must have the same shape when passed directly, but" + f" got: `prompt_embeds_t5` {prompt_embeds_t5.shape} != `negative_prompt_embeds_t5`" + f" {negative_prompt_embeds_t5.shape}." + ) + if prompt_embeds_llama3 is not None and negative_prompt_embeds_llama3 is not None: + if prompt_embeds_llama3.shape != negative_prompt_embeds_llama3.shape: + raise ValueError( + "`prompt_embeds_llama3` and `negative_prompt_embeds_llama3`" + " must have the same shape when passed directly, but" + f" got: `prompt_embeds_llama3` {prompt_embeds_llama3.shape} != `negative_prompt_embeds_llama3`" + f" {negative_prompt_embeds_llama3.shape}." + ) + + def _get_t5_prompt_embeds( + self, + prompt: str | list[str] = None, + max_sequence_length: int = 128, + dtype: torch.dtype | None = None, + ): + dtype = dtype or self.text_encoder_3.dtype + + prompt = [prompt] if isinstance(prompt, str) else prompt + + text_inputs = self.tokenizer_3( + prompt, + padding="max_length", + max_length=min(max_sequence_length, self.tokenizer_3.model_max_length), + truncation=True, + add_special_tokens=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + attention_mask = text_inputs.attention_mask + untruncated_ids = self.tokenizer_3(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer_3.batch_decode( + untruncated_ids[:, min(max_sequence_length, self.tokenizer_3.model_max_length) - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because `max_sequence_length` is set to " + f" {min(max_sequence_length, self.tokenizer_3.model_max_length)} tokens: {removed_text}" + ) + + prompt_embeds = self.text_encoder_3( + text_input_ids.to(self.device), attention_mask=attention_mask.to(self.device) + )[0] + prompt_embeds = prompt_embeds.to(dtype=dtype, device=self.device) + return prompt_embeds + + def _get_clip_prompt_embeds( + self, + tokenizer, + text_encoder, + prompt: str | list[str], + max_sequence_length: int = 128, + dtype: torch.dtype | None = None, + ): + dtype = dtype or text_encoder.dtype + + prompt = [prompt] if isinstance(prompt, str) else prompt + + model_max_length = self.tokenizer.model_max_length + text_inputs = tokenizer( + prompt, + padding="max_length", + max_length=min(max_sequence_length, model_max_length), + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids): + removed_text = tokenizer.batch_decode(untruncated_ids[:, model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {model_max_length} tokens: {removed_text}" + ) + prompt_embeds = text_encoder(text_input_ids.to(self.device), output_hidden_states=True) + + # Use pooled output of CLIPTextModel + prompt_embeds = prompt_embeds[0] + prompt_embeds = prompt_embeds.to(dtype=dtype, device=self.device) + return prompt_embeds + + def _get_llama3_prompt_embeds( + self, + prompt: str | list[str] = None, + max_sequence_length: int = 128, + dtype: torch.dtype | None = None, + ): + dtype = dtype or self.text_encoder_4.dtype + + prompt = [prompt] if isinstance(prompt, str) else prompt + + text_inputs = self.tokenizer_4( + prompt, + padding="max_length", + max_length=min(max_sequence_length, self.tokenizer_4.model_max_length), + truncation=True, + add_special_tokens=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + attention_mask = text_inputs.attention_mask + untruncated_ids = self.tokenizer_4(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer_4.batch_decode( + untruncated_ids[:, min(max_sequence_length, self.tokenizer_4.model_max_length) - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because `max_sequence_length` is set to " + f" {min(max_sequence_length, self.tokenizer_4.model_max_length)} tokens: {removed_text}" + ) + + outputs = self.text_encoder_4( + text_input_ids.to(self.device), + attention_mask=attention_mask.to(self.device), + output_hidden_states=True, + ) + + prompt_embeds = outputs.hidden_states[1:] + prompt_embeds = torch.stack(prompt_embeds, dim=0) + return prompt_embeds + + def encode_prompt( + self, + prompt: str | list[str] | None = None, + prompt_2: str | list[str] | None = None, + prompt_3: str | list[str] | None = None, + prompt_4: str | list[str] | None = None, + dtype: torch.dtype | None = None, + num_images_per_prompt: int = 1, + do_classifier_free_guidance: bool = True, + negative_prompt: str | list[str] | None = None, + negative_prompt_2: str | list[str] | None = None, + negative_prompt_3: str | list[str] | None = None, + negative_prompt_4: str | list[str] | None = None, + prompt_embeds_t5: list[torch.FloatTensor] | None = None, + prompt_embeds_llama3: list[torch.FloatTensor] | None = None, + negative_prompt_embeds_t5: list[torch.FloatTensor] | None = None, + negative_prompt_embeds_llama3: list[torch.FloatTensor] | None = None, + pooled_prompt_embeds: torch.FloatTensor | None = None, + negative_pooled_prompt_embeds: torch.FloatTensor | None = None, + max_sequence_length: int = 128, + lora_scale: float | None = None, + ): + prompt = [prompt] if isinstance(prompt, str) else prompt + if prompt is not None: + batch_size = len(prompt) + else: + batch_size = pooled_prompt_embeds.shape[0] + + if pooled_prompt_embeds is None: + pooled_prompt_embeds_1 = self._get_clip_prompt_embeds( + self.tokenizer, self.text_encoder, prompt, max_sequence_length, dtype + ) + + if do_classifier_free_guidance and negative_pooled_prompt_embeds is None: + negative_prompt = negative_prompt or "" + negative_prompt = [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt + + if len(negative_prompt) > 1 and len(negative_prompt) != batch_size: + raise ValueError(f"negative_prompt must be of length 1 or {batch_size}") + + negative_pooled_prompt_embeds_1 = self._get_clip_prompt_embeds( + self.tokenizer, self.text_encoder, negative_prompt, max_sequence_length, dtype + ) + + if negative_pooled_prompt_embeds_1.shape[0] == 1 and batch_size > 1: + negative_pooled_prompt_embeds_1 = negative_pooled_prompt_embeds_1.repeat(batch_size, 1) + + if pooled_prompt_embeds is None: + prompt_2 = prompt_2 or prompt + prompt_2 = [prompt_2] if isinstance(prompt_2, str) else prompt_2 + + if len(prompt_2) > 1 and len(prompt_2) != batch_size: + raise ValueError(f"prompt_2 must be of length 1 or {batch_size}") + + pooled_prompt_embeds_2 = self._get_clip_prompt_embeds( + self.tokenizer_2, self.text_encoder_2, prompt_2, max_sequence_length, dtype + ) + + if pooled_prompt_embeds_2.shape[0] == 1 and batch_size > 1: + pooled_prompt_embeds_2 = pooled_prompt_embeds_2.repeat(batch_size, 1) + + if do_classifier_free_guidance and negative_pooled_prompt_embeds is None: + negative_prompt_2 = negative_prompt_2 or negative_prompt + negative_prompt_2 = [negative_prompt_2] if isinstance(negative_prompt_2, str) else negative_prompt_2 + + if len(negative_prompt_2) > 1 and len(negative_prompt_2) != batch_size: + raise ValueError(f"negative_prompt_2 must be of length 1 or {batch_size}") + + negative_pooled_prompt_embeds_2 = self._get_clip_prompt_embeds( + self.tokenizer_2, self.text_encoder_2, negative_prompt_2, max_sequence_length, dtype + ) + + if negative_pooled_prompt_embeds_2.shape[0] == 1 and batch_size > 1: + negative_pooled_prompt_embeds_2 = negative_pooled_prompt_embeds_2.repeat(batch_size, 1) + + if pooled_prompt_embeds is None: + pooled_prompt_embeds = torch.cat([pooled_prompt_embeds_1, pooled_prompt_embeds_2], dim=-1) + + if do_classifier_free_guidance and negative_pooled_prompt_embeds is None: + negative_pooled_prompt_embeds = torch.cat( + [negative_pooled_prompt_embeds_1, negative_pooled_prompt_embeds_2], dim=-1 + ) + + if prompt_embeds_t5 is None: + prompt_3 = prompt_3 or prompt + prompt_3 = [prompt_3] if isinstance(prompt_3, str) else prompt_3 + + if len(prompt_3) > 1 and len(prompt_3) != batch_size: + raise ValueError(f"prompt_3 must be of length 1 or {batch_size}") + + prompt_embeds_t5 = self._get_t5_prompt_embeds(prompt_3, max_sequence_length, dtype) + + if prompt_embeds_t5.shape[0] == 1 and batch_size > 1: + prompt_embeds_t5 = prompt_embeds_t5.repeat(batch_size, 1, 1) + + if do_classifier_free_guidance and negative_prompt_embeds_t5 is None: + negative_prompt_3 = negative_prompt_3 or negative_prompt + negative_prompt_3 = [negative_prompt_3] if isinstance(negative_prompt_3, str) else negative_prompt_3 + + if len(negative_prompt_3) > 1 and len(negative_prompt_3) != batch_size: + raise ValueError(f"negative_prompt_3 must be of length 1 or {batch_size}") + + negative_prompt_embeds_t5 = self._get_t5_prompt_embeds(negative_prompt_3, max_sequence_length, dtype) + + if negative_prompt_embeds_t5.shape[0] == 1 and batch_size > 1: + negative_prompt_embeds_t5 = negative_prompt_embeds_t5.repeat(batch_size, 1, 1) + + if prompt_embeds_llama3 is None: + prompt_4 = prompt_4 or prompt + prompt_4 = [prompt_4] if isinstance(prompt_4, str) else prompt_4 + + if len(prompt_4) > 1 and len(prompt_4) != batch_size: + raise ValueError(f"prompt_4 must be of length 1 or {batch_size}") + + prompt_embeds_llama3 = self._get_llama3_prompt_embeds(prompt_4, max_sequence_length, dtype) + + if prompt_embeds_llama3.shape[0] == 1 and batch_size > 1: + prompt_embeds_llama3 = prompt_embeds_llama3.repeat(1, batch_size, 1, 1) + + if do_classifier_free_guidance and negative_prompt_embeds_llama3 is None: + negative_prompt_4 = negative_prompt_4 or negative_prompt + negative_prompt_4 = [negative_prompt_4] if isinstance(negative_prompt_4, str) else negative_prompt_4 + + if len(negative_prompt_4) > 1 and len(negative_prompt_4) != batch_size: + raise ValueError(f"negative_prompt_4 must be of length 1 or {batch_size}") + + negative_prompt_embeds_llama3 = self._get_llama3_prompt_embeds( + negative_prompt_4, max_sequence_length, dtype + ) + + if negative_prompt_embeds_llama3.shape[0] == 1 and batch_size > 1: + negative_prompt_embeds_llama3 = negative_prompt_embeds_llama3.repeat(1, batch_size, 1, 1) + + # duplicate pooled_prompt_embeds for each generation per prompt + pooled_prompt_embeds = pooled_prompt_embeds.repeat(1, num_images_per_prompt) + pooled_prompt_embeds = pooled_prompt_embeds.view(batch_size * num_images_per_prompt, -1) + + # duplicate t5_prompt_embeds for batch_size and num_images_per_prompt + bs_embed, seq_len, _ = prompt_embeds_t5.shape + if bs_embed == 1 and batch_size > 1: + prompt_embeds_t5 = prompt_embeds_t5.repeat(batch_size, 1, 1) + elif bs_embed > 1 and bs_embed != batch_size: + raise ValueError(f"cannot duplicate prompt_embeds_t5 of batch size {bs_embed}") + prompt_embeds_t5 = prompt_embeds_t5.repeat(1, num_images_per_prompt, 1) + prompt_embeds_t5 = prompt_embeds_t5.view(batch_size * num_images_per_prompt, seq_len, -1) + + # duplicate llama3_prompt_embeds for batch_size and num_images_per_prompt + _, bs_embed, seq_len, dim = prompt_embeds_llama3.shape + if bs_embed == 1 and batch_size > 1: + prompt_embeds_llama3 = prompt_embeds_llama3.repeat(1, batch_size, 1, 1) + elif bs_embed > 1 and bs_embed != batch_size: + raise ValueError(f"cannot duplicate prompt_embeds_llama3 of batch size {bs_embed}") + prompt_embeds_llama3 = prompt_embeds_llama3.repeat(1, 1, num_images_per_prompt, 1) + prompt_embeds_llama3 = prompt_embeds_llama3.view(-1, batch_size * num_images_per_prompt, seq_len, dim) + + if do_classifier_free_guidance: + # duplicate negative_pooled_prompt_embeds for batch_size and num_images_per_prompt + bs_embed, seq_len = negative_pooled_prompt_embeds.shape + if bs_embed == 1 and batch_size > 1: + negative_pooled_prompt_embeds = negative_pooled_prompt_embeds.repeat(batch_size, 1) + elif bs_embed > 1 and bs_embed != batch_size: + raise ValueError(f"cannot duplicate negative_pooled_prompt_embeds of batch size {bs_embed}") + negative_pooled_prompt_embeds = negative_pooled_prompt_embeds.repeat(1, num_images_per_prompt) + negative_pooled_prompt_embeds = negative_pooled_prompt_embeds.view(batch_size * num_images_per_prompt, -1) + + # duplicate negative_t5_prompt_embeds for batch_size and num_images_per_prompt + bs_embed, seq_len, _ = negative_prompt_embeds_t5.shape + if bs_embed == 1 and batch_size > 1: + negative_prompt_embeds_t5 = negative_prompt_embeds_t5.repeat(batch_size, 1, 1) + elif bs_embed > 1 and bs_embed != batch_size: + raise ValueError(f"cannot duplicate negative_prompt_embeds_t5 of batch size {bs_embed}") + negative_prompt_embeds_t5 = negative_prompt_embeds_t5.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds_t5 = negative_prompt_embeds_t5.view(batch_size * num_images_per_prompt, seq_len, -1) + + # duplicate negative_prompt_embeds_llama3 for batch_size and num_images_per_prompt + _, bs_embed, seq_len, dim = negative_prompt_embeds_llama3.shape + if bs_embed == 1 and batch_size > 1: + negative_prompt_embeds_llama3 = negative_prompt_embeds_llama3.repeat(1, batch_size, 1, 1) + elif bs_embed > 1 and bs_embed != batch_size: + raise ValueError(f"cannot duplicate negative_prompt_embeds_llama3 of batch size {bs_embed}") + negative_prompt_embeds_llama3 = negative_prompt_embeds_llama3.repeat(1, 1, num_images_per_prompt, 1) + negative_prompt_embeds_llama3 = negative_prompt_embeds_llama3.view( + -1, batch_size * num_images_per_prompt, seq_len, dim + ) + + return ( + prompt_embeds_t5, + negative_prompt_embeds_t5, + prompt_embeds_llama3, + negative_prompt_embeds_llama3, + pooled_prompt_embeds, + negative_pooled_prompt_embeds, + ) + + def enable_vae_slicing(self): + r""" + Enable sliced VAE decoding. When this option is enabled, the VAE will split the input tensor in slices to + compute decoding in several steps. This is useful to save some memory and allow larger batch sizes. + """ + depr_message = ( + f"Calling `enable_vae_slicing()` on a `{self.__class__.__name__}` is deprecated and this" + " method will be removed in a future version. Please use `pipe.vae.enable_slicing()`." + ) + deprecate( + "enable_vae_slicing", + "0.40.0", + depr_message, + ) + self.vae.enable_slicing() + + def disable_vae_slicing(self): + r""" + Disable sliced VAE decoding. If `enable_vae_slicing` was previously enabled, this method will go back to + computing decoding in one step. + """ + depr_message = ( + f"Calling `disable_vae_slicing()` on a `{self.__class__.__name__}` is deprecated and this" + " method will be removed in a future version. Please use `pipe.vae.disable_slicing()`." + ) + deprecate( + "disable_vae_slicing", + "0.40.0", + depr_message, + ) + self.vae.disable_slicing() + + def enable_vae_tiling(self): + r""" + Enable tiled VAE decoding. When this option is enabled, the VAE will split the input tensor into tiles to + compute decoding and encoding in several steps. This is useful for saving a large amount of memory and to allow + processing larger images. + """ + depr_message = ( + f"Calling `enable_vae_tiling()` on a `{self.__class__.__name__}`" + " is deprecated and this method will be removed in a future version. Please use `pipe.vae.enable_tiling()`." + ) + deprecate( + "enable_vae_tiling", + "0.40.0", + depr_message, + ) + self.vae.enable_tiling() + + def disable_vae_tiling(self): + r""" + Disable tiled VAE decoding. If `enable_vae_tiling` was previously enabled, this method will go back to + computing decoding in one step. + """ + depr_message = ( + f"Calling `disable_vae_tiling()` on a `{self.__class__.__name__}` is" + " deprecated and this method will be removed in a future version. Please use `pipe.vae.disable_tiling()`." + ) + deprecate( + "disable_vae_tiling", + "0.40.0", + depr_message, + ) + self.vae.disable_tiling() + + def prepare_latents( + self, + batch_size, + num_channels_latents, + height, + width, + dtype, + generator, + latents=None, + ): + # VAE applies 8x compression on images but we must also account for packing which requires + # latent height and width to be divisible by 2. + height = 2 * (int(height) // (self.vae_scale_factor * 2)) + width = 2 * (int(width) // (self.vae_scale_factor * 2)) + + shape = (batch_size, num_channels_latents, height, width) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=self.device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(self.device) + return latents + + def prepare_timesteps(self, num_inference_steps, sigmas, image_seq_len): + sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas + mu = calculate_shift( + image_seq_len, + self.scheduler.config.get("base_image_seq_len", 256), + self.scheduler.config.get("max_image_seq_len", 4096), + self.scheduler.config.get("base_shift", 0.5), + self.scheduler.config.get("max_shift", 1.15), + ) + timesteps, num_inference_steps = retrieve_timesteps( + self.scheduler, + num_inference_steps, + self.device, + sigmas=sigmas, + mu=mu, + ) + return timesteps, num_inference_steps + + def _extract_prompts(self, prompts): + """Extract prompt and negative_prompt from OmniPromptType list.""" + prompt = [p if isinstance(p, str) else (p.get("prompt") or "") for p in prompts] or None + if all(isinstance(p, str) or p.get("negative_prompt") is None for p in prompts): + negative_prompt = None + elif prompts: + negative_prompt = ["" if isinstance(p, str) else (p.get("negative_prompt") or "") for p in prompts] + else: + negative_prompt = None + return prompt, negative_prompt + + def check_cfg_parallel_validity(self, true_cfg_scale: float, has_neg_prompt: bool): + if get_classifier_free_guidance_world_size() == 1: + return True + + if true_cfg_scale <= 1: + logger.warning("CFG parallel is NOT working correctly when true_cfg_scale <= 1.") + return False + + if not has_neg_prompt: + logger.warning( + "CFG parallel is NOT working correctly when there is no negative prompt or negative prompt embeddings." + ) + return False + return True + + def diffuse( + self, + prompt_embeds_t5: torch.Tensor, + prompt_embeds_llama3: torch.Tensor, + pooled_prompt_embeds: torch.Tensor, + latents: torch.Tensor, + timesteps: torch.Tensor, + do_true_cfg: bool, + ) -> torch.Tensor: + with self.progress_bar(total=len(timesteps)) as pbar: + for i, t in enumerate(timesteps): + if self.interrupt: + continue + + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timestep = t.expand(latent_model_input.shape[0]) + noise_pred = self.transformer( + hidden_states=latent_model_input, + timesteps=timestep, + encoder_hidden_states_t5=prompt_embeds_t5, + encoder_hidden_states_llama3=prompt_embeds_llama3, + pooled_embeds=pooled_prompt_embeds, + return_dict=False, + )[0] + noise_pred = -noise_pred + + # TODO: Modify CFG guidance + # perform guidance + if self.do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg) + + pbar.update() + + return latents + + @property + def guidance_scale(self): + return self._guidance_scale + + @property + def do_classifier_free_guidance(self): + return self._guidance_scale > 1 + + @property + def attention_kwargs(self): + return self._attention_kwargs + + @property + def num_timesteps(self): + return self._num_timesteps + + @property + def interrupt(self): + return self._interrupt + + def forward( + self, + req: OmniDiffusionRequest, + prompt: str | list[str] = None, + prompt_2: str | list[str] | None = None, + prompt_3: str | list[str] | None = None, + prompt_4: str | list[str] | None = None, + height: int | None = None, + width: int | None = None, + num_inference_steps: int = 50, + sigmas: list[float] | None = None, + guidance_scale: float = 5.0, + negative_prompt: str | list[str] | None = None, + negative_prompt_2: str | list[str] | None = None, + negative_prompt_3: str | list[str] | None = None, + negative_prompt_4: str | list[str] | None = None, + num_images_per_prompt: int | None = 1, + generator: torch.Generator | list[torch.Generator] | None = None, + latents: torch.FloatTensor | None = None, + prompt_embeds_t5: torch.FloatTensor | None = None, + prompt_embeds_llama3: torch.FloatTensor | None = None, + negative_prompt_embeds_t5: torch.FloatTensor | None = None, + negative_prompt_embeds_llama3: torch.FloatTensor | None = None, + pooled_prompt_embeds: torch.FloatTensor | None = None, + negative_pooled_prompt_embeds: torch.FloatTensor | None = None, + output_type: str | None = "pil", + return_dict: bool = True, + attention_kwargs: dict[str, Any] | None = None, + callback_on_step_end: Callable[[int, int], None] | None = None, + callback_on_step_end_tensor_inputs: list[str] = ["latents"], + max_sequence_length: int = 128, + **kwargs, + ): + extracted_prompt, negative_prompt = self._extract_prompts(req.prompts) + prompt = extracted_prompt or prompt + + height = req.sampling_params.height or self.default_sample_size * self.vae_scale_factor + width = req.sampling_params.width or self.default_sample_size * self.vae_scale_factor + + num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps + sigmas = req.sampling_params.sigmas or sigmas + max_sequence_length = req.sampling_params.max_sequence_length or max_sequence_length + generator = req.sampling_params.generator or generator + true_cfg_scale = req.sampling_params.true_cfg_scale or guidance_scale + if req.sampling_params.guidance_scale_provided: + guidance_scale = req.sampling_params.guidance_scale + num_images_per_prompt = ( + req.sampling_params.num_outputs_per_prompt + if req.sampling_params.num_outputs_per_prompt > 0 + else num_images_per_prompt + ) + + prompt_embeds = kwargs.get("prompt_embeds", None) + negative_prompt_embeds = kwargs.get("negative_prompt_embeds", None) + + if prompt_embeds is not None: + deprecation_message = ( + "The `prompt_embeds` argument is deprecated." + " Please use `prompt_embeds_t5` and `prompt_embeds_llama3` instead." + ) + deprecate("prompt_embeds", "0.35.0", deprecation_message) + prompt_embeds_t5 = prompt_embeds[0] + prompt_embeds_llama3 = prompt_embeds[1] + + if negative_prompt_embeds is not None: + deprecation_message = ( + "The `negative_prompt_embeds` argument is deprecated." + "Please use `negative_prompt_embeds_t5` and `negative_prompt_embeds_llama3` instead." + ) + deprecate("negative_prompt_embeds", "0.35.0", deprecation_message) + negative_prompt_embeds_t5 = negative_prompt_embeds[0] + negative_prompt_embeds_llama3 = negative_prompt_embeds[1] + + division = self.vae_scale_factor * 2 + S_max = (self.default_sample_size * self.vae_scale_factor) ** 2 + scale = S_max / (width * height) + scale = math.sqrt(scale) + width, height = int(width * scale // division * division), int(height * scale // division * division) + + # 1. Check inputs. Raise error if not correct + self.check_inputs( + prompt, + prompt_2, + prompt_3, + prompt_4, + negative_prompt=negative_prompt, + negative_prompt_2=negative_prompt_2, + negative_prompt_3=negative_prompt_3, + negative_prompt_4=negative_prompt_4, + prompt_embeds_t5=prompt_embeds_t5, + prompt_embeds_llama3=prompt_embeds_llama3, + negative_prompt_embeds_t5=negative_prompt_embeds_t5, + negative_prompt_embeds_llama3=negative_prompt_embeds_llama3, + pooled_prompt_embeds=pooled_prompt_embeds, + negative_pooled_prompt_embeds=negative_pooled_prompt_embeds, + callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs, + ) + + self._guidance_scale = guidance_scale + self._attention_kwargs = attention_kwargs + self._interrupt = False + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + elif pooled_prompt_embeds is not None: + batch_size = pooled_prompt_embeds.shape[0] + + # TODO: CFG guidance configuration + has_neg_prompt = negative_prompt is not None or ( + negative_prompt_embeds is not None and negative_pooled_prompt_embeds is not None + ) + do_true_cfg = true_cfg_scale > 1 and has_neg_prompt + self.check_cfg_parallel_validity(true_cfg_scale, has_neg_prompt) + + # 3. Encode prompt + lora_scale = self.attention_kwargs.get("scale", None) if self.attention_kwargs is not None else None + ( + prompt_embeds_t5, + negative_prompt_embeds_t5, + prompt_embeds_llama3, + negative_prompt_embeds_llama3, + pooled_prompt_embeds, + negative_pooled_prompt_embeds, + ) = self.encode_prompt( + prompt=prompt, + prompt_2=prompt_2, + prompt_3=prompt_3, + prompt_4=prompt_4, + negative_prompt=negative_prompt, + negative_prompt_2=negative_prompt_2, + negative_prompt_3=negative_prompt_3, + negative_prompt_4=negative_prompt_4, + do_classifier_free_guidance=self.do_classifier_free_guidance, + prompt_embeds_t5=prompt_embeds_t5, + prompt_embeds_llama3=prompt_embeds_llama3, + negative_prompt_embeds_t5=negative_prompt_embeds_t5, + negative_prompt_embeds_llama3=negative_prompt_embeds_llama3, + pooled_prompt_embeds=pooled_prompt_embeds, + negative_pooled_prompt_embeds=negative_pooled_prompt_embeds, + num_images_per_prompt=num_images_per_prompt, + max_sequence_length=max_sequence_length, + lora_scale=lora_scale, + ) + + if self.do_classifier_free_guidance: + prompt_embeds_t5 = torch.cat([negative_prompt_embeds_t5, prompt_embeds_t5], dim=0) + prompt_embeds_llama3 = torch.cat([negative_prompt_embeds_llama3, prompt_embeds_llama3], dim=1) + pooled_prompt_embeds = torch.cat([negative_pooled_prompt_embeds, pooled_prompt_embeds], dim=0) + + # 4. Prepare latent variables + num_channels_latents = self.transformer.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + pooled_prompt_embeds.dtype, + generator, + latents, + ) + + # 5. Prepare timesteps + timesteps, num_inference_steps = self.prepare_timesteps(num_inference_steps, sigmas, latents.shape[1]) + self._num_timesteps = len(timesteps) + + # 6. Denoising loop + latents = self.diffuse( + prompt_embeds_t5, + prompt_embeds_llama3, + pooled_prompt_embeds, + latents, + timesteps, + do_true_cfg, + ) + + if output_type == "latent": + image = latents + + else: + latents = (latents / self.vae.config.scaling_factor) + self.vae.config.shift_factor + + image = self.vae.decode(latents, return_dict=False)[0] + + return DiffusionOutput( + output=image, stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None + ) + + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: + loader = AutoWeightsLoader(self) + return loader.load_weights(weights) diff --git a/vllm_omni/diffusion/models/hidream_image/pipeline_hidream_image.py b/vllm_omni/diffusion/models/hidream_image/pipeline_hidream_image.py new file mode 100644 index 000000000..0c5486ae5 --- /dev/null +++ b/vllm_omni/diffusion/models/hidream_image/pipeline_hidream_image.py @@ -0,0 +1,1027 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +import inspect +import json +import math +import os +from collections.abc import Callable, Iterable +from typing import Any + +import numpy as np +import torch +from diffusers.image_processor import VaeImageProcessor +from diffusers.models import AutoencoderKL +from diffusers.schedulers import FlowMatchEulerDiscreteScheduler +from diffusers.utils import deprecate, logging +from diffusers.utils.torch_utils import randn_tensor +from torch import nn +from transformers import ( + CLIPTextModelWithProjection, + CLIPTokenizer, + LlamaForCausalLM, + PreTrainedTokenizerFast, + T5EncoderModel, + T5Tokenizer, +) +from vllm.model_executor.models.utils import AutoWeightsLoader + +from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig +from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin +from vllm_omni.diffusion.distributed.parallel_state import get_classifier_free_guidance_world_size +from vllm_omni.diffusion.distributed.utils import get_local_device +from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader +from vllm_omni.diffusion.models.hidream_image import HiDreamImageTransformer2DModel +from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin +from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin +from vllm_omni.diffusion.request import OmniDiffusionRequest +from vllm_omni.diffusion.utils.tf_utils import get_transformer_config_kwargs +from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific + +logger = logging.get_logger(__name__) + + +def get_hidream_image_post_process_func( + od_config: OmniDiffusionConfig, +): + model_name = od_config.model + if os.path.exists(model_name): + model_path = model_name + else: + model_path = download_weights_from_hf_specific(model_name, None, ["*"]) + vae_config_path = os.path.join(model_path, "vae/config.json") + if not os.path.exists(vae_config_path): + raise FileNotFoundError( + f"VAE config not found at {vae_config_path}. " + "Please ensure the model path contains a valid VAE configuration." + ) + with open(vae_config_path) as f: + vae_config = json.load(f) + vae_scale_factor = 2 ** (len(vae_config["block_out_channels"]) - 1) if "block_out_channels" in vae_config else 8 + + image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor * 2) + + def post_process_func( + images: torch.Tensor, + ): + return image_processor.postprocess(images) + + return post_process_func + + +# Copied from diffusers.pipelines.flux.pipeline_flux.calculate_shift +def calculate_shift( + image_seq_len, + base_seq_len: int = 256, + max_seq_len: int = 4096, + base_shift: float = 0.5, + max_shift: float = 1.15, +): + m = (max_shift - base_shift) / (max_seq_len - base_seq_len) + b = base_shift - m * base_seq_len + mu = image_seq_len * m + b + return mu + + +# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps +def retrieve_timesteps( + scheduler, + num_inference_steps: int | None = None, + device: str | torch.device | None = None, + timesteps: list[int] | None = None, + sigmas: list[float] | None = None, + **kwargs, +): + r""" + Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles + custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`. + + Args: + scheduler (`SchedulerMixin`): + The scheduler to get timesteps from. + num_inference_steps (`int`): + The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps` + must be `None`. + device (`str` or `torch.device`, *optional*): + The device to which the timesteps should be moved to. If `None`, the timesteps are not moved. + timesteps (`list[int]`, *optional*): + Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed, + `num_inference_steps` and `sigmas` must be `None`. + sigmas (`list[float]`, *optional*): + Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed, + `num_inference_steps` and `timesteps` must be `None`. + + Returns: + timesteps (`torch.Tensor`): The timestep schedule from the scheduler. + num_inference_steps (`int`): The number of inference steps. + """ + if timesteps is not None and sigmas is not None: + raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values") + if timesteps is not None: + accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys()) + if not accepts_timesteps: + raise ValueError( + f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom" + f" timestep schedules. Please check whether you are using the correct scheduler." + ) + scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs) + timesteps = scheduler.timesteps + num_inference_steps = len(timesteps) + elif sigmas is not None: + accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys()) + if not accept_sigmas: + raise ValueError( + f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom" + f" sigmas schedules. Please check whether you are using the correct scheduler." + ) + scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs) + timesteps = scheduler.timesteps + num_inference_steps = len(timesteps) + else: + scheduler.set_timesteps(num_inference_steps, device=device, **kwargs) + timesteps = scheduler.timesteps + return timesteps, num_inference_steps + + +class HiDreamImagePipeline(nn.Module, CFGParallelMixin, DiffusionPipelineProfilerMixin, ProgressBarMixin): + def __init__( + self, + *, + od_config: OmniDiffusionConfig, + prefix: str = "", + ): + super().__init__() + self.od_config = od_config + dtype = getattr(od_config, "dtype", torch.bfloat16) + llama_path = self.od_config.extras["auxiliary_text_encoder"] + if llama_path is None: + logger.warning( + f"auxiliary_text_encoder is not provided. " + f"Attempting to load default LLaMA model: {self.od_config.extras['default_llama_model_id']}" + ) + llama_path = self.od_config.extras["default_llama_model_id"] + logger.info(f"Loading LLAMA model from: {llama_path}") + + self.weights_sources = [ + DiffusersPipelineLoader.ComponentSource( + model_or_path=od_config.model, + subfolder="transformer", + revision=None, + prefix="transformer.", + fall_back_to_pt=True, + ), + ] + + self.device = get_local_device() + model = od_config.model + # Check if model is a local path + local_files_only = os.path.exists(model) + + self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained( + model, subfolder="scheduler", local_files_only=local_files_only + ) + self.vae = AutoencoderKL.from_pretrained(model, subfolder="vae", local_files_only=local_files_only).to( + self.device + ) + self.text_encoder = CLIPTextModelWithProjection.from_pretrained( + model, subfolder="text_encoder", local_files_only=local_files_only + ) + self.tokenizer = CLIPTokenizer.from_pretrained(model, subfolder="tokenizer", local_files_only=local_files_only) + self.text_encoder_2 = CLIPTextModelWithProjection.from_pretrained( + model, subfolder="text_encoder_2", local_files_only=local_files_only + ) + self.tokenizer_2 = CLIPTokenizer.from_pretrained( + model, subfolder="tokenizer_2", local_files_only=local_files_only + ) + self.text_encoder_3 = T5EncoderModel.from_pretrained( + model, subfolder="text_encoder_3", local_files_only=local_files_only + ) + self.tokenizer_3 = T5Tokenizer.from_pretrained( + model, subfolder="tokenizer_3", local_files_only=local_files_only + ) + self.text_encoder_4 = LlamaForCausalLM.from_pretrained(llama_path, output_hidden_states=True, dtype=dtype).to( + self.device + ) + self.tokenizer_4 = PreTrainedTokenizerFast.from_pretrained(llama_path, use_fast=False) + transformer_kwargs = get_transformer_config_kwargs(od_config.tf_model_config, HiDreamImageTransformer2DModel) + self.transformer = HiDreamImageTransformer2DModel( + od_config=od_config, quant_config=od_config.quantization_config, **transformer_kwargs + ) + + self.stage = None + + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) if getattr(self, "vae", None) else 8 + # HiDreamImage latents are turned into 2x2 patches and packed. + # This means the latent width and height has to be divisible + # by the patch size. So the vae scale factor is multiplied by the patch size to account for this + self.default_sample_size = 128 + if getattr(self, "tokenizer_4", None) is not None: + self.tokenizer_4.pad_token = self.tokenizer_4.eos_token + + self.setup_diffusion_pipeline_profiler( + enable_diffusion_pipeline_profiler=self.od_config.enable_diffusion_pipeline_profiler + ) + + def check_inputs( + self, + prompt, + prompt_2, + prompt_3, + prompt_4, + negative_prompt=None, + negative_prompt_2=None, + negative_prompt_3=None, + negative_prompt_4=None, + prompt_embeds_t5=None, + prompt_embeds_llama3=None, + negative_prompt_embeds_t5=None, + negative_prompt_embeds_llama3=None, + pooled_prompt_embeds=None, + negative_pooled_prompt_embeds=None, + callback_on_step_end_tensor_inputs=None, + ): + if prompt is not None and pooled_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt`: {prompt} and `pooled_prompt_embeds`: {pooled_prompt_embeds}. " + "Please make sure to only forward one of the two." + ) + elif prompt_2 is not None and pooled_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `prompt_2`: {prompt_2} and `pooled_prompt_embeds`: {pooled_prompt_embeds}." + "Please make sure to only forward one of the two." + ) + elif prompt_3 is not None and prompt_embeds_t5 is not None: + raise ValueError( + f"Cannot forward both `prompt_3`: {prompt_3} and `prompt_embeds_t5`: {prompt_embeds_t5}." + "Please make sure to only forward one of the two." + ) + elif prompt_4 is not None and prompt_embeds_llama3 is not None: + raise ValueError( + f"Cannot forward both `prompt_4`: {prompt_4} and `prompt_embeds_llama3`: {prompt_embeds_llama3}." + "Please make sure to only forward one of the two." + ) + elif prompt is None and pooled_prompt_embeds is None: + raise ValueError( + "Provide either `prompt` or `pooled_prompt_embeds`." + "Cannot leave both `prompt` and `pooled_prompt_embeds` undefined." + ) + elif prompt is None and prompt_embeds_t5 is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds_t5`." + "Cannot leave both `prompt` and `prompt_embeds_t5` undefined." + ) + elif prompt is None and prompt_embeds_llama3 is None: + raise ValueError( + "Provide either `prompt` or `prompt_embeds_llama3`." + "Cannot leave both `prompt` and `prompt_embeds_llama3` undefined." + ) + elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)): + raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}") + elif prompt_2 is not None and (not isinstance(prompt_2, str) and not isinstance(prompt_2, list)): + raise ValueError(f"`prompt_2` has to be of type `str` or `list` but is {type(prompt_2)}") + elif prompt_3 is not None and (not isinstance(prompt_3, str) and not isinstance(prompt_3, list)): + raise ValueError(f"`prompt_3` has to be of type `str` or `list` but is {type(prompt_3)}") + elif prompt_4 is not None and (not isinstance(prompt_4, str) and not isinstance(prompt_4, list)): + raise ValueError(f"`prompt_4` has to be of type `str` or `list` but is {type(prompt_4)}") + + if negative_prompt is not None and negative_pooled_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_pooled_prompt_embeds`:" + f" {negative_pooled_prompt_embeds}. Please make sure to only forward one of the two." + ) + elif negative_prompt_2 is not None and negative_pooled_prompt_embeds is not None: + raise ValueError( + f"Cannot forward both `negative_prompt_2`: {negative_prompt_2} and `negative_pooled_prompt_embeds`:" + f" {negative_pooled_prompt_embeds}. Please make sure to only forward one of the two." + ) + elif negative_prompt_3 is not None and negative_prompt_embeds_t5 is not None: + raise ValueError( + f"Cannot forward both `negative_prompt_3`: {negative_prompt_3} and `negative_prompt_embeds_t5`:" + f" {negative_prompt_embeds_t5}. Please make sure to only forward one of the two." + ) + elif negative_prompt_4 is not None and negative_prompt_embeds_llama3 is not None: + raise ValueError( + f"Cannot forward both `negative_prompt_4`: {negative_prompt_4} and `negative_prompt_embeds_llama3`:" + f" {negative_prompt_embeds_llama3}. Please make sure to only forward one of the two." + ) + + if pooled_prompt_embeds is not None and negative_pooled_prompt_embeds is not None: + if pooled_prompt_embeds.shape != negative_pooled_prompt_embeds.shape: + raise ValueError( + "`pooled_prompt_embeds` and `negative_pooled_prompt_embeds`" + " must have the same shape when passed directly, but" + f" got: `pooled_prompt_embeds` {pooled_prompt_embeds.shape} != `negative_pooled_prompt_embeds`" + f" {negative_pooled_prompt_embeds.shape}." + ) + if prompt_embeds_t5 is not None and negative_prompt_embeds_t5 is not None: + if prompt_embeds_t5.shape != negative_prompt_embeds_t5.shape: + raise ValueError( + "`prompt_embeds_t5` and `negative_prompt_embeds_t5`" + " must have the same shape when passed directly, but" + f" got: `prompt_embeds_t5` {prompt_embeds_t5.shape} != `negative_prompt_embeds_t5`" + f" {negative_prompt_embeds_t5.shape}." + ) + if prompt_embeds_llama3 is not None and negative_prompt_embeds_llama3 is not None: + if prompt_embeds_llama3.shape != negative_prompt_embeds_llama3.shape: + raise ValueError( + "`prompt_embeds_llama3` and `negative_prompt_embeds_llama3`" + " must have the same shape when passed directly, but" + f" got: `prompt_embeds_llama3` {prompt_embeds_llama3.shape} != `negative_prompt_embeds_llama3`" + f" {negative_prompt_embeds_llama3.shape}." + ) + + def _get_t5_prompt_embeds( + self, + prompt: str | list[str] = None, + max_sequence_length: int = 128, + dtype: torch.dtype | None = None, + ): + dtype = dtype or self.text_encoder_3.dtype + + prompt = [prompt] if isinstance(prompt, str) else prompt + + text_inputs = self.tokenizer_3( + prompt, + padding="max_length", + max_length=min(max_sequence_length, self.tokenizer_3.model_max_length), + truncation=True, + add_special_tokens=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + attention_mask = text_inputs.attention_mask + untruncated_ids = self.tokenizer_3(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer_3.batch_decode( + untruncated_ids[:, min(max_sequence_length, self.tokenizer_3.model_max_length) - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because `max_sequence_length` is set to " + f" {min(max_sequence_length, self.tokenizer_3.model_max_length)} tokens: {removed_text}" + ) + + prompt_embeds = self.text_encoder_3( + text_input_ids.to(self.device), attention_mask=attention_mask.to(self.device) + )[0] + prompt_embeds = prompt_embeds.to(dtype=dtype, device=self.device) + return prompt_embeds + + def _get_clip_prompt_embeds( + self, + tokenizer, + text_encoder, + prompt: str | list[str], + max_sequence_length: int = 128, + dtype: torch.dtype | None = None, + ): + dtype = dtype or text_encoder.dtype + + prompt = [prompt] if isinstance(prompt, str) else prompt + + model_max_length = self.tokenizer.model_max_length + text_inputs = tokenizer( + prompt, + padding="max_length", + max_length=min(max_sequence_length, model_max_length), + truncation=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + untruncated_ids = tokenizer(prompt, padding="longest", return_tensors="pt").input_ids + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids): + removed_text = tokenizer.batch_decode(untruncated_ids[:, model_max_length - 1 : -1]) + logger.warning( + "The following part of your input was truncated because CLIP can only handle sequences up to" + f" {model_max_length} tokens: {removed_text}" + ) + prompt_embeds = text_encoder(text_input_ids.to(self.device), output_hidden_states=True) + + # Use pooled output of CLIPTextModel + prompt_embeds = prompt_embeds[0] + prompt_embeds = prompt_embeds.to(dtype=dtype, device=self.device) + return prompt_embeds + + def _get_llama3_prompt_embeds( + self, + prompt: str | list[str] = None, + max_sequence_length: int = 128, + dtype: torch.dtype | None = None, + ): + dtype = dtype or self.text_encoder_4.dtype + + prompt = [prompt] if isinstance(prompt, str) else prompt + + text_inputs = self.tokenizer_4( + prompt, + padding="max_length", + max_length=min(max_sequence_length, self.tokenizer_4.model_max_length), + truncation=True, + add_special_tokens=True, + return_tensors="pt", + ) + text_input_ids = text_inputs.input_ids + attention_mask = text_inputs.attention_mask + untruncated_ids = self.tokenizer_4(prompt, padding="longest", return_tensors="pt").input_ids + + if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids): + removed_text = self.tokenizer_4.batch_decode( + untruncated_ids[:, min(max_sequence_length, self.tokenizer_4.model_max_length) - 1 : -1] + ) + logger.warning( + "The following part of your input was truncated because `max_sequence_length` is set to " + f" {min(max_sequence_length, self.tokenizer_4.model_max_length)} tokens: {removed_text}" + ) + + outputs = self.text_encoder_4( + text_input_ids.to(self.device), + attention_mask=attention_mask.to(self.device), + output_hidden_states=True, + ) + + prompt_embeds = outputs.hidden_states[1:] + prompt_embeds = torch.stack(prompt_embeds, dim=0) + return prompt_embeds + + def encode_prompt( + self, + prompt: str | list[str] | None = None, + prompt_2: str | list[str] | None = None, + prompt_3: str | list[str] | None = None, + prompt_4: str | list[str] | None = None, + dtype: torch.dtype | None = None, + num_images_per_prompt: int = 1, + do_classifier_free_guidance: bool = True, + negative_prompt: str | list[str] | None = None, + negative_prompt_2: str | list[str] | None = None, + negative_prompt_3: str | list[str] | None = None, + negative_prompt_4: str | list[str] | None = None, + prompt_embeds_t5: list[torch.FloatTensor] | None = None, + prompt_embeds_llama3: list[torch.FloatTensor] | None = None, + negative_prompt_embeds_t5: list[torch.FloatTensor] | None = None, + negative_prompt_embeds_llama3: list[torch.FloatTensor] | None = None, + pooled_prompt_embeds: torch.FloatTensor | None = None, + negative_pooled_prompt_embeds: torch.FloatTensor | None = None, + max_sequence_length: int = 128, + lora_scale: float | None = None, + ): + prompt = [prompt] if isinstance(prompt, str) else prompt + if prompt is not None: + batch_size = len(prompt) + else: + batch_size = pooled_prompt_embeds.shape[0] + + if pooled_prompt_embeds is None: + pooled_prompt_embeds_1 = self._get_clip_prompt_embeds( + self.tokenizer, self.text_encoder, prompt, max_sequence_length, dtype + ) + + if do_classifier_free_guidance and negative_pooled_prompt_embeds is None: + negative_prompt = negative_prompt or "" + negative_prompt = [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt + + if len(negative_prompt) > 1 and len(negative_prompt) != batch_size: + raise ValueError(f"negative_prompt must be of length 1 or {batch_size}") + + negative_pooled_prompt_embeds_1 = self._get_clip_prompt_embeds( + self.tokenizer, self.text_encoder, negative_prompt, max_sequence_length, dtype + ) + + if negative_pooled_prompt_embeds_1.shape[0] == 1 and batch_size > 1: + negative_pooled_prompt_embeds_1 = negative_pooled_prompt_embeds_1.repeat(batch_size, 1) + + if pooled_prompt_embeds is None: + prompt_2 = prompt_2 or prompt + prompt_2 = [prompt_2] if isinstance(prompt_2, str) else prompt_2 + + if len(prompt_2) > 1 and len(prompt_2) != batch_size: + raise ValueError(f"prompt_2 must be of length 1 or {batch_size}") + + pooled_prompt_embeds_2 = self._get_clip_prompt_embeds( + self.tokenizer_2, self.text_encoder_2, prompt_2, max_sequence_length, dtype + ) + + if pooled_prompt_embeds_2.shape[0] == 1 and batch_size > 1: + pooled_prompt_embeds_2 = pooled_prompt_embeds_2.repeat(batch_size, 1) + + if do_classifier_free_guidance and negative_pooled_prompt_embeds is None: + negative_prompt_2 = negative_prompt_2 or negative_prompt + negative_prompt_2 = [negative_prompt_2] if isinstance(negative_prompt_2, str) else negative_prompt_2 + + if len(negative_prompt_2) > 1 and len(negative_prompt_2) != batch_size: + raise ValueError(f"negative_prompt_2 must be of length 1 or {batch_size}") + + negative_pooled_prompt_embeds_2 = self._get_clip_prompt_embeds( + self.tokenizer_2, self.text_encoder_2, negative_prompt_2, max_sequence_length, dtype + ) + + if negative_pooled_prompt_embeds_2.shape[0] == 1 and batch_size > 1: + negative_pooled_prompt_embeds_2 = negative_pooled_prompt_embeds_2.repeat(batch_size, 1) + + if pooled_prompt_embeds is None: + pooled_prompt_embeds = torch.cat([pooled_prompt_embeds_1, pooled_prompt_embeds_2], dim=-1) + + if do_classifier_free_guidance and negative_pooled_prompt_embeds is None: + negative_pooled_prompt_embeds = torch.cat( + [negative_pooled_prompt_embeds_1, negative_pooled_prompt_embeds_2], dim=-1 + ) + + if prompt_embeds_t5 is None: + prompt_3 = prompt_3 or prompt + prompt_3 = [prompt_3] if isinstance(prompt_3, str) else prompt_3 + + if len(prompt_3) > 1 and len(prompt_3) != batch_size: + raise ValueError(f"prompt_3 must be of length 1 or {batch_size}") + + prompt_embeds_t5 = self._get_t5_prompt_embeds(prompt_3, max_sequence_length, dtype) + + if prompt_embeds_t5.shape[0] == 1 and batch_size > 1: + prompt_embeds_t5 = prompt_embeds_t5.repeat(batch_size, 1, 1) + + if do_classifier_free_guidance and negative_prompt_embeds_t5 is None: + negative_prompt_3 = negative_prompt_3 or negative_prompt + negative_prompt_3 = [negative_prompt_3] if isinstance(negative_prompt_3, str) else negative_prompt_3 + + if len(negative_prompt_3) > 1 and len(negative_prompt_3) != batch_size: + raise ValueError(f"negative_prompt_3 must be of length 1 or {batch_size}") + + negative_prompt_embeds_t5 = self._get_t5_prompt_embeds(negative_prompt_3, max_sequence_length, dtype) + + if negative_prompt_embeds_t5.shape[0] == 1 and batch_size > 1: + negative_prompt_embeds_t5 = negative_prompt_embeds_t5.repeat(batch_size, 1, 1) + + if prompt_embeds_llama3 is None: + prompt_4 = prompt_4 or prompt + prompt_4 = [prompt_4] if isinstance(prompt_4, str) else prompt_4 + + if len(prompt_4) > 1 and len(prompt_4) != batch_size: + raise ValueError(f"prompt_4 must be of length 1 or {batch_size}") + + prompt_embeds_llama3 = self._get_llama3_prompt_embeds(prompt_4, max_sequence_length, dtype) + + if prompt_embeds_llama3.shape[0] == 1 and batch_size > 1: + prompt_embeds_llama3 = prompt_embeds_llama3.repeat(1, batch_size, 1, 1) + + if do_classifier_free_guidance and negative_prompt_embeds_llama3 is None: + negative_prompt_4 = negative_prompt_4 or negative_prompt + negative_prompt_4 = [negative_prompt_4] if isinstance(negative_prompt_4, str) else negative_prompt_4 + + if len(negative_prompt_4) > 1 and len(negative_prompt_4) != batch_size: + raise ValueError(f"negative_prompt_4 must be of length 1 or {batch_size}") + + negative_prompt_embeds_llama3 = self._get_llama3_prompt_embeds( + negative_prompt_4, max_sequence_length, dtype + ) + + if negative_prompt_embeds_llama3.shape[0] == 1 and batch_size > 1: + negative_prompt_embeds_llama3 = negative_prompt_embeds_llama3.repeat(1, batch_size, 1, 1) + + # duplicate pooled_prompt_embeds for each generation per prompt + pooled_prompt_embeds = pooled_prompt_embeds.repeat(1, num_images_per_prompt) + pooled_prompt_embeds = pooled_prompt_embeds.view(batch_size * num_images_per_prompt, -1) + + # duplicate t5_prompt_embeds for batch_size and num_images_per_prompt + bs_embed, seq_len, _ = prompt_embeds_t5.shape + if bs_embed == 1 and batch_size > 1: + prompt_embeds_t5 = prompt_embeds_t5.repeat(batch_size, 1, 1) + elif bs_embed > 1 and bs_embed != batch_size: + raise ValueError(f"cannot duplicate prompt_embeds_t5 of batch size {bs_embed}") + prompt_embeds_t5 = prompt_embeds_t5.repeat(1, num_images_per_prompt, 1) + prompt_embeds_t5 = prompt_embeds_t5.view(batch_size * num_images_per_prompt, seq_len, -1) + + # duplicate llama3_prompt_embeds for batch_size and num_images_per_prompt + _, bs_embed, seq_len, dim = prompt_embeds_llama3.shape + if bs_embed == 1 and batch_size > 1: + prompt_embeds_llama3 = prompt_embeds_llama3.repeat(1, batch_size, 1, 1) + elif bs_embed > 1 and bs_embed != batch_size: + raise ValueError(f"cannot duplicate prompt_embeds_llama3 of batch size {bs_embed}") + prompt_embeds_llama3 = prompt_embeds_llama3.repeat(1, 1, num_images_per_prompt, 1) + prompt_embeds_llama3 = prompt_embeds_llama3.view(-1, batch_size * num_images_per_prompt, seq_len, dim) + + if do_classifier_free_guidance: + # duplicate negative_pooled_prompt_embeds for batch_size and num_images_per_prompt + bs_embed, seq_len = negative_pooled_prompt_embeds.shape + if bs_embed == 1 and batch_size > 1: + negative_pooled_prompt_embeds = negative_pooled_prompt_embeds.repeat(batch_size, 1) + elif bs_embed > 1 and bs_embed != batch_size: + raise ValueError(f"cannot duplicate negative_pooled_prompt_embeds of batch size {bs_embed}") + negative_pooled_prompt_embeds = negative_pooled_prompt_embeds.repeat(1, num_images_per_prompt) + negative_pooled_prompt_embeds = negative_pooled_prompt_embeds.view(batch_size * num_images_per_prompt, -1) + + # duplicate negative_t5_prompt_embeds for batch_size and num_images_per_prompt + bs_embed, seq_len, _ = negative_prompt_embeds_t5.shape + if bs_embed == 1 and batch_size > 1: + negative_prompt_embeds_t5 = negative_prompt_embeds_t5.repeat(batch_size, 1, 1) + elif bs_embed > 1 and bs_embed != batch_size: + raise ValueError(f"cannot duplicate negative_prompt_embeds_t5 of batch size {bs_embed}") + negative_prompt_embeds_t5 = negative_prompt_embeds_t5.repeat(1, num_images_per_prompt, 1) + negative_prompt_embeds_t5 = negative_prompt_embeds_t5.view(batch_size * num_images_per_prompt, seq_len, -1) + + # duplicate negative_prompt_embeds_llama3 for batch_size and num_images_per_prompt + _, bs_embed, seq_len, dim = negative_prompt_embeds_llama3.shape + if bs_embed == 1 and batch_size > 1: + negative_prompt_embeds_llama3 = negative_prompt_embeds_llama3.repeat(1, batch_size, 1, 1) + elif bs_embed > 1 and bs_embed != batch_size: + raise ValueError(f"cannot duplicate negative_prompt_embeds_llama3 of batch size {bs_embed}") + negative_prompt_embeds_llama3 = negative_prompt_embeds_llama3.repeat(1, 1, num_images_per_prompt, 1) + negative_prompt_embeds_llama3 = negative_prompt_embeds_llama3.view( + -1, batch_size * num_images_per_prompt, seq_len, dim + ) + + return ( + prompt_embeds_t5, + negative_prompt_embeds_t5, + prompt_embeds_llama3, + negative_prompt_embeds_llama3, + pooled_prompt_embeds, + negative_pooled_prompt_embeds, + ) + + def enable_vae_slicing(self): + r""" + Enable sliced VAE decoding. When this option is enabled, the VAE will split the input tensor in slices to + compute decoding in several steps. This is useful to save some memory and allow larger batch sizes. + """ + depr_message = ( + f"Calling `enable_vae_slicing()` on a `{self.__class__.__name__}` is deprecated and this" + " method will be removed in a future version. Please use `pipe.vae.enable_slicing()`." + ) + deprecate( + "enable_vae_slicing", + "0.40.0", + depr_message, + ) + self.vae.enable_slicing() + + def disable_vae_slicing(self): + r""" + Disable sliced VAE decoding. If `enable_vae_slicing` was previously enabled, this method will go back to + computing decoding in one step. + """ + depr_message = ( + f"Calling `disable_vae_slicing()` on a `{self.__class__.__name__}` is deprecated and this" + " method will be removed in a future version. Please use `pipe.vae.disable_slicing()`." + ) + deprecate( + "disable_vae_slicing", + "0.40.0", + depr_message, + ) + self.vae.disable_slicing() + + def enable_vae_tiling(self): + r""" + Enable tiled VAE decoding. When this option is enabled, the VAE will split the input tensor into tiles to + compute decoding and encoding in several steps. This is useful for saving a large amount of memory and to allow + processing larger images. + """ + depr_message = ( + f"Calling `enable_vae_tiling()` on a `{self.__class__.__name__}`" + " is deprecated and this method will be removed in a future version. Please use `pipe.vae.enable_tiling()`." + ) + deprecate( + "enable_vae_tiling", + "0.40.0", + depr_message, + ) + self.vae.enable_tiling() + + def disable_vae_tiling(self): + r""" + Disable tiled VAE decoding. If `enable_vae_tiling` was previously enabled, this method will go back to + computing decoding in one step. + """ + depr_message = ( + f"Calling `disable_vae_tiling()` on a `{self.__class__.__name__}` is" + " deprecated and this method will be removed in a future version. Please use `pipe.vae.disable_tiling()`." + ) + deprecate( + "disable_vae_tiling", + "0.40.0", + depr_message, + ) + self.vae.disable_tiling() + + def prepare_latents( + self, + batch_size, + num_channels_latents, + height, + width, + dtype, + generator, + latents=None, + ): + # VAE applies 8x compression on images but we must also account for packing which requires + # latent height and width to be divisible by 2. + height = 2 * (int(height) // (self.vae_scale_factor * 2)) + width = 2 * (int(width) // (self.vae_scale_factor * 2)) + + shape = (batch_size, num_channels_latents, height, width) + + if latents is None: + latents = randn_tensor(shape, generator=generator, device=self.device, dtype=dtype) + else: + if latents.shape != shape: + raise ValueError(f"Unexpected latents shape, got {latents.shape}, expected {shape}") + latents = latents.to(self.device) + return latents + + def prepare_timesteps(self, num_inference_steps, sigmas, image_seq_len): + sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas + mu = calculate_shift( + image_seq_len, + self.scheduler.config.get("base_image_seq_len", 256), + self.scheduler.config.get("max_image_seq_len", 4096), + self.scheduler.config.get("base_shift", 0.5), + self.scheduler.config.get("max_shift", 1.15), + ) + timesteps, num_inference_steps = retrieve_timesteps( + self.scheduler, + num_inference_steps, + self.device, + sigmas=sigmas, + mu=mu, + ) + return timesteps, num_inference_steps + + def _extract_prompts(self, prompts): + """Extract prompt and negative_prompt from OmniPromptType list.""" + prompt = [p if isinstance(p, str) else (p.get("prompt") or "") for p in prompts] or None + if all(isinstance(p, str) or p.get("negative_prompt") is None for p in prompts): + negative_prompt = None + elif prompts: + negative_prompt = ["" if isinstance(p, str) else (p.get("negative_prompt") or "") for p in prompts] + else: + negative_prompt = None + return prompt, negative_prompt + + def check_cfg_parallel_validity(self, true_cfg_scale: float, has_neg_prompt: bool): + if get_classifier_free_guidance_world_size() == 1: + return True + + if true_cfg_scale <= 1: + logger.warning("CFG parallel is NOT working correctly when true_cfg_scale <= 1.") + return False + + if not has_neg_prompt: + logger.warning( + "CFG parallel is NOT working correctly when there is no negative prompt or negative prompt embeddings." + ) + return False + return True + + def diffuse( + self, + prompt_embeds_t5: torch.Tensor, + prompt_embeds_llama3: torch.Tensor, + pooled_prompt_embeds: torch.Tensor, + latents: torch.Tensor, + timesteps: torch.Tensor, + do_true_cfg: bool, + ) -> torch.Tensor: + with self.progress_bar(total=len(timesteps)) as pbar: + for i, t in enumerate(timesteps): + if self.interrupt: + continue + + # expand the latents if we are doing classifier free guidance + latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timestep = t.expand(latent_model_input.shape[0]) + noise_pred = self.transformer( + hidden_states=latent_model_input, + timesteps=timestep, + encoder_hidden_states_t5=prompt_embeds_t5, + encoder_hidden_states_llama3=prompt_embeds_llama3, + pooled_embeds=pooled_prompt_embeds, + return_dict=False, + )[0] + noise_pred = -noise_pred + + # TODO: Modify CFG guidance + # perform guidance + if self.do_classifier_free_guidance: + noise_pred_uncond, noise_pred_text = noise_pred.chunk(2) + noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond) + + # compute the previous noisy sample x_t -> x_t-1 + latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg) + + pbar.update() + + return latents + + @property + def guidance_scale(self): + return self._guidance_scale + + @property + def do_classifier_free_guidance(self): + return self._guidance_scale > 1 + + @property + def attention_kwargs(self): + return self._attention_kwargs + + @property + def num_timesteps(self): + return self._num_timesteps + + @property + def interrupt(self): + return self._interrupt + + def forward( + self, + req: OmniDiffusionRequest, + prompt: str | list[str] = None, + prompt_2: str | list[str] | None = None, + prompt_3: str | list[str] | None = None, + prompt_4: str | list[str] | None = None, + height: int | None = None, + width: int | None = None, + num_inference_steps: int = 50, + sigmas: list[float] | None = None, + guidance_scale: float = 5.0, + negative_prompt: str | list[str] | None = None, + negative_prompt_2: str | list[str] | None = None, + negative_prompt_3: str | list[str] | None = None, + negative_prompt_4: str | list[str] | None = None, + num_images_per_prompt: int | None = 1, + generator: torch.Generator | list[torch.Generator] | None = None, + latents: torch.FloatTensor | None = None, + prompt_embeds_t5: torch.FloatTensor | None = None, + prompt_embeds_llama3: torch.FloatTensor | None = None, + negative_prompt_embeds_t5: torch.FloatTensor | None = None, + negative_prompt_embeds_llama3: torch.FloatTensor | None = None, + pooled_prompt_embeds: torch.FloatTensor | None = None, + negative_pooled_prompt_embeds: torch.FloatTensor | None = None, + output_type: str | None = "pil", + return_dict: bool = True, + attention_kwargs: dict[str, Any] | None = None, + callback_on_step_end: Callable[[int, int], None] | None = None, + callback_on_step_end_tensor_inputs: list[str] = ["latents"], + max_sequence_length: int = 128, + **kwargs, + ): + extracted_prompt, negative_prompt = self._extract_prompts(req.prompts) + prompt = extracted_prompt or prompt + + height = req.sampling_params.height or self.default_sample_size * self.vae_scale_factor + width = req.sampling_params.width or self.default_sample_size * self.vae_scale_factor + + num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps + sigmas = req.sampling_params.sigmas or sigmas + max_sequence_length = req.sampling_params.max_sequence_length or max_sequence_length + generator = req.sampling_params.generator or generator + true_cfg_scale = req.sampling_params.true_cfg_scale or guidance_scale + if req.sampling_params.guidance_scale_provided: + guidance_scale = req.sampling_params.guidance_scale + num_images_per_prompt = ( + req.sampling_params.num_outputs_per_prompt + if req.sampling_params.num_outputs_per_prompt > 0 + else num_images_per_prompt + ) + + prompt_embeds = kwargs.get("prompt_embeds", None) + negative_prompt_embeds = kwargs.get("negative_prompt_embeds", None) + + if prompt_embeds is not None: + deprecation_message = ( + "The `prompt_embeds` argument is deprecated." + " Please use `prompt_embeds_t5` and `prompt_embeds_llama3` instead." + ) + deprecate("prompt_embeds", "0.35.0", deprecation_message) + prompt_embeds_t5 = prompt_embeds[0] + prompt_embeds_llama3 = prompt_embeds[1] + + if negative_prompt_embeds is not None: + deprecation_message = ( + "The `negative_prompt_embeds` argument is deprecated." + "Please use `negative_prompt_embeds_t5` and `negative_prompt_embeds_llama3` instead." + ) + deprecate("negative_prompt_embeds", "0.35.0", deprecation_message) + negative_prompt_embeds_t5 = negative_prompt_embeds[0] + negative_prompt_embeds_llama3 = negative_prompt_embeds[1] + + division = self.vae_scale_factor * 2 + S_max = (self.default_sample_size * self.vae_scale_factor) ** 2 + scale = S_max / (width * height) + scale = math.sqrt(scale) + width, height = int(width * scale // division * division), int(height * scale // division * division) + + # 1. Check inputs. Raise error if not correct + self.check_inputs( + prompt, + prompt_2, + prompt_3, + prompt_4, + negative_prompt=negative_prompt, + negative_prompt_2=negative_prompt_2, + negative_prompt_3=negative_prompt_3, + negative_prompt_4=negative_prompt_4, + prompt_embeds_t5=prompt_embeds_t5, + prompt_embeds_llama3=prompt_embeds_llama3, + negative_prompt_embeds_t5=negative_prompt_embeds_t5, + negative_prompt_embeds_llama3=negative_prompt_embeds_llama3, + pooled_prompt_embeds=pooled_prompt_embeds, + negative_pooled_prompt_embeds=negative_pooled_prompt_embeds, + callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs, + ) + + self._guidance_scale = guidance_scale + self._attention_kwargs = attention_kwargs + self._interrupt = False + + # 2. Define call parameters + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + elif pooled_prompt_embeds is not None: + batch_size = pooled_prompt_embeds.shape[0] + + # TODO: CFG guidance configuration + has_neg_prompt = negative_prompt is not None or ( + negative_prompt_embeds is not None and negative_pooled_prompt_embeds is not None + ) + do_true_cfg = true_cfg_scale > 1 and has_neg_prompt + self.check_cfg_parallel_validity(true_cfg_scale, has_neg_prompt) + + # 3. Encode prompt + lora_scale = self.attention_kwargs.get("scale", None) if self.attention_kwargs is not None else None + ( + prompt_embeds_t5, + negative_prompt_embeds_t5, + prompt_embeds_llama3, + negative_prompt_embeds_llama3, + pooled_prompt_embeds, + negative_pooled_prompt_embeds, + ) = self.encode_prompt( + prompt=prompt, + prompt_2=prompt_2, + prompt_3=prompt_3, + prompt_4=prompt_4, + negative_prompt=negative_prompt, + negative_prompt_2=negative_prompt_2, + negative_prompt_3=negative_prompt_3, + negative_prompt_4=negative_prompt_4, + do_classifier_free_guidance=self.do_classifier_free_guidance, + prompt_embeds_t5=prompt_embeds_t5, + prompt_embeds_llama3=prompt_embeds_llama3, + negative_prompt_embeds_t5=negative_prompt_embeds_t5, + negative_prompt_embeds_llama3=negative_prompt_embeds_llama3, + pooled_prompt_embeds=pooled_prompt_embeds, + negative_pooled_prompt_embeds=negative_pooled_prompt_embeds, + num_images_per_prompt=num_images_per_prompt, + max_sequence_length=max_sequence_length, + lora_scale=lora_scale, + ) + + if self.do_classifier_free_guidance: + prompt_embeds_t5 = torch.cat([negative_prompt_embeds_t5, prompt_embeds_t5], dim=0) + prompt_embeds_llama3 = torch.cat([negative_prompt_embeds_llama3, prompt_embeds_llama3], dim=1) + pooled_prompt_embeds = torch.cat([negative_pooled_prompt_embeds, pooled_prompt_embeds], dim=0) + + # 4. Prepare latent variables + num_channels_latents = self.transformer.in_channels + latents = self.prepare_latents( + batch_size * num_images_per_prompt, + num_channels_latents, + height, + width, + pooled_prompt_embeds.dtype, + generator, + latents, + ) + + # 5. Prepare timesteps + timesteps, num_inference_steps = self.prepare_timesteps(num_inference_steps, sigmas, latents.shape[1]) + self._num_timesteps = len(timesteps) + + # 6. Denoising loop + latents = self.diffuse( + prompt_embeds_t5, + prompt_embeds_llama3, + pooled_prompt_embeds, + latents, + timesteps, + do_true_cfg, + ) + + if output_type == "latent": + image = latents + + else: + latents = (latents / self.vae.config.scaling_factor) + self.vae.config.shift_factor + + image = self.vae.decode(latents, return_dict=False)[0] + + return DiffusionOutput( + output=image, stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None + ) + + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: + loader = AutoWeightsLoader(self) + return loader.load_weights(weights) diff --git a/vllm_omni/diffusion/registry.py b/vllm_omni/diffusion/registry.py index b6a5a3700..d8302c115 100644 --- a/vllm_omni/diffusion/registry.py +++ b/vllm_omni/diffusion/registry.py @@ -261,6 +261,11 @@ _DIFFUSION_MODELS = { "pipeline_diffusers_adapter", "DiffusersAdapterPipeline", ), + "HiDreamImagePipeline": ( + "hidream_image", + "pipeline_hidream_image", + "HiDreamImagePipeline", + ), } @@ -482,6 +487,7 @@ _DIFFUSION_POST_PROCESS_FUNCS = { "OmniVoicePipeline": "get_omnivoice_post_process_func", "DreamIDOmniPipeline": "get_dreamid_omni_post_process_func", "SenseNovaU1Pipeline": "get_sensenova_u1_post_process_func", + "HiDreamImagePipeline": "get_hidream_image_post_process_func", } _DIFFUSION_PRE_PROCESS_FUNCS = { diff --git a/vllm_omni/engine/async_omni_engine.py b/vllm_omni/engine/async_omni_engine.py index ad8dbff6f..23ef9b855 100644 --- a/vllm_omni/engine/async_omni_engine.py +++ b/vllm_omni/engine/async_omni_engine.py @@ -1950,6 +1950,10 @@ class AsyncOmniEngine: "force_cutlass_fp8": bool(kwargs.get("force_cutlass_fp8", False)), "enable_diffusion_pipeline_profiler": kwargs.get("enable_diffusion_pipeline_profiler", False), "enable_ar_profiler": kwargs.get("enable_ar_profiler", False), + "extras": { + "auxiliary_text_encoder": kwargs.get("auxiliary_text_encoder", None), + "default_llama_model_id": kwargs.get("default_llama_model_id", "meta-llama/Meta-Llama-3.1-8B-Instruct"), + }, **( { "profiler_config": asdict(kwargs["profiler_config"]) diff --git a/vllm_omni/entrypoints/cli/serve.py b/vllm_omni/entrypoints/cli/serve.py index e4fddc830..cc6e9a4da 100644 --- a/vllm_omni/entrypoints/cli/serve.py +++ b/vllm_omni/entrypoints/cli/serve.py @@ -650,6 +650,16 @@ class OmniServeCommand(CLISubcommand): action="store_true", help="Enable AR stage profiler to include AR stage timing in stage_durations.", ) + + # Supplementary auxiliary text encoder parameters + # (e.g., the meta llama/meta llama-3.1-8b-instrument used by hidream) + omni_config_group.add_argument( + "--auxiliary-text-encoder", + type=str, + default=None, + help="Auxiliary text encoder parameters model name or path (especially for Hidream-l1-full).", + ) + # Stash via type(self) so the docs hook (which execs this function in a # sandboxed globals dict via ``DummySelf``) doesn't fail on a NameError. type(self)._parser = serve_parser From 90ed618b5abb4fd68937402967c42bfa91d8e15d Mon Sep 17 00:00:00 2001 From: skf <54565339+skf-1999@users.noreply.github.com> Date: Fri, 22 May 2026 16:08:04 +0800 Subject: [PATCH 008/112] [Bugfix] Align Offline and Online Inference (#3506) --- vllm_omni/entrypoints/openai/api_server.py | 15 ++++++++++-- .../entrypoints/openai/protocol/images.py | 10 ++++++++ vllm_omni/entrypoints/openai/serving_chat.py | 24 +++++++++++++++---- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/vllm_omni/entrypoints/openai/api_server.py b/vllm_omni/entrypoints/openai/api_server.py index f00e6448a..29788d958 100644 --- a/vllm_omni/entrypoints/openai/api_server.py +++ b/vllm_omni/entrypoints/openai/api_server.py @@ -1535,6 +1535,12 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request) # Keep /images validation semantics: invalid LoRA should fail with 400. _parse_lora_request(request.lora) extra_body["lora"] = request.lora + if request.bot_task is not None: + extra_body["bot_task"] = request.bot_task + if request.use_system_prompt is not None: + extra_body["use_system_prompt"] = request.use_system_prompt + if request.system_prompt is not None: + extra_body["system_prompt"] = request.system_prompt generation_result = await chat_handler.generate_diffusion_images( prompt=request.prompt, @@ -1546,8 +1552,9 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request) status_code=generation_result.error.code if generation_result.error else 400, content=generation_result.model_dump(), ) - flat_images, _, _ = generation_result + flat_images, _, _, _ = generation_result image_data = [ImageData(b64_json=encode_image_base64(img), revised_prompt=None) for img in flat_images] + return ImageGenerationResponse(created=int(time.time()), data=image_data) # Build params - pass through user values directly @@ -1560,6 +1567,8 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request) extra_args["use_system_prompt"] = request.use_system_prompt if request.system_prompt is not None: extra_args["system_prompt"] = request.system_prompt + if request.bot_task is not None: + extra_args["bot_task"] = request.bot_task if extra_args: gen_params.extra_args = extra_args # Parse per-request LoRA (compatible with chat's extra_body.lora shape). @@ -1727,6 +1736,7 @@ async def edit_images( ) try: # 2. Build prompt & images params + cot_output = None prompt: OmniTextPrompt = {"prompt": prompt} if negative_prompt is not None: prompt["negative_prompt"] = negative_prompt @@ -1937,7 +1947,7 @@ async def edit_images( status_code=generation_result.error.code if generation_result.error else 400, detail=generation_result.message, ) - images, _, _ = generation_result + images, _, _, cot_output = generation_result else: # Single-stage diffusion: use the direct path. result = await _generate_with_async_omni( @@ -1967,6 +1977,7 @@ async def edit_images( data=image_data, output_format=output_format, size=size_str, + cot_output=cot_output, ) except (EngineGenerateError, EngineDeadError) as exc: diff --git a/vllm_omni/entrypoints/openai/protocol/images.py b/vllm_omni/entrypoints/openai/protocol/images.py index 0fb22a548..dbf4c24b3 100644 --- a/vllm_omni/entrypoints/openai/protocol/images.py +++ b/vllm_omni/entrypoints/openai/protocol/images.py @@ -32,6 +32,11 @@ class ImageGenerationRequest(BaseModel): # Required fields prompt: str = Field(..., description="Text description of the desired image(s)") + bot_task: str | None = Field( + None, + description="Task mode for the model (e.g., 'cot' enables chain-of-thought generation). " + "Only supported by specific diffusion models.", + ) # OpenAI standard fields model: str | None = Field( @@ -165,3 +170,8 @@ class ImageGenerationResponse(BaseModel): data: list[ImageData] = Field(..., description="Array of generated images") output_format: str = Field(None, description="The output format of the image generation") size: str = Field(None, description="The size of the image generated") + cot_output: str | None = Field( + None, + description="Chain-of-thought text output from the AR stage. " + "Only present for image editing (IT2I) with CoT-enabled models.", + ) diff --git a/vllm_omni/entrypoints/openai/serving_chat.py b/vllm_omni/entrypoints/openai/serving_chat.py index 66a86e7c9..d3bf29d89 100644 --- a/vllm_omni/entrypoints/openai/serving_chat.py +++ b/vllm_omni/entrypoints/openai/serving_chat.py @@ -2275,7 +2275,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): layers = extra_body.get("layers") resolution = extra_body.get("resolution") bot_task = extra_body.get("bot_task") - sys_type = extra_body.get("sys_type") + use_system_prompt = extra_body.get("use_system_prompt") or extra_body.get("sys_type") custom_system_prompt = extra_body.get("system_prompt") engine_prompt_data: dict[str, Any] | None = None @@ -2289,7 +2289,9 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): prompt_token_ids: list[int] | None = None system_prompt_type: str | None = None - if bot_task is not None or sys_type is not None or custom_system_prompt is not None: + build_kwargs: dict[str, Any] = {} + + if bot_task is not None or use_system_prompt is not None or custom_system_prompt is not None: from vllm_omni.diffusion.models.hunyuan_image3.prompt_utils import ( build_prompt, build_prompt_tokens, @@ -2297,16 +2299,17 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): build_kwargs: dict[str, Any] = { "task": "it2i" if reference_images else "t2i", - "sys_type": sys_type, + "sys_type": use_system_prompt, "custom_system_prompt": custom_system_prompt, "num_images": len(reference_images) if reference_images else 1, } + if bot_task is not None: build_kwargs["bot_task"] = bot_task elif "bot_task" in extra_body: # Explicit None from the caller is plain-mode; omitted lets # each task fall back to its default trigger. - build_kwargs["bot_task"] = None + build_kwargs["bot_task"] = extra_body["bot_task"] if tokenizer is not None: # Feed segment-tokenized prompt_token_ids so AR matches HF # apply_chat_template byte-for-byte (engine BPE would merge @@ -2542,6 +2545,17 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): images = getattr(result.request_output, "images", []) stage_durations = result.stage_durations peak_memory_mb = result.peak_memory_mb + cot_output = None + + req_out = getattr(result, "request_output", None) + if req_out: + prompt_obj = getattr(req_out, "prompt", None) + if isinstance(prompt_obj, dict): + extra = prompt_obj.get("extra", {}) + if isinstance(extra, dict): + ar_text = extra.get("ar_generated_text") + if isinstance(ar_text, str) and ar_text.strip(): + cot_output = ar_text flat_images: list[Image.Image] = [] for item in images: @@ -2550,7 +2564,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): else: flat_images.append(item) - return flat_images, stage_durations, peak_memory_mb + return flat_images, stage_durations, peak_memory_mb, cot_output async def _create_diffusion_chat_completion( self, From 89e389cf37aa72c656386bb972fbcd585edb058e Mon Sep 17 00:00:00 2001 From: Alicia <115451386+congw729@users.noreply.github.com> Date: Fri, 22 May 2026 17:29:45 +0800 Subject: [PATCH 009/112] [CI] Fix email bug & skip email distribution. (#3814) Signed-off-by: Alicia <115451386+congw729@users.noreply.github.com> --- .buildkite/test-nightly.yml | 2 +- tools/nightly/generate_nightly_perf_excel.py | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.buildkite/test-nightly.yml b/.buildkite/test-nightly.yml index ae93d2353..82e872581 100644 --- a/.buildkite/test-nightly.yml +++ b/.buildkite/test-nightly.yml @@ -1196,7 +1196,7 @@ steps: - nightly-diffusion-x2iat-performance-qwen-image-layered - nightly-diffusion-x2v-performance - nightly-testcase-statistics - if: build.env("NIGHTLY") == "1" + if: build.env("NIGHTLY") == "1" && build.env("EMAIL_DISTRIBUTION") == "1" commands: - pip install openpyxl - export DEFAULT_INPUT_DIR=tests/dfx/perf/results diff --git a/tools/nightly/generate_nightly_perf_excel.py b/tools/nightly/generate_nightly_perf_excel.py index e76dc4346..54c4b3f34 100644 --- a/tools/nightly/generate_nightly_perf_excel.py +++ b/tools/nightly/generate_nightly_perf_excel.py @@ -18,11 +18,13 @@ from openpyxl import Workbook from openpyxl.styles import PatternFill from openpyxl.utils import get_column_letter -from benchmarks.diffusion.backends import normalize_endpoint - LOGGER = logging.getLogger(__name__) GREY_BLOCK_FILL = PatternFill(start_color="D3D3D3", fill_type="solid") +LEGACY_BACKEND_ENDPOINT_ALIASES = { + "vllm-omni": "/v1/chat/completions", + "openai": "/v1/images/generations", +} # Diffusion sheet columns (Qwen-Image diffusion benchmark). # Per-stage latency metrics. Unpack from stage_durations_mean/p50/p99 dicts @@ -145,6 +147,20 @@ def _diffusion_group_key(record: dict[str, Any]) -> tuple[Any, ...]: return (record.get("test_name") or "",) +def normalize_endpoint(value: str) -> str: + """Normalize legacy backend aliases and endpoint paths for report rows.""" + endpoint = str(value).strip() + if not endpoint: + raise ValueError("endpoint must not be empty.") + endpoint = LEGACY_BACKEND_ENDPOINT_ALIASES.get( + endpoint, + LEGACY_BACKEND_ENDPOINT_ALIASES.get(endpoint.lstrip("/"), endpoint), + ) + if not endpoint.startswith("/"): + endpoint = f"/{endpoint}" + return endpoint + + def _load_summary_columns(script_dir: str) -> list[str]: """Load summary column names from a file next to this script; fallback to default if missing.""" path = os.path.join(script_dir, _COLUMNS_FILENAME) From 5799d85a51679cca2bacf2f28ea47121d01b2f41 Mon Sep 17 00:00:00 2001 From: Jialong Liu <88185941+Galleons2029@users.noreply.github.com> Date: Fri, 22 May 2026 19:25:37 +0800 Subject: [PATCH 010/112] [Bugfix] Revert MiMo-Audio local_sampler to greedy to fix text truncation under concurrent batching (followup to #3686) (#3817) Signed-off-by: Jialong Liu <88185941+Galleons2029@users.noreply.github.com> Signed-off-by: Galleons2029 Co-authored-by: Hongsheng Liu --- .../models/mimo_audio/mimo_audio_llm.py | 22 +++++++++++-------- .../stage_input_processors/mimo_audio.py | 7 +++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/vllm_omni/model_executor/models/mimo_audio/mimo_audio_llm.py b/vllm_omni/model_executor/models/mimo_audio/mimo_audio_llm.py index fed27c2c5..a640ade3a 100644 --- a/vllm_omni/model_executor/models/mimo_audio/mimo_audio_llm.py +++ b/vllm_omni/model_executor/models/mimo_audio/mimo_audio_llm.py @@ -549,13 +549,17 @@ class MiMoAudioLLMForConditionalGeneration(nn.Module, SupportsMultiModal, Suppor # do_sample=True here without also routing vLLM's sampled token back into # this gate check would cause the two to diverge and corrupt KV-cache state. self.global_sampler = MiMoSampler(do_sample=False, temperature=0.6, top_p=0.95) - # local_sampler drives audio-code generation inside local_forward. It is - # entirely internal and NOT subject to vLLM's SamplingParams, so stochastic - # sampling is safe here and is required to produce natural, varied speech. - # Setting do_sample=True also disables the CUDA-graph path (use_cg gate in - # local_forward checks `do_sample is False`), preventing MiMoLocalSamplerTensor - # from silently forcing argmax even when temperature > 0. - self.local_sampler = MiMoSampler(do_sample=True, temperature=0.9, top_p=0.95) + # local_sampler drives audio-code generation inside local_forward. Keep + # it greedy (do_sample=False) so the CUDA-graph path (use_cg gate in + # local_forward) stays active AND so the audio codes — and therefore the + # `new_audio_emb` written into `_cached_new_audio_emb_by_req` — are + # deterministic. That cache is fed back into `inputs_embeds` on the next + # decode step (see `_prepare_multimodal_embeddings_with_cache`), so any + # stochasticity here propagates into subsequent *text* logits via the + # audio-embedding feedback path and destabilises text continuations even + # though `global_sampler` is greedy. Voice diversity must be tackled in + # the codec/vocoder path (stage-1), not by randomising local_sampler. + self.local_sampler = MiMoSampler(do_sample=False, temperature=0.9, top_p=0.95) self.removed_tokens = None self.speech_vocab_sizes = config.parsed_speech_vocab_sizes() @@ -818,7 +822,7 @@ class MiMoAudioLLMForConditionalGeneration(nn.Module, SupportsMultiModal, Suppor device=tokens_device, ) if local_sampler is None: - local_sampler = MiMoSampler(do_sample=True, temperature=0.9, top_p=0.95) + local_sampler = MiMoSampler(do_sample=False, temperature=0.9, top_p=0.95) past_key_values = DynamicCache() for t in range(delay_iters): @@ -864,7 +868,7 @@ class MiMoAudioLLMForConditionalGeneration(nn.Module, SupportsMultiModal, Suppor local_sampler: MiMoSampler | None = None, ): if local_sampler is None: - local_sampler = MiMoSampler(do_sample=True, temperature=0.9, top_p=0.95) + local_sampler = MiMoSampler(do_sample=False, temperature=0.9, top_p=0.95) b = int(local_embeds.shape[0]) use_cg = (local_sampler.do_sample is None or local_sampler.do_sample is False) and bool( diff --git a/vllm_omni/model_executor/stage_input_processors/mimo_audio.py b/vllm_omni/model_executor/stage_input_processors/mimo_audio.py index 55aa003f6..b0b6e8878 100644 --- a/vllm_omni/model_executor/stage_input_processors/mimo_audio.py +++ b/vllm_omni/model_executor/stage_input_processors/mimo_audio.py @@ -173,7 +173,12 @@ def llm2code2wav_async_chunk( request_id = getattr(request, "external_req_id", None) - po_codes = pooling_output.get("codes", {}) + # Text-only paths (e.g. modalities=["text"]) yield no codec pooling output; + # stage-0 still drives the chunk transfer adapter, so treat None as "no codes + # this step" rather than letting `.get()` raise AttributeError — an unhandled + # error here drops the chunk, starves stage-1 of the finished payload, and + # the stage subprocesses die before the final token is emitted. + po_codes = pooling_output.get("codes", {}) if pooling_output is not None else {} if "audio" not in po_codes: if is_finished: return _flush_remaining_codes(transfer_manager, request_id, chunk_size, left_context_size) From 23e243378f907ebf0ce0e52a694202d36219b9a1 Mon Sep 17 00:00:00 2001 From: Alex Brooks Date: Fri, 22 May 2026 05:56:41 -0600 Subject: [PATCH 011/112] [Bugfix] Set separate CFG flag in Helios for CacheDiT (#3756) Signed-off-by: Alex Brooks --- tests/diffusion/cache/test_cache_dit.py | 1 + vllm_omni/diffusion/cache/cache_dit_backend.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/diffusion/cache/test_cache_dit.py b/tests/diffusion/cache/test_cache_dit.py index 0b7ef7235..570c4f238 100644 --- a/tests/diffusion/cache/test_cache_dit.py +++ b/tests/diffusion/cache/test_cache_dit.py @@ -16,6 +16,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu] SEPARATE_CFG_ENABLERS = [ cd_backend.enable_cache_for_ltx2, + cd_backend.enable_cache_for_helios, cd_backend.enable_cache_for_wan22, cd_backend.enable_cache_for_longcat_image, ] diff --git a/vllm_omni/diffusion/cache/cache_dit_backend.py b/vllm_omni/diffusion/cache/cache_dit_backend.py index daf9b3939..3f8229722 100644 --- a/vllm_omni/diffusion/cache/cache_dit_backend.py +++ b/vllm_omni/diffusion/cache/cache_dit_backend.py @@ -1413,7 +1413,7 @@ def enable_cache_for_helios(pipeline: Any, cache_config: Any) -> Callable[[int], params_modifiers=[ ParamsModifier(cache_config=db_cache_config, calibrator_config=calibrator_config), ], - has_separate_cfg=False, + has_separate_cfg=True, ), cache_config=db_cache_config, calibrator_config=calibrator_config, From d714b4ce0496928ac2cbd51ca0b4ecb6df8dbee2 Mon Sep 17 00:00:00 2001 From: Yueqian Lin <70319226+linyueqian@users.noreply.github.com> Date: Fri, 22 May 2026 08:14:03 -0400 Subject: [PATCH 012/112] [Recipe] Add Fish Speech S2 Pro 2-GPU deploy profile (#3323) Signed-off-by: Yueqian Lin --- recipes/fishaudio/Fish-Speech-S2-Pro.md | 27 ++++++++ vllm_omni/deploy/fish_qwen3_omni_2gpu.yaml | 72 ++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 vllm_omni/deploy/fish_qwen3_omni_2gpu.yaml diff --git a/recipes/fishaudio/Fish-Speech-S2-Pro.md b/recipes/fishaudio/Fish-Speech-S2-Pro.md index 91ac3fe03..2a88bd169 100644 --- a/recipes/fishaudio/Fish-Speech-S2-Pro.md +++ b/recipes/fishaudio/Fish-Speech-S2-Pro.md @@ -102,3 +102,30 @@ curl -X POST http://localhost:8091/v1/audio/speech \ both `ref_audio` and `ref_text` parameters. - Memory usage: Model loads at ~48.3 GiB, peaks at ~48.9 GiB during inference headroom for video frames and audio caches. + +### 2x H100 80GB + +#### Environment + +- OS: Linux +- Python: 3.10+ +- Required package: `fish-speech` (for DAC codec) +- CUDA 12.8 +- vLLM version: 0.19.0+ + +#### Command + +```bash +CUDA_VISIBLE_DEVICES=0,1 vllm serve fishaudio/s2-pro --omni --port 8091 \ + --deploy-config vllm_omni/deploy/fish_qwen3_omni_2gpu.yaml +``` + +This profile pins Stage0 (Slow/Fast AR) to GPU 0 and Stage1 (DAC decoder) +to GPU 1 to remove AR/DAC contention, sets `max_num_seqs=64` on both +stages so concurrencies above 8 don't queue, and reduces +`connector_get_sleep_s` from 10 ms to 1 ms so DAC chunks aren't held in +the connector once they're ready. + +For benchmark numbers see the discussion in +vllm-project/vllm-omni#2515 (initial H100x2 sweep by @Sy0307) and +vllm-project/vllm-omni#3323 (H20x2 sweep with the shipped config). diff --git a/vllm_omni/deploy/fish_qwen3_omni_2gpu.yaml b/vllm_omni/deploy/fish_qwen3_omni_2gpu.yaml new file mode 100644 index 000000000..9fec34d69 --- /dev/null +++ b/vllm_omni/deploy/fish_qwen3_omni_2gpu.yaml @@ -0,0 +1,72 @@ +# Fish Speech S2 Pro 2-GPU deploy: Stage0 AR on GPU 0, Stage1 DAC on GPU 1. +# +# Opt-in profile validated on H100x2. Compared with the single-GPU default +# (``fish_qwen3_omni.yaml``), it splits AR and DAC across devices to remove +# AR/DAC contention, enlarges Stage0 AR batching, and lets Stage1 consume +# multiple DAC chunks in parallel. +# +# Select via: +# CUDA_VISIBLE_DEVICES=0,1 vllm serve fishaudio/s2-pro --omni \ +# --deploy-config vllm_omni/deploy/fish_qwen3_omni_2gpu.yaml +# +# Or programmatically: +# Omni(model="fishaudio/s2-pro", +# deploy_config_path="vllm_omni/deploy/fish_qwen3_omni_2gpu.yaml") + +async_chunk: true +enable_chunked_prefill: false + +connectors: + connector_of_shared_memory: + name: SharedMemoryConnector + extra: + shm_threshold_bytes: 65536 + codec_streaming: true + # Tightened from 10ms to 1ms: chunks become available sooner under + # cross-GPU pipelining, so connector idle waiting dominates if left high. + connector_get_sleep_s: 0.001 + connector_get_max_wait_first_chunk: 3000 + connector_get_max_wait: 300 + # ~21 Hz codec; 25 frames ≈ 1.16s of audio. + codec_chunk_frames: 25 + codec_left_context_frames: 25 + initial_codec_chunk_frames: 4 + +stages: + - stage_id: 0 + max_num_seqs: 64 + gpu_memory_utilization: 0.75 + enforce_eager: false + async_scheduling: false + # vLLM scheduler requires max_num_batched_tokens >= max_model_len when + # chunked prefill is off (SlowAR decode loop isn't chunked-prefill-safe). + max_num_batched_tokens: 16384 + max_model_len: 16384 + devices: "0" + output_connectors: + to_stage_1: connector_of_shared_memory + default_sampling_params: + temperature: 0.8 + top_k: 30 + top_p: 0.9 + max_tokens: 2048 + seed: 42 + repetition_penalty: 1.0 + + - stage_id: 1 + max_num_seqs: 64 + gpu_memory_utilization: 0.2 + enforce_eager: true + async_scheduling: false + max_num_batched_tokens: 16384 + max_model_len: 16384 + devices: "1" + input_connectors: + from_stage_0: connector_of_shared_memory + default_sampling_params: + temperature: 0.0 + top_p: 1.0 + top_k: -1 + max_tokens: 65536 + seed: 42 + repetition_penalty: 1.0 From e7644daa7f45610665f33d85682ba24014186698 Mon Sep 17 00:00:00 2001 From: univa-HARRY Date: Fri, 22 May 2026 22:12:47 +0900 Subject: [PATCH 013/112] [Perf] [OmniVoice] Triton kernel fusion + CUDA Graph acceleration (#3336) Signed-off-by: univa-HARRY Signed-off-by: Harry Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com> --- .../models/omnivoice/__init__.py | 0 .../omnivoice/test_cuda_graph_generator.py | 263 +++++++++++ .../models/omnivoice/test_decoder.py | 214 +++++++++ .../models/omnivoice/test_triton_kernels.py | 200 ++++++++ .../models/omnivoice/omnivoice_decoder.py | 10 +- .../models/omnivoice/omnivoice_generator.py | 441 +++++++++++++++++- .../transformers_utils/configs/omnivoice.py | 10 + 7 files changed, 1129 insertions(+), 9 deletions(-) create mode 100644 tests/model_executor/models/omnivoice/__init__.py create mode 100644 tests/model_executor/models/omnivoice/test_cuda_graph_generator.py create mode 100644 tests/model_executor/models/omnivoice/test_decoder.py create mode 100644 tests/model_executor/models/omnivoice/test_triton_kernels.py diff --git a/tests/model_executor/models/omnivoice/__init__.py b/tests/model_executor/models/omnivoice/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/model_executor/models/omnivoice/test_cuda_graph_generator.py b/tests/model_executor/models/omnivoice/test_cuda_graph_generator.py new file mode 100644 index 000000000..dc6126713 --- /dev/null +++ b/tests/model_executor/models/omnivoice/test_cuda_graph_generator.py @@ -0,0 +1,263 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +""" +Tests for OmniVoice CUDA Graph generator wrapper numerical equivalence. + +Verifies that _OmniVoiceCUDAGraphForward produces results equivalent to +eager mode across three scenarios: + - Exact-size inputs (no padding) → bit-identical + - Padded inputs (padded to nearest bucket) → correct slicing, exact match + for position-independent models + - Oversized inputs (fallback to eager) → bit-identical + +Uses a lightweight synthetic generator to keep warmup fast and avoid +loading actual model weights. +""" + +from __future__ import annotations + +import pytest +import torch +import torch.nn as nn + +pytestmark = [ + pytest.mark.core_model, + pytest.mark.cuda, + pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"), +] + +DEVICE = torch.device("cuda:0") +NUM_CB = 8 +VOCAB = 1025 +HIDDEN = 64 +CAPTURE_SIZES = [32, 64, 128] + + +# --------------------------------------------------------------------------- +# Synthetic generator: matches _OmniVoiceCUDAGraphForward's interface +# --------------------------------------------------------------------------- + + +class _SyntheticGenerator: + """Position-independent Embedding+Linear replacing OmniVoiceGenerator's 28-layer transformer. + + Each token position is processed independently (no attention), so padded + runs produce bit-identical results to non-padded runs on the same positions. + This makes the padding/slicing logic easy to verify exactly. + """ + + class _Cfg: + num_audio_codebook = NUM_CB + + def __init__(self, device: torch.device): + self.config = self._Cfg() + self.text_embedding = nn.Embedding(1000, HIDDEN).to(device).eval() + self._linear = nn.Linear(HIDDEN, NUM_CB * VOCAB, bias=False).to(device).eval() + self._rope_cos: torch.Tensor | None = None + self._rope_sin: torch.Tensor | None = None + + def _ensure_rope(self, seq_len: int, device: torch.device) -> None: + if self._rope_cos is None or self._rope_cos.shape[0] < seq_len: + max_len = max(seq_len, 512) + self._rope_cos = torch.zeros(max_len, 64, device=device) + self._rope_sin = torch.zeros(max_len, 64, device=device) + + def _step_forward( + self, + input_ids: torch.Tensor, + audio_mask: torch.Tensor, + attention_mask: torch.Tensor | None, + cos: torch.Tensor, + sin: torch.Tensor, + ) -> torch.Tensor: + two_b, _, S = input_ids.shape + x = self.text_embedding(input_ids[:, 0, :].clamp(0, 999)) # [two_b, S, H] + logits = self._linear(x) # [two_b, S, 8*1025] + return logits.view(two_b, S, NUM_CB, VOCAB).permute(0, 2, 1, 3) # [two_b, 8, S, 1025] + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _eager(gen: _SyntheticGenerator, ids: torch.Tensor, mask: torch.Tensor, attn: torch.Tensor) -> torch.Tensor: + """Run _step_forward with zero RoPE tensors (synthetic gen ignores them).""" + S = ids.shape[-1] + cos = torch.zeros(S, 64, device=ids.device) + sin = torch.zeros(S, 64, device=ids.device) + return gen._step_forward(ids, mask, attn, cos, sin) + + +def _make_inputs(seq_len: int, device: torch.device = DEVICE): + two_b = 2 + ids = torch.randint(0, 100, (two_b, NUM_CB, seq_len), dtype=torch.long, device=device) + mask = torch.ones(two_b, seq_len, dtype=torch.bool, device=device) + attn = torch.ones(two_b, 1, seq_len, seq_len, dtype=torch.bool, device=device) + return ids, mask, attn + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def gen(): + torch.manual_seed(42) + return _SyntheticGenerator(DEVICE) + + +@pytest.fixture(scope="module") +def wrapper(gen): + from vllm_omni.model_executor.models.omnivoice.omnivoice_generator import ( + _OmniVoiceCUDAGraphForward, + ) + + w = _OmniVoiceCUDAGraphForward(gen, capture_sizes=CAPTURE_SIZES) + w.warmup(DEVICE) + return w + + +# --------------------------------------------------------------------------- +# 1. Exact-size inputs → bit-identical +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("seq_len", CAPTURE_SIZES) +def test_exact_size_bit_identical(gen, wrapper, seq_len): + """When input exactly matches a captured bucket, output must be bit-identical to eager.""" + ids, mask, attn = _make_inputs(seq_len) + with torch.no_grad(): + eager_out = _eager(gen, ids, mask, attn) + graph_out = wrapper(ids, mask, attn) + torch.testing.assert_close(graph_out, eager_out, atol=0, rtol=0) + + +# --------------------------------------------------------------------------- +# 2. Padded inputs → correct shape, values match at actual positions +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("seq_len", [1, 15, 33, 60, 100]) +def test_padded_output_shape(gen, wrapper, seq_len): + """Graph output must be sliced back to actual seq_len, not the bucket size.""" + ids, mask, attn = _make_inputs(seq_len) + with torch.no_grad(): + graph_out = wrapper(ids, mask, attn) + assert graph_out.shape == (2, NUM_CB, seq_len, VOCAB) + + +@pytest.mark.parametrize("seq_len", [15, 33, 60, 100]) +def test_padded_output_matches_eager(gen, wrapper, seq_len): + """Padded graph output must equal eager output at actual positions. + + The synthetic model has no attention across positions, so zero-padding + does not affect non-padded positions — exact match is expected. + """ + ids, mask, attn = _make_inputs(seq_len) + with torch.no_grad(): + eager_out = _eager(gen, ids, mask, attn) + graph_out = wrapper(ids, mask, attn) + torch.testing.assert_close(graph_out, eager_out, atol=0, rtol=0) + + +# --------------------------------------------------------------------------- +# 3. Oversized inputs → fallback to eager (lazy capture), bit-identical +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("seq_len", [129, 200, 256]) +def test_fallback_eager_bit_identical(gen, wrapper, seq_len): + """Sequences exceeding the largest bucket fall back to lazy capture → bit-identical.""" + ids, mask, attn = _make_inputs(seq_len) + with torch.no_grad(): + eager_out = _eager(gen, ids, mask, attn) + graph_out = wrapper(ids, mask, attn) + torch.testing.assert_close(graph_out, eager_out, atol=0, rtol=0) + + +# --------------------------------------------------------------------------- +# 4. Determinism +# --------------------------------------------------------------------------- + + +def test_deterministic_across_calls(wrapper): + """Same input must produce identical output on repeated CUDA graph replays.""" + ids, mask, attn = _make_inputs(32) + with torch.no_grad(): + out1 = wrapper(ids, mask, attn) + out2 = wrapper(ids, mask, attn) + torch.testing.assert_close(out1, out2, atol=0, rtol=0) + + +# --------------------------------------------------------------------------- +# 5. _find_bucket logic (CPU, no CUDA graph) +# --------------------------------------------------------------------------- + + +def test_find_bucket_returns_nearest_bucket(): + """_find_bucket must return the smallest bucket >= seq_len, or None if all are smaller.""" + from vllm_omni.model_executor.models.omnivoice.omnivoice_generator import ( + _OmniVoiceCUDAGraphForward, + ) + + w = _OmniVoiceCUDAGraphForward.__new__(_OmniVoiceCUDAGraphForward) + w._capture_sizes = [32, 64, 128] + w._graphs = {} + + assert w._find_bucket(1) == 32 + assert w._find_bucket(32) == 32 + assert w._find_bucket(33) == 64 + assert w._find_bucket(64) == 64 + assert w._find_bucket(128) == 128 + assert w._find_bucket(129) is None + assert w._find_bucket(1000) is None + + +# --------------------------------------------------------------------------- +# 6. enable_cuda_graph=False produces same tokens as enable_cuda_graph=True +# --------------------------------------------------------------------------- + + +def test_cuda_graph_disabled_matches_eager_generator(): + """OmniVoiceGenerator with enable_cuda_graph=False must produce the same + _step_forward output as one with enable_cuda_graph=True (after warmup). + + Uses a 2-layer config so warmup completes quickly without loading weights. + """ + from vllm_omni.model_executor.models.omnivoice.omnivoice_generator import OmniVoiceGenerator + from vllm_omni.transformers_utils.configs.omnivoice import OmniVoiceConfig + + cfg = OmniVoiceConfig() + cfg.llm_num_hidden_layers = 2 + cfg.num_hidden_layers = 2 + + torch.manual_seed(0) + gen_eager = OmniVoiceGenerator(cfg).to(DEVICE).eval() + gen_eager.config.enable_cuda_graph = False + gen_eager._cuda_graph_fwd = None + + torch.manual_seed(0) + gen_graph = OmniVoiceGenerator(cfg).to(DEVICE).eval() + gen_graph.config.cuda_graph_capture_sizes = [64] + from vllm_omni.model_executor.models.omnivoice.omnivoice_generator import _OmniVoiceCUDAGraphForward + + gen_graph._cuda_graph_fwd = _OmniVoiceCUDAGraphForward(gen_graph, capture_sizes=[64]) + gen_graph._cuda_graph_fwd.warmup(DEVICE) + + seq_len = 64 + ids = torch.zeros(2, cfg.num_audio_codebook, seq_len, dtype=torch.long, device=DEVICE) + mask = torch.ones(2, seq_len, dtype=torch.bool, device=DEVICE) + attn = torch.ones(2, 1, seq_len, seq_len, dtype=torch.bool, device=DEVICE) + + gen_eager._ensure_rope(seq_len, DEVICE) + dtype = gen_eager.text_embedding.weight.dtype + cos = gen_eager._rope_cos[:seq_len].to(device=DEVICE, dtype=dtype) + sin = gen_eager._rope_sin[:seq_len].to(device=DEVICE, dtype=dtype) + + with torch.no_grad(): + eager_logits = gen_eager._step_forward(ids, mask, attn, cos, sin) + graph_logits = gen_graph._cuda_graph_fwd(ids, mask, attn) + + torch.testing.assert_close(graph_logits, eager_logits, atol=0, rtol=0) diff --git a/tests/model_executor/models/omnivoice/test_decoder.py b/tests/model_executor/models/omnivoice/test_decoder.py new file mode 100644 index 000000000..db8e7328e --- /dev/null +++ b/tests/model_executor/models/omnivoice/test_decoder.py @@ -0,0 +1,214 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +""" +Correctness and regression tests for OmniVoiceDecoder. + +Covers three areas: +1. NaN regression — verifies the fp16 overflow fix: + acoustic decoder must operate in float32 even when fc2 weights are fp16. + Demonstrates that the pre-fix code path (no .float() after fc2) produces + NaN/Inf with large-scale weights, while the fixed path does not. +2. Output shape / value sanity — correct [B, 1, T*upsample] shape, finite values. +3. Acoustic decoder float32 invariant — weights must be float32 after build. +""" + +from __future__ import annotations + +import pytest +import torch +import torch.nn as nn + +from vllm_omni.model_executor.models.omnivoice.omnivoice_decoder import ( + HiggsAudioRVQ, + OmniVoiceDecoder, +) +from vllm_omni.transformers_utils.configs.omnivoice import OmniVoiceConfig + +pytestmark = [ + pytest.mark.core_model, + pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"), +] + +DEVICE = torch.device("cuda:0") +UPSAMPLE = 4 # synthetic acoustic decoder stride (real DAC uses 960) +T_FRAMES = 25 # number of audio token frames to decode + + +# --------------------------------------------------------------------------- +# Tiny acoustic decoder: ConvTranspose1d upsample by UPSAMPLE +# --------------------------------------------------------------------------- + + +class _TinyAcousticDecoder(nn.Module): + def __init__(self, in_channels: int = 256, upsample: int = UPSAMPLE): + super().__init__() + self.conv = nn.ConvTranspose1d(in_channels, 1, kernel_size=upsample, stride=upsample) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.conv(x) # [B, 1, T*upsample] + + +# --------------------------------------------------------------------------- +# Builder +# --------------------------------------------------------------------------- + + +def _build_decoder( + *, + fc2_dtype: torch.dtype = torch.float32, + acoustic_dtype: torch.dtype = torch.float32, + weight_scale: float = 1.0, +) -> OmniVoiceDecoder: + """Build an OmniVoiceDecoder with synthetic components (no model checkpoint required). + + Args: + fc2_dtype: dtype for the fc2 linear layer weights. + acoustic_dtype: dtype for the acoustic decoder weights. + weight_scale: constant weight value for overflow testing. + """ + config = OmniVoiceConfig() + decoder = OmniVoiceDecoder(config) + + decoder.quantizer = HiggsAudioRVQ(num_quantizers=8, codebook_size=1024, codebook_dim=64, hidden_size=1024).to( + DEVICE + ) + + decoder.fc2 = nn.Linear(1024, 256).to(DEVICE).to(fc2_dtype) + + acoustic = _TinyAcousticDecoder(in_channels=256, upsample=UPSAMPLE).to(DEVICE).to(acoustic_dtype) + if weight_scale != 1.0: + with torch.no_grad(): + nn.init.constant_(acoustic.conv.weight, weight_scale) + nn.init.constant_(acoustic.conv.bias, weight_scale) + decoder.acoustic_decoder = acoustic + + decoder._loaded = True + return decoder + + +# --------------------------------------------------------------------------- +# 1. NaN regression +# --------------------------------------------------------------------------- + + +def test_no_nan_fp16_fc2_float32_acoustic(): + """fp16 fc2 + float32 acoustic decoder (the fixed path) must not produce NaN or Inf.""" + decoder = _build_decoder(fc2_dtype=torch.float16, acoustic_dtype=torch.float32) + tokens = torch.randint(0, 1024, (1, 8, T_FRAMES), device=DEVICE) + + audio = decoder(tokens) + + assert not torch.isnan(audio).any(), "NaN in output — fp16 overflow may have reached acoustic decoder" + assert not torch.isinf(audio).any(), "Inf in output" + + +def test_fp16_acoustic_decoder_produces_nan_without_fix(): + """Pre-fix scenario: fp16 acoustic decoder with large weights overflows to NaN/Inf. + + This test documents the original bug and will fail (raising AssertionError) + if the underlying hardware / Triton version somehow avoids overflow, but + with weights scaled to 300.0 the ConvTranspose1d intermediate activations + reliably exceed fp16 max (~65504) in practice. + """ + decoder = _build_decoder(fc2_dtype=torch.float16, acoustic_dtype=torch.float16, weight_scale=300.0) + with torch.no_grad(): + nn.init.constant_(decoder.fc2.weight, 300.0) + nn.init.constant_(decoder.fc2.bias, 300.0) + for q in decoder.quantizer.quantizers: + nn.init.constant_(q.codebook.weight, 300.0) + + tokens = torch.randint(0, 1024, (1, 8, T_FRAMES), device=DEVICE) + + # Simulate pre-fix forward: no .float() after fc2 + codes = tokens.transpose(0, 1).long() + quantized = decoder.quantizer.decode(codes) + quantized_fp16 = decoder.fc2(quantized.transpose(1, 2).to(decoder.fc2.weight.dtype)).transpose( + 1, 2 + ) # stays fp16, no .float() + audio_broken = decoder.acoustic_decoder(quantized_fp16) + + assert torch.isnan(audio_broken).any() or torch.isinf(audio_broken).any(), ( + "Expected NaN/Inf in pre-fix fp16 path with large weights — test setup may need a higher weight_scale" + ) + + +def test_acoustic_decoder_receives_float32_input(): + """The .float() cast after fc2 must ensure acoustic decoder always gets float32 input, + regardless of fc2 weight dtype.""" + decoder = _build_decoder(fc2_dtype=torch.float16, acoustic_dtype=torch.float32) + + captured_dtypes: list[torch.dtype] = [] + original_fwd = decoder.acoustic_decoder.forward + + def _hook(x: torch.Tensor) -> torch.Tensor: + captured_dtypes.append(x.dtype) + return original_fwd(x) + + decoder.acoustic_decoder.forward = _hook # type: ignore[method-assign] + tokens = torch.randint(0, 1024, (1, 8, T_FRAMES), device=DEVICE) + decoder(tokens) + + assert captured_dtypes, "acoustic_decoder.forward was never called" + assert captured_dtypes[0] == torch.float32, ( + f"Acoustic decoder received {captured_dtypes[0]}, expected torch.float32 — " + "the .float() upcast after fc2 may be missing" + ) + + +# --------------------------------------------------------------------------- +# 2. Output shape and value sanity +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("batch,t_frames", [(1, 10), (1, 50), (2, 25)]) +def test_output_shape(batch, t_frames): + """Decoder must return [B, 1, T*upsample] waveform tensor.""" + decoder = _build_decoder() + tokens = torch.randint(0, 1024, (batch, 8, t_frames), device=DEVICE) + + audio = decoder(tokens) + + assert audio.shape == (batch, 1, t_frames * UPSAMPLE), ( + f"Expected ({batch}, 1, {t_frames * UPSAMPLE}), got {audio.shape}" + ) + + +def test_no_nan_float32_baseline(): + """Float32 fc2 + float32 acoustic decoder must always produce finite output.""" + decoder = _build_decoder(fc2_dtype=torch.float32, acoustic_dtype=torch.float32) + tokens = torch.randint(0, 1024, (1, 8, T_FRAMES), device=DEVICE) + + audio = decoder(tokens) + + assert not torch.isnan(audio).any() + assert not torch.isinf(audio).any() + + +def test_output_is_3d(): + """Output must be 3-D: [B, 1, samples]. OmniVoiceDecoder.forward unsqueezes if needed.""" + decoder = _build_decoder() + tokens = torch.randint(0, 1024, (1, 8, 10), device=DEVICE) + + audio = decoder(tokens) + + assert audio.dim() == 3, f"Expected 3-D output, got {audio.dim()}-D" + assert audio.shape[1] == 1, f"Expected channel dim=1, got {audio.shape[1]}" + + +# --------------------------------------------------------------------------- +# 3. Acoustic decoder float32 invariant +# --------------------------------------------------------------------------- + + +def test_acoustic_decoder_weights_are_float32(): + """Acoustic decoder weights must be float32 — any fp16 parameter is a regression indicator. + + In production, load_weights() calls self.acoustic_decoder.float() to enforce this. + The synthetic builder mirrors that invariant. + """ + decoder = _build_decoder(fc2_dtype=torch.float16, acoustic_dtype=torch.float32) + + for name, param in decoder.acoustic_decoder.named_parameters(): + assert param.dtype == torch.float32, ( + f"acoustic_decoder.{name} is {param.dtype} — must be float32 (see load_weights fix)" + ) diff --git a/tests/model_executor/models/omnivoice/test_triton_kernels.py b/tests/model_executor/models/omnivoice/test_triton_kernels.py new file mode 100644 index 000000000..1598b0f74 --- /dev/null +++ b/tests/model_executor/models/omnivoice/test_triton_kernels.py @@ -0,0 +1,200 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +""" +Correctness tests for OmniVoice Triton kernels. + +Verifies that RMSNorm, SwiGLU, and fused-add+RMSNorm Triton kernels +produce results numerically equivalent to the PyTorch reference. +All computations follow the kernel's internal float32 promotion rules. +""" + +from __future__ import annotations + +import pytest +import torch + +pytestmark = [ + pytest.mark.core_model, + pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required"), +] + +DEVICE = torch.device("cuda:0") +EPS = 1e-6 + +try: + from vllm_omni.model_executor.models.omnivoice.omnivoice_generator import ( + _TRITON_AVAILABLE, + triton_fused_add_rms_norm, + triton_rms_norm, + triton_swiglu, + ) +except ImportError: + _TRITON_AVAILABLE = False + +triton_available = pytest.mark.skipif(not _TRITON_AVAILABLE, reason="Triton not available on this platform") + + +# --------------------------------------------------------------------------- +# PyTorch reference implementations (mirror the kernel's internal arithmetic) +# --------------------------------------------------------------------------- + + +def _ref_rms_norm(x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor: + # Kernel promotes x to float32, computes RMS, casts back to x.dtype, then mul weight + x_f32 = x.float() + ms = x_f32.pow(2).mean(-1, keepdim=True) + normed = (x_f32 * torch.rsqrt(ms + eps)).to(x.dtype) + return normed * weight.to(normed.dtype) + + +def _ref_swiglu(gate: torch.Tensor, up: torch.Tensor) -> torch.Tensor: + # Kernel: gate → float32, silu, cast to up.dtype, mul up + gate_f32 = gate.float() + silu = gate_f32 * torch.sigmoid(gate_f32) + return silu.to(up.dtype) * up + + +def _ref_fused_add_rms_norm( + x: torch.Tensor, residual: torch.Tensor, weight: torch.Tensor, eps: float +) -> tuple[torch.Tensor, torch.Tensor]: + # Kernel: s_f32 = x+r in float32, s stored as x.dtype; y = norm(s_f32)*weight + s_f32 = x.float() + residual.float() + s = s_f32.to(x.dtype) + ms = s_f32.pow(2).mean(-1, keepdim=True) + normed = (s_f32 * torch.rsqrt(ms + eps)).to(x.dtype) + y = normed * weight.to(normed.dtype) + return y, s + + +# --------------------------------------------------------------------------- +# RMSNorm +# --------------------------------------------------------------------------- + + +@triton_available +@pytest.mark.parametrize("rows,cols", [(1, 64), (4, 512), (16, 1024), (32, 3072)]) +def test_rms_norm_float32(rows, cols): + torch.manual_seed(0) + x = torch.randn(rows, cols, device=DEVICE) + w = torch.ones(cols, device=DEVICE) + + ref = _ref_rms_norm(x, w, EPS) + out = triton_rms_norm(x, w, EPS) + + torch.testing.assert_close(out, ref, atol=1e-5, rtol=1e-5) + + +@triton_available +@pytest.mark.parametrize("shape", [(2, 16, 64), (4, 32, 512)]) +def test_rms_norm_3d_input(shape): + """Triton wrapper must handle 3-D [B, S, H] inputs by flattening and restoring shape.""" + torch.manual_seed(1) + hidden = shape[-1] + x = torch.randn(*shape, device=DEVICE) + w = torch.ones(hidden, device=DEVICE) + + ref = _ref_rms_norm(x, w, EPS) + out = triton_rms_norm(x, w, EPS) + + torch.testing.assert_close(out, ref, atol=1e-5, rtol=1e-5) + assert out.shape == x.shape + + +@triton_available +def test_rms_norm_no_nan_on_zero_input(): + """RMSNorm of the zero vector must not produce NaN (eps prevents divide-by-zero).""" + x = torch.zeros(4, 256, device=DEVICE) + w = torch.ones(256, device=DEVICE) + + out = triton_rms_norm(x, w, EPS) + + assert not torch.isnan(out).any() + + +# --------------------------------------------------------------------------- +# SwiGLU +# --------------------------------------------------------------------------- + + +@triton_available +@pytest.mark.parametrize("rows,cols", [(1, 64), (4, 512), (16, 3072)]) +def test_swiglu_float32(rows, cols): + torch.manual_seed(2) + gate = torch.randn(rows, cols, device=DEVICE) + up = torch.randn(rows, cols, device=DEVICE) + + ref = _ref_swiglu(gate, up) + out = triton_swiglu(gate, up) + + torch.testing.assert_close(out, ref, atol=1e-5, rtol=1e-5) + + +@triton_available +def test_swiglu_negative_gate(): + """silu(x) = x*sigmoid(x) is defined for negative x; result must not be NaN.""" + gate = torch.full((4, 64), -10.0, device=DEVICE) + up = torch.ones(4, 64, device=DEVICE) + + ref = _ref_swiglu(gate, up) + out = triton_swiglu(gate, up) + + torch.testing.assert_close(out, ref, atol=1e-5, rtol=1e-5) + assert not torch.isnan(out).any() + + +@triton_available +def test_swiglu_output_shape_preserved(): + gate = torch.randn(3, 8, 512, device=DEVICE) + up = torch.randn(3, 8, 512, device=DEVICE) + out = triton_swiglu(gate, up) + assert out.shape == gate.shape + + +# --------------------------------------------------------------------------- +# Fused Add + RMSNorm +# --------------------------------------------------------------------------- + + +@triton_available +@pytest.mark.parametrize("rows,cols", [(1, 64), (4, 512), (16, 1024)]) +def test_fused_add_rms_norm_float32(rows, cols): + torch.manual_seed(3) + x = torch.randn(rows, cols, device=DEVICE) + residual = torch.randn(rows, cols, device=DEVICE) + weight = torch.ones(cols, device=DEVICE) + + ref_y, ref_s = _ref_fused_add_rms_norm(x, residual, weight, EPS) + out_y, out_s = triton_fused_add_rms_norm(x, residual, weight, EPS) + + torch.testing.assert_close(out_y, ref_y, atol=1e-5, rtol=1e-5) + # Sum output s = x + r must be bit-exact (no numerics involved, just addition) + torch.testing.assert_close(out_s, ref_s, atol=0, rtol=0) + + +@triton_available +def test_fused_add_rms_norm_sum_is_bitexact(): + """The residual sum s = x + r must be bit-exact between Triton and PyTorch.""" + torch.manual_seed(4) + x = torch.randn(8, 512, device=DEVICE) + r = torch.randn(8, 512, device=DEVICE) + w = torch.ones(512, device=DEVICE) + + _, ref_s = _ref_fused_add_rms_norm(x, r, w, EPS) + _, out_s = triton_fused_add_rms_norm(x, r, w, EPS) + + torch.testing.assert_close(out_s, ref_s, atol=0, rtol=0) + + +@triton_available +def test_fused_add_rms_norm_returns_two_tensors(): + """Wrapper must return exactly two tensors: (normed_output, sum).""" + x = torch.randn(4, 256, device=DEVICE) + r = torch.randn(4, 256, device=DEVICE) + w = torch.ones(256, device=DEVICE) + + result = triton_fused_add_rms_norm(x, r, w, EPS) + + assert len(result) == 2 + y, s = result + assert y.shape == x.shape + assert s.shape == x.shape diff --git a/vllm_omni/model_executor/models/omnivoice/omnivoice_decoder.py b/vllm_omni/model_executor/models/omnivoice/omnivoice_decoder.py index 59f23d0d0..efb6d9dc7 100644 --- a/vllm_omni/model_executor/models/omnivoice/omnivoice_decoder.py +++ b/vllm_omni/model_executor/models/omnivoice/omnivoice_decoder.py @@ -108,7 +108,10 @@ class OmniVoiceDecoder(nn.Module): quantized = self.quantizer.decode(codes) # Project: [B, 1024, T] → fc2 → [B, 256, T] - quantized = self.fc2(quantized.transpose(1, 2)).transpose(1, 2) + # Cast to fc2 weight dtype (may be fp16 when checkpoint stores weights as fp16), + # then upcast back to float32 — acoustic decoder ConvTranspose1d upsampling + # produces intermediate values that exceed the fp16 range (~65504), causing NaN. + quantized = self.fc2(quantized.transpose(1, 2).to(self.fc2.weight.dtype)).transpose(1, 2).float() # Acoustic decoder: [B, 256, T] → [B, 1, T*960] audio = self.acoustic_decoder(quantized) @@ -192,6 +195,11 @@ class OmniVoiceDecoder(nn.Module): param.data.copy_(state_dict[higgs_name]) loaded += 1 + # Checkpoint weights may be fp16; force float32 so ConvTranspose1d + # upsampling doesn't overflow (fp16 max ~65504 is too small for + # intermediate activations in the DAC upsampling chain). + self.acoustic_decoder.float() + # Apply HiggsAudioV2 output padding adjustment self._adjust_output_padding(self.acoustic_decoder) diff --git a/vllm_omni/model_executor/models/omnivoice/omnivoice_generator.py b/vllm_omni/model_executor/models/omnivoice/omnivoice_generator.py index eedb5828a..0de2a1f60 100644 --- a/vllm_omni/model_executor/models/omnivoice/omnivoice_generator.py +++ b/vllm_omni/model_executor/models/omnivoice/omnivoice_generator.py @@ -13,6 +13,8 @@ via FlashAttention/SageAttention/SDPA backends. from __future__ import annotations import math +import threading +from collections import OrderedDict import torch import torch.nn as nn @@ -24,6 +26,184 @@ from vllm_omni.transformers_utils.configs.omnivoice import OmniVoiceConfig logger = init_logger(__name__) +# --------------------------------------------------------------------------- +# Triton kernels (inference-only; graceful fallback when triton is absent) +# --------------------------------------------------------------------------- + +_TRITON_AVAILABLE = False +try: + import triton + import triton.language as tl + + try: + from triton.language.extra.libdevice import rsqrt # noqa: F401 + except ModuleNotFoundError: + from triton.language.extra.cuda.libdevice import rsqrt # noqa: F401 + + def _calculate_settings(n: int) -> tuple[int, int]: + MAX_FUSED_SIZE = 65536 + BLOCK_SIZE = triton.next_power_of_2(n) + if BLOCK_SIZE > MAX_FUSED_SIZE: + raise RuntimeError(f"n={n} exceeds max Triton block size {MAX_FUSED_SIZE}") + num_warps = 4 + if BLOCK_SIZE >= 32768: + num_warps = 32 + elif BLOCK_SIZE >= 8192: + num_warps = 16 + elif BLOCK_SIZE >= 2048: + num_warps = 8 + return BLOCK_SIZE, num_warps + + @triton.jit + def _rms_norm_fwd_kernel( + Y_ptr, # noqa: N803 + Y_stride, # noqa: N803 + X_ptr, # noqa: N803 + X_stride, # noqa: N803 + W_ptr, # noqa: N803 + n_cols, + eps, + BLOCK_SIZE: tl.constexpr, # noqa: N803 + ): + row = tl.program_id(0).to(tl.int64) + cols = tl.arange(0, BLOCK_SIZE) + mask = cols < n_cols + x = tl.load(X_ptr + row * X_stride + cols, mask=mask, other=0.0) + w = tl.load(W_ptr + cols, mask=mask, other=0.0) + x_f32 = x.to(tl.float32) + ms = tl.sum(x_f32 * x_f32, axis=0) / n_cols + rstd = rsqrt(ms + eps) + y = (x_f32 * rstd).to(x.dtype) * w + tl.store(Y_ptr + row * Y_stride + cols, y, mask=mask) + + def triton_rms_norm(x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor: + shape = x.shape + n_cols = shape[-1] + x2d = x.contiguous().view(-1, n_cols) + w = weight.contiguous() + BLOCK_SIZE, num_warps = _calculate_settings(n_cols) + y = torch.empty_like(x2d) + _rms_norm_fwd_kernel[(x2d.shape[0],)]( + y, + y.stride(0), + x2d, + x2d.stride(0), + w, + n_cols, + eps, + BLOCK_SIZE=BLOCK_SIZE, + num_warps=num_warps, + ) + return y.view(*shape) + + @triton.jit + def _swiglu_fwd_kernel( + gate_ptr, + up_ptr, + out_ptr, + stride, + n_cols: tl.constexpr, + BLOCK_SIZE: tl.constexpr, # noqa: N803 + ): + pid = tl.program_id(0).to(tl.int64) + gate_ptr += pid * stride + up_ptr += pid * stride + out_ptr += pid * stride + cols = tl.arange(0, BLOCK_SIZE) + mask = cols < n_cols + gate = tl.load(gate_ptr + cols, mask=mask, other=0).to(tl.float32) + up = tl.load(up_ptr + cols, mask=mask, other=0) + silu_gate = gate * tl.sigmoid(gate) + out = silu_gate.cast(up.dtype) * up + tl.store(out_ptr + cols, out, mask=mask) + + def triton_swiglu(gate: torch.Tensor, up: torch.Tensor) -> torch.Tensor: + gate = gate.contiguous() + up = up.contiguous() + shape = gate.shape + n_cols = shape[-1] + g2d = gate.view(-1, n_cols) + u2d = up.view(-1, n_cols) + out = torch.empty_like(g2d) + BLOCK_SIZE, num_warps = _calculate_settings(n_cols) + _swiglu_fwd_kernel[(g2d.shape[0],)]( + g2d, + u2d, + out, + out.stride(0), + n_cols=n_cols, + BLOCK_SIZE=BLOCK_SIZE, + num_warps=num_warps, + ) + return out.view(*shape) + + @triton.jit + def _fused_add_rms_norm_fwd_kernel( + Y_ptr, # noqa: N803 + Y_stride, # noqa: N803 + S_ptr, # noqa: N803 + S_stride, # noqa: N803 + X_ptr, # noqa: N803 + X_stride, # noqa: N803 + R_ptr, # noqa: N803 + R_stride, # noqa: N803 + W_ptr, # noqa: N803 + n_cols, + eps, + BLOCK_SIZE: tl.constexpr, # noqa: N803 + ): + row = tl.program_id(0).to(tl.int64) + cols = tl.arange(0, BLOCK_SIZE) + mask = cols < n_cols + x = tl.load(X_ptr + row * X_stride + cols, mask=mask, other=0.0) + r = tl.load(R_ptr + row * R_stride + cols, mask=mask, other=0.0) + dtype = x.dtype + s_f32 = x.to(tl.float32) + r.to(tl.float32) + s = s_f32.to(dtype) + tl.store(S_ptr + row * S_stride + cols, s, mask=mask) + w = tl.load(W_ptr + cols, mask=mask, other=0.0) + ms = tl.sum(s_f32 * s_f32, axis=0) / n_cols + rstd = rsqrt(ms + eps) + y = (s_f32 * rstd).to(dtype) * w + tl.store(Y_ptr + row * Y_stride + cols, y, mask=mask) + + def triton_fused_add_rms_norm( + x: torch.Tensor, + residual: torch.Tensor, + weight: torch.Tensor, + eps: float, + ) -> tuple[torch.Tensor, torch.Tensor]: + shape = x.shape + n_cols = shape[-1] + x2d = x.contiguous().view(-1, n_cols) + r2d = residual.contiguous().view(-1, n_cols) + w = weight.contiguous() + BLOCK_SIZE, num_warps = _calculate_settings(n_cols) + y = torch.empty_like(x2d) + s = torch.empty_like(x2d) + _fused_add_rms_norm_fwd_kernel[(x2d.shape[0],)]( + y, + y.stride(0), + s, + s.stride(0), + x2d, + x2d.stride(0), + r2d, + r2d.stride(0), + w, + n_cols, + eps, + BLOCK_SIZE=BLOCK_SIZE, + num_warps=num_warps, + ) + return y.view(*shape), s.view(*shape) + + _TRITON_AVAILABLE = True + logger.debug("OmniVoice Triton kernels loaded") +except Exception: + logger.debug("Triton not available; using PyTorch fallback for OmniVoice kernels") + + # --------------------------------------------------------------------------- # Unmasking schedule helpers # --------------------------------------------------------------------------- @@ -63,6 +243,8 @@ class OmniVoiceRMSNorm(nn.Module): self.eps = eps def forward(self, x: torch.Tensor) -> torch.Tensor: + if _TRITON_AVAILABLE: + return triton_rms_norm(x, self.weight, self.eps) variance = x.to(torch.float32).pow(2).mean(-1, keepdim=True) x = x * torch.rsqrt(variance + self.eps) return self.weight * x.to(self.weight.dtype) @@ -158,6 +340,8 @@ class OmniVoiceMLP(nn.Module): self.down_proj = nn.Linear(config.llm_intermediate_size, config.llm_hidden_size, bias=False) def forward(self, x: torch.Tensor) -> torch.Tensor: + if _TRITON_AVAILABLE: + return self.down_proj(triton_swiglu(self.gate_proj(x), self.up_proj(x))) return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x)) @@ -181,13 +365,22 @@ class OmniVoiceTransformerBlock(nn.Module): residual = hidden_states hidden_states = self.input_layernorm(hidden_states) hidden_states = self.self_attn(hidden_states, attention_mask=attention_mask, cos=cos, sin=sin) - hidden_states = residual + hidden_states - residual = hidden_states - hidden_states = self.post_attention_layernorm(hidden_states) + if _TRITON_AVAILABLE: + # Fused: (attn_out + residual) + RMSNorm in one kernel + hidden_states, residual = triton_fused_add_rms_norm( + hidden_states, + residual, + self.post_attention_layernorm.weight, + self.post_attention_layernorm.eps, + ) + else: + hidden_states = residual + hidden_states + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + hidden_states = self.mlp(hidden_states) hidden_states = residual + hidden_states - return hidden_states @@ -222,6 +415,214 @@ def _apply_rotary_pos_emb(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) return x * torch.cat([cos, cos], dim=-1) + rotated * torch.cat([sin, sin], dim=-1) +# --------------------------------------------------------------------------- +# CUDA Graph wrapper +# --------------------------------------------------------------------------- + + +class _OmniVoiceCUDAGraphForward: + """Pre-captures CUDA graphs for predefined sequence-length buckets. + + Memory layout: all graphs share a single per-instance pool handle, which + isolates OmniVoice CUDA memory from other vllm modules. Sequential replay + (one step at a time) means pool sharing is safe. + """ + + # Default bucket count is 10; 16 gives modest headroom for edge cases + # (seq_len > max bucket or non-CFG batch) without unbounded GPU growth. + _MAX_LAZY_GRAPHS: int = 16 + + def __init__(self, generator: OmniVoiceGenerator, capture_sizes: list[int]) -> None: + self._gen = generator + self._capture_sizes = sorted(capture_sizes) + # Pre-warmed graphs keyed by (two_b, bucket); fixed set, never evicted. + self._graphs: dict[tuple[int, int], dict] = {} + # Lazy-captured graphs for oversized / non-CFG shapes; capped via LRU. + self._lazy_graphs: OrderedDict[tuple[int, int], dict] = OrderedDict() + self._lock = threading.Lock() + # Per-instance pool handle: isolates OmniVoice CUDA memory from other + # vllm modules while still allowing safe re-use across sequential replays. + self._pool_handle: int | None = None + + def _find_bucket(self, seq_len: int) -> int | None: + for bucket in self._capture_sizes: + if bucket >= seq_len: + return bucket + return None + + def _pad_inputs( + self, + input_ids: torch.Tensor, + audio_mask: torch.Tensor, + attention_mask: torch.Tensor | None, + bucket: int, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]: + S = input_ids.shape[-1] + if S == bucket: + return input_ids, audio_mask, attention_mask + + two_b = input_ids.shape[0] + num_cb = input_ids.shape[1] + + ids_padded = torch.zeros(two_b, num_cb, bucket, dtype=input_ids.dtype, device=input_ids.device) + ids_padded[:, :, :S] = input_ids + + mask_padded = torch.zeros(two_b, bucket, dtype=torch.bool, device=audio_mask.device) + mask_padded[:, :S] = audio_mask + + if attention_mask is not None: + attn_padded = torch.zeros( + two_b, + 1, + bucket, + bucket, + dtype=attention_mask.dtype, + device=attention_mask.device, + ) + attn_padded[:, :, :S, :S] = attention_mask + else: + attn_padded = None + + return ids_padded, mask_padded, attn_padded + + def _capture_for_key( + self, + key: tuple[int, int], + input_ids: torch.Tensor, + audio_mask: torch.Tensor, + attention_mask: torch.Tensor | None, + ) -> dict: + _, bucket = key + device = input_ids.device + + self._gen._ensure_rope(bucket, device) + model_dtype = self._gen.text_embedding.weight.dtype + static_cos = self._gen._rope_cos[:bucket].to(device=device, dtype=model_dtype).contiguous() + static_sin = self._gen._rope_sin[:bucket].to(device=device, dtype=model_dtype).contiguous() + + static_input_ids = input_ids.clone() + static_audio_mask = audio_mask.clone() + static_attn_mask = attention_mask.clone() if attention_mask is not None else None + + with torch.no_grad(): + _ = self._gen._step_forward( + static_input_ids, + static_audio_mask, + static_attn_mask, + static_cos, + static_sin, + ) + torch.accelerator.synchronize(device) + + # Lazy-init per-instance pool handle: isolates OmniVoice CUDA Graph + # memory from other vllm modules (unlike get_global_graph_pool which + # shares a single pool across all captured graphs and can cause memory + # aliasing when two graphs replay concurrently). + if self._pool_handle is None: + self._pool_handle = torch.cuda.graph_pool_handle() + + graph = torch.cuda.CUDAGraph() + with torch.no_grad(): + with torch.cuda.graph(graph, pool=self._pool_handle): + static_output = self._gen._step_forward( + static_input_ids, + static_audio_mask, + static_attn_mask, + static_cos, + static_sin, + ) + + entry = { + "graph": graph, + "static_input_ids": static_input_ids, + "static_audio_mask": static_audio_mask, + "static_attn_mask": static_attn_mask, + "static_cos": static_cos, + "static_sin": static_sin, + "static_output": static_output, + } + logger.info("OmniVoice CUDA Graph captured for key %s", key) + return entry + + def warmup(self, device: torch.device) -> None: + """Pre-capture graphs for all bucket sizes with B=1 (two_b=2 for CFG).""" + if not torch.cuda.is_available(): + return + logger.info( + "OmniVoice CUDA Graph warmup: capturing %d bucket sizes %s", + len(self._capture_sizes), + self._capture_sizes, + ) + two_b = 2 + num_cb = self._gen.config.num_audio_codebook + for bucket in self._capture_sizes: + key = (two_b, bucket) + dummy_ids = torch.zeros(two_b, num_cb, bucket, dtype=torch.long, device=device) + dummy_mask = torch.zeros(two_b, bucket, dtype=torch.bool, device=device) + dummy_attn = torch.ones(two_b, 1, bucket, bucket, dtype=torch.bool, device=device) + self._graphs[key] = self._capture_for_key(key, dummy_ids, dummy_mask, dummy_attn) + logger.info("OmniVoice CUDA Graph warmup complete (%d graphs)", len(self._graphs)) + + def __call__( + self, + input_ids: torch.Tensor, + audio_mask: torch.Tensor, + attention_mask: torch.Tensor | None, + ) -> torch.Tensor: + if torch.cuda.is_current_stream_capturing(): + seq_len = input_ids.shape[-1] + self._gen._ensure_rope(seq_len, input_ids.device) + dtype = self._gen.text_embedding.weight.dtype + cos = self._gen._rope_cos[:seq_len].to(device=input_ids.device, dtype=dtype) + sin = self._gen._rope_sin[:seq_len].to(device=input_ids.device, dtype=dtype) + return self._gen._step_forward(input_ids, audio_mask, attention_mask, cos, sin) + + seq_len = input_ids.shape[-1] + two_b = input_ids.shape[0] + bucket = self._find_bucket(seq_len) if two_b == 2 else None + + if bucket is None: + # Lazy capture: oversized sequence or non-unit batch (no pre-warmed bucket). + # Lock prevents concurrent threads from double-capturing the same key. + # _lazy_graphs is capped at _MAX_LAZY_GRAPHS with LRU eviction to + # prevent unbounded GPU memory growth when seq_len varies widely. + key = (two_b, seq_len) + ids_in, mask_in, attn_in = input_ids, audio_mask, attention_mask + with self._lock: + entry = self._lazy_graphs.get(key) + if entry is None: + entry = self._capture_for_key(key, ids_in, mask_in, attn_in) + if len(self._lazy_graphs) >= self._MAX_LAZY_GRAPHS: + evicted_key, _ = self._lazy_graphs.popitem(last=False) + logger.warning("OmniVoice CUDA Graph lazy cache full; evicted key %s", evicted_key) + self._lazy_graphs[key] = entry + else: + key = (two_b, bucket) + ids_in, mask_in, attn_in = self._pad_inputs(input_ids, audio_mask, attention_mask, bucket) + with self._lock: + entry = self._graphs.get(key) + if entry is None: + entry = self._capture_for_key(key, ids_in, mask_in, attn_in) + self._graphs[key] = entry + + entry["static_input_ids"].copy_(ids_in) + entry["static_audio_mask"].copy_(mask_in) + if attn_in is not None and entry["static_attn_mask"] is not None: + entry["static_attn_mask"].copy_(attn_in) + + entry["graph"].replay() + + output = entry["static_output"] + if bucket is not None and bucket != seq_len: + output = output[:, :, :seq_len, :] + return output + + def clear(self) -> None: + with self._lock: + self._graphs.clear() + self._lazy_graphs.clear() + + # --------------------------------------------------------------------------- # Generator model # --------------------------------------------------------------------------- @@ -278,6 +679,11 @@ class OmniVoiceGenerator(nn.Module): self._rope_cos = None self._rope_sin = None + # CUDA Graph (bucket-size pre-capture; lazy fallback for oversized shapes) + self._cuda_graph_fwd: _OmniVoiceCUDAGraphForward | None = ( + _OmniVoiceCUDAGraphForward(self, config.cuda_graph_capture_sizes) if config.enable_cuda_graph else None + ) + def _ensure_rope(self, seq_len: int, device: torch.device) -> None: """Lazily compute RoPE cos/sin if needed.""" if self._rope_cos is None or self._rope_cos.shape[0] < seq_len: @@ -364,6 +770,20 @@ class OmniVoiceGenerator(nn.Module): self.config.audio_vocab_size, ).permute(0, 2, 1, 3) # [B, 8, S, 1025] + def _step_forward( + self, + input_ids: torch.Tensor, + audio_mask: torch.Tensor, + attention_mask: torch.Tensor | None, + cos: torch.Tensor, + sin: torch.Tensor, + ) -> torch.Tensor: + """Single unmasking-step forward using pre-cast RoPE tensors (CUDA graph safe).""" + hidden_states = self._prepare_embeddings(input_ids, audio_mask) + for layer in self.layers: + hidden_states = layer(hidden_states, attention_mask=attention_mask, cos=cos, sin=sin) + return self._get_logits(self.norm(hidden_states)) + @torch.inference_mode() def forward( self, @@ -440,10 +860,12 @@ class OmniVoiceGenerator(nn.Module): # Main iterative loop for step in range(num_step): - # Prepare embeddings and run transformer - inputs_embeds = self._prepare_embeddings(input_ids, audio_mask) - hidden_states = self._transformer_forward(inputs_embeds, attention_mask) - batch_logits = self._get_logits(hidden_states).to(torch.float32) + if self._cuda_graph_fwd is not None and input_ids.is_cuda: + batch_logits = self._cuda_graph_fwd(input_ids, audio_mask, attention_mask).to(torch.float32) + else: + inputs_embeds = self._prepare_embeddings(input_ids, audio_mask) + hidden_states = self._transformer_forward(inputs_embeds, attention_mask) + batch_logits = self._get_logits(hidden_states).to(torch.float32) # batch_logits: [2*B, 8, S, 1025] for i in range(B): @@ -586,3 +1008,6 @@ class OmniVoiceGenerator(nn.Module): ) else: logger.info("Generator: all %d weights loaded", len(loaded_keys)) + + if self._cuda_graph_fwd is not None: + self._cuda_graph_fwd.warmup(device) diff --git a/vllm_omni/transformers_utils/configs/omnivoice.py b/vllm_omni/transformers_utils/configs/omnivoice.py index d1bd3527f..1bd7207b6 100644 --- a/vllm_omni/transformers_utils/configs/omnivoice.py +++ b/vllm_omni/transformers_utils/configs/omnivoice.py @@ -2,6 +2,8 @@ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """OmniVoice configuration for vLLM-Omni two-stage pipeline.""" +import os + from transformers import AutoConfig from transformers.configuration_utils import PretrainedConfig @@ -80,6 +82,14 @@ class OmniVoiceConfig(PretrainedConfig): # Serving self.speculative_config = None + # Precedence: config.json > OMNIVOICE_CUDA_GRAPH env var > default (enabled). + # Read once at init; changing OMNIVOICE_CUDA_GRAPH at runtime has no effect. + self.enable_cuda_graph = getattr(self, "enable_cuda_graph", os.environ.get("OMNIVOICE_CUDA_GRAPH", "1") != "0") + self.cuda_graph_capture_sizes = getattr( + self, + "cuda_graph_capture_sizes", + [128, 192, 256, 320, 384, 448, 512, 640, 768, 1024], + ) AutoConfig.register("omnivoice", OmniVoiceConfig) From 4e4458e7bf8b16f34b9f76e07af04c77c5889e2a Mon Sep 17 00:00:00 2001 From: Yueqian Lin <70319226+linyueqian@users.noreply.github.com> Date: Fri, 22 May 2026 19:46:14 -0400 Subject: [PATCH 014/112] [Bugfix][CI] Run Whisper validation on CPU for single-GPU runners (#3822) Signed-off-by: Yueqian Lin --- tests/helpers/media.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/helpers/media.py b/tests/helpers/media.py index daf3a2ba4..5c76a9e1e 100644 --- a/tests/helpers/media.py +++ b/tests/helpers/media.py @@ -623,9 +623,11 @@ def _whisper_transcribe_in_current_process(output_path: str) -> str: if current_omni_platform.is_available(): n = current_omni_platform.get_device_count() - if n == 1: - device_index = 0 - elif n > 1: + # Single-GPU runners (e.g. the L4 nightly): the model server already + # occupies device 0. Loading Whisper there, once per concurrent + # request, competes for VRAM and OOMs. Only borrow an accelerator + # when a spare device exists; otherwise validate on CPU. + if n > 1: device_index = n - 1 if device_index is not None: From d01eaca39a747a8ef1273767f0f809c09dc494c4 Mon Sep 17 00:00:00 2001 From: fywc Date: Sat, 23 May 2026 08:09:33 +0800 Subject: [PATCH 015/112] [Feat] support cache-dit for DreamID-Omni (#3265) Signed-off-by: hanzheli Signed-off-by: fywc --- .../x_to_video_audio/x_to_video_audio.md | 1 + .../x_to_video_audio/x_to_video_audio.py | 18 ++ tests/diffusion/cache/test_cache_backends.py | 45 +++ tests/diffusion/cache/test_cache_dit.py | 1 + .../test_dreamid_omni_cache_dit.py | 290 ++++++++++++++++++ .../diffusion/cache/cache_dit_backend.py | 69 +++++ .../diffusion/models/dreamid_omni/fusion.py | 6 +- 7 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/offline_inference/test_dreamid_omni_cache_dit.py diff --git a/examples/offline_inference/x_to_video_audio/x_to_video_audio.md b/examples/offline_inference/x_to_video_audio/x_to_video_audio.md index 7621a6058..921fc1bd9 100644 --- a/examples/offline_inference/x_to_video_audio/x_to_video_audio.md +++ b/examples/offline_inference/x_to_video_audio/x_to_video_audio.md @@ -101,3 +101,4 @@ Key arguments: - `--video-negative-prompt`: negative prompt for video generation. - `--audio-negative-prompt`: negative prompt for audio generation. - `--enable-cpu-offload`: enable CPU offload (defaults False). +- `--cache-backend`: enable `cache_dit` for acceleration. diff --git a/examples/offline_inference/x_to_video_audio/x_to_video_audio.py b/examples/offline_inference/x_to_video_audio/x_to_video_audio.py index 544c285ab..96e5b3592 100644 --- a/examples/offline_inference/x_to_video_audio/x_to_video_audio.py +++ b/examples/offline_inference/x_to_video_audio/x_to_video_audio.py @@ -63,6 +63,7 @@ def parse_args() -> argparse.Namespace: action="store_true", help="Enable layerwise (blockwise) offloading on DiT modules.", ) + parser.add_argument("--cache-backend", type=str, default=None, choices=["cache_dit"], help="Cache backend.") parser.add_argument( "--use-hsdp", action="store_true", @@ -146,12 +147,29 @@ def main() -> None: hsdp_replicate_size=args.hsdp_replicate_size, ) + cache_config = None + if args.cache_backend == "cache_dit": + cache_config = { + "Fn_compute_blocks": 1, + "Bn_compute_blocks": 0, + "max_warmup_steps": 4, + "max_cached_steps": 20, + "residual_diff_threshold": 0.24, + "max_continuous_cached_steps": 3, + "enable_taylorseer": False, + "taylorseer_order": 1, + "scm_steps_mask_policy": None, + "scm_steps_policy": "dynamic", + } + omni = Omni( model=args.model, parallel_config=parallel_config, model_type=args.model_type, enable_cpu_offload=args.enable_cpu_offload, enable_layerwise_offload=args.enable_layerwise_offload, + cache_backend=args.cache_backend, + cache_config=cache_config, ) start = time.perf_counter() outputs = omni.generate(prompt, sampling_params) diff --git a/tests/diffusion/cache/test_cache_backends.py b/tests/diffusion/cache/test_cache_backends.py index ecb993945..b68944c6c 100644 --- a/tests/diffusion/cache/test_cache_backends.py +++ b/tests/diffusion/cache/test_cache_backends.py @@ -127,6 +127,10 @@ class TestCacheDiTBackend: """Test HunyuanImage3 custom cache-dit enabler is registered.""" assert "HunyuanImage3Pipeline" in CUSTOM_DIT_ENABLERS + def test_dreamid_custom_enabler_registered(self): + """Test DreamIDOmni custom cache-dit enabler is registered.""" + assert "DreamIDOmniPipeline" in CUSTOM_DIT_ENABLERS + @patch("vllm_omni.diffusion.cache.cache_dit_backend.BlockAdapter") @patch("vllm_omni.diffusion.cache.cache_dit_backend.cache_dit") def test_enable_hunyuan_pipeline_uses_model_transformer(self, mock_cache_dit, mock_block_adapter): @@ -169,6 +173,47 @@ class TestCacheDiTBackend: with pytest.raises(ValueError, match="HunyuanImage3Pipeline"): backend.enable(mock_pipeline) + @patch("vllm_omni.diffusion.cache.cache_dit_backend.BlockAdapter") + @patch("vllm_omni.diffusion.cache.cache_dit_backend.cache_dit") + def test_enable_dreamid_pipeline_uses_fused_blocks(self, mock_cache_dit, mock_block_adapter): + mock_pipeline = Mock() + mock_pipeline.__class__.__name__ = "DreamIDOmniPipeline" + mock_pipeline.transformer = Mock() + mock_pipeline.transformer.fused_blocks = Mock() + + mock_cache_dit.enable_cache = Mock() + mock_cache_dit.refresh_context = Mock() + + backend = CacheDiTBackend({"Fn_compute_blocks": 2}) + backend.enable(mock_pipeline) + + assert backend.enabled is True + assert backend._refresh_func is not None + mock_block_adapter.assert_called_once() + adapter_kwargs = mock_block_adapter.call_args.kwargs + assert adapter_kwargs["transformer"] is mock_pipeline.transformer + assert adapter_kwargs["blocks"] is mock_pipeline.transformer.fused_blocks + assert adapter_kwargs["forward_pattern"] == adapter_kwargs["forward_pattern"].__class__.Pattern_0 + assert adapter_kwargs["has_separate_cfg"] is True + assert len(adapter_kwargs["params_modifiers"]) == 1 + mock_cache_dit.enable_cache.assert_called_once() + + backend.refresh(mock_pipeline, num_inference_steps=12) + mock_cache_dit.refresh_context.assert_called_once() + call_args = mock_cache_dit.refresh_context.call_args + assert call_args[0][0] is mock_pipeline.transformer + assert call_args[1]["num_inference_steps"] == 12 + + def test_enable_dreamid_pipeline_requires_fused_blocks(self): + mock_pipeline = Mock() + mock_pipeline.__class__.__name__ = "DreamIDOmniPipeline" + mock_pipeline.transformer = Mock(spec=[]) + + backend = CacheDiTBackend({"Fn_compute_blocks": 2}) + + with pytest.raises(ValueError, match="DreamIDOmniPipeline"): + backend.enable(mock_pipeline) + class TestTeaCacheBackend: """Test TeaCacheBackend implementation.""" diff --git a/tests/diffusion/cache/test_cache_dit.py b/tests/diffusion/cache/test_cache_dit.py index 570c4f238..40a564485 100644 --- a/tests/diffusion/cache/test_cache_dit.py +++ b/tests/diffusion/cache/test_cache_dit.py @@ -15,6 +15,7 @@ from vllm_omni.diffusion.data import DiffusionCacheConfig pytestmark = [pytest.mark.core_model, pytest.mark.cpu] SEPARATE_CFG_ENABLERS = [ + cd_backend.enable_cache_for_dreamid_omni, cd_backend.enable_cache_for_ltx2, cd_backend.enable_cache_for_helios, cd_backend.enable_cache_for_wan22, diff --git a/tests/e2e/offline_inference/test_dreamid_omni_cache_dit.py b/tests/e2e/offline_inference/test_dreamid_omni_cache_dit.py new file mode 100644 index 000000000..a3f57064f --- /dev/null +++ b/tests/e2e/offline_inference/test_dreamid_omni_cache_dit.py @@ -0,0 +1,290 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +"""E2E test for DreamID-Omni offline inference with Cache-Dit. + +Uses the official DreamID-Omni ``test_case/twoip`` example assets instead of +synthetic inputs so the coverage matches the upstream demo structure. +""" + +from __future__ import annotations + +import importlib.util +import json +import os +import re +import time +from functools import lru_cache +from pathlib import Path +from typing import NamedTuple + +import numpy as np +import pytest +import soundfile as sf +from PIL import Image + +from tests.helpers.mark import hardware_test +from tests.helpers.runtime import OmniRunner +from vllm_omni.diffusion.data import DiffusionParallelConfig +from vllm_omni.inputs.data import OmniDiffusionSamplingParams + +os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1" + +pytestmark = [pytest.mark.full_model, pytest.mark.diffusion, pytest.mark.cache, pytest.mark.slow] + +REPO_ROOT = Path(__file__).resolve().parents[3] +DEFAULT_MODEL_PATH = REPO_ROOT / "dreamid_omni" +MODEL_PATH_ENV = "DREAMID_OMNI_MODEL_PATH" +EXAMPLE_ROOT_ENV = "DREAMID_OMNI_EXAMPLE_ROOT" +NUM_INFERENCE_STEPS = 45 + +CACHE_CONFIG = { + "Fn_compute_blocks": 1, + "Bn_compute_blocks": 0, + "max_warmup_steps": 4, + "max_cached_steps": 20, + "residual_diff_threshold": 0.24, + "max_continuous_cached_steps": 3, + "enable_taylorseer": False, + "taylorseer_order": 1, + "scm_steps_mask_policy": None, + "scm_steps_policy": "dynamic", +} + + +class DreamIDGenerationResult(NamedTuple): + video: np.ndarray + audio: np.ndarray + fps: int + duration_s: float + elapsed_s: float + + +class DreamIDExampleCase(NamedTuple): + prompt: str + image_paths: tuple[Path, ...] + audio_paths: tuple[Path, ...] + + +def _require_model_path() -> str: + model_path = os.environ.get(MODEL_PATH_ENV, str(DEFAULT_MODEL_PATH)) + if not Path(model_path).exists(): + pytest.skip(f"DreamID-Omni model bundle not found at {model_path}. Set {MODEL_PATH_ENV}.") + + if importlib.util.find_spec("dreamid_omni") is None: + pytest.skip( + "dreamid_omni dependency package is not importable. " + "Run examples/offline_inference/x_to_video_audio/download_dreamid_omni.py first." + ) + + return model_path + + +def _dreamid_repo_root_candidates() -> list[Path]: + candidates: list[Path] = [] + example_root = os.environ.get(EXAMPLE_ROOT_ENV) + if example_root: + candidates.append(Path(example_root).expanduser().resolve()) + + spec = importlib.util.find_spec("dreamid_omni") + if spec is not None and spec.origin: + candidates.append(Path(spec.origin).resolve().parents[1] / "test_case" / "twoip") + + return candidates + + +@lru_cache(maxsize=1) +def _load_example_case() -> DreamIDExampleCase: + for candidate_root in _dreamid_repo_root_candidates(): + caption_path = candidate_root / "captions" / "20.json" + image_paths = ( + candidate_root / "imgs" / "20" / "0.png", + candidate_root / "imgs" / "20" / "1.png", + ) + audio_paths = ( + candidate_root / "audios" / "20" / "0.wav", + candidate_root / "audios" / "20" / "1.wav", + ) + if caption_path.exists() and all(path.exists() for path in image_paths + audio_paths): + prompt = _load_prompt_from_json(caption_path) + return DreamIDExampleCase( + prompt=prompt, + image_paths=image_paths, + audio_paths=audio_paths, + ) + + searched = ", ".join(str(path) for path in _dreamid_repo_root_candidates()) or "" + pytest.skip( + "DreamID-Omni official example assets not found. " + f"Set {EXAMPLE_ROOT_ENV} to the local 'test_case/twoip' directory. " + f"Searched: {searched}" + ) + + +def _load_prompt_from_json(path: Path) -> str: + raw_text = path.read_text(encoding="utf-8").strip() + parsed = json.loads(raw_text) + if isinstance(parsed, str): + prompt = parsed + elif isinstance(parsed, dict) and isinstance(parsed.get("content"), str): + prompt = parsed["content"] + else: + raise AssertionError(f"Unexpected DreamID caption format in {path}") + + prompt = re.sub( + r"\[SPEAKER_TIMESTAMPS_START\].*?\[SPEAKER_TIMESTAMPS_END\]", + "", + prompt, + flags=re.DOTALL, + ).strip() + prompt = re.sub( + r"\[AUDIO_DESCRIPTION_START].*?\[AUDIO_DESCRIPTION_END]", + "", + prompt, + flags=re.DOTALL, + ).strip() + prompt = re.sub(r"\[[A-Z_]+\]", "", prompt) + prompt = re.sub(r"\n\s*\n", "\n", prompt).strip() + return prompt + + +def _load_example_images(example_case: DreamIDExampleCase) -> list[Image.Image]: + images: list[Image.Image] = [] + for path in example_case.image_paths: + with Image.open(path) as image: + images.append(image.convert("RGB")) + return images + + +def _load_example_audios(example_case: DreamIDExampleCase) -> list[np.ndarray]: + audios: list[np.ndarray] = [] + for path in example_case.audio_paths: + audio_array, sample_rate = sf.read(str(path), dtype="float32") + audio_np = np.asarray(audio_array, dtype=np.float32) + if audio_np.ndim == 2: + audio_np = np.mean(audio_np, axis=1) + audio_np = audio_np[int(sample_rate * 1) : int(sample_rate * 3)] + audios.append(audio_np.reshape(-1)) + return audios + + +def _sampling_params(num_inference_steps: int) -> OmniDiffusionSamplingParams: + return OmniDiffusionSamplingParams( + height=704, + width=1280, + num_inference_steps=num_inference_steps, + seed=42, + extra_args={ + "solver_name": "unipc", + "shift": 5.0, + }, + ) + + +def _request_payload(example_case: DreamIDExampleCase) -> dict[str, object]: + return { + "prompt": example_case.prompt, + "video_negative_prompt": "jitter, bad hands, blur, distortion", + "audio_negative_prompt": "robotic, muffled, echo, distorted", + "multi_modal_data": { + "image": _load_example_images(example_case), + "audio": _load_example_audios(example_case), + }, + } + + +def _extract_generation(outputs: list[object], elapsed_s: float) -> DreamIDGenerationResult: + """Extract video and audio from raw Omni outputs""" + assert outputs, "DreamID-Omni returned no outputs" + + result = outputs[0] + assert hasattr(result, "images") and result.images, "not return video frames" + + video = result.images[0] + assert isinstance(video, np.ndarray), f"Expected video ndarray, got {type(video)}" + assert video.ndim == 4, f"Expected 4D DreamID video tensor, got {video.shape}" + + multimodal_output = getattr(result, "multimodal_output", None) or {} + assert "audio" in multimodal_output, "not return audio" + audio = np.asarray(multimodal_output["audio"], dtype=np.float32).reshape(-1) + assert audio.size > 0, "returned empty audio" + + audio_sample_rate = int(multimodal_output.get("audio_sample_rate", 16000)) + fps = multimodal_output.get("fps", 24) + duration_s = float(audio.size) / float(audio_sample_rate) + + generation = DreamIDGenerationResult( + video=video.astype(np.float32, copy=False), + audio=audio, + fps=fps, + duration_s=duration_s, + elapsed_s=elapsed_s, + ) + return generation + + +def _run_generation( + *, + model_path: str, + example_case: DreamIDExampleCase, + use_cache_dit: bool, +) -> DreamIDGenerationResult: + runner_kwargs = { + "model_type": "dreamid-omni", + "parallel_config": DiffusionParallelConfig(cfg_parallel_size=1), + } + if use_cache_dit: + runner_kwargs["cache_backend"] = "cache_dit" + runner_kwargs["cache_config"] = CACHE_CONFIG + + with OmniRunner(model_path, **runner_kwargs) as runner: + warmup_payload = _request_payload(example_case) + _ = runner.omni.generate(warmup_payload, _sampling_params(2)) + + elapsed_samples: list[float] = [] + extracted_result: DreamIDGenerationResult | None = None + + request_payload = _request_payload(example_case) + start = time.perf_counter() + outputs = runner.omni.generate(request_payload, _sampling_params(NUM_INFERENCE_STEPS)) + elapsed_s = time.perf_counter() - start + elapsed_samples.append(elapsed_s) + if extracted_result is None: + extracted_result = _extract_generation(outputs, elapsed_s) + + assert extracted_result is not None + median_elapsed_s = float(np.median(np.asarray(elapsed_samples, dtype=np.float64))) + return extracted_result._replace(elapsed_s=median_elapsed_s) + + +def _video_mae(a: np.ndarray, b: np.ndarray) -> float: + assert a.shape == b.shape, f"Video shapes differ: {a.shape} vs {b.shape}" + return float(np.mean(np.abs(a - b))) + + +def _audio_rms(audio: np.ndarray) -> float: + audio = np.asarray(audio, dtype=np.float32).reshape(-1) + return float(np.sqrt(np.mean(audio**2))) + + +def _audio_rms_ratio(a: DreamIDGenerationResult, b: DreamIDGenerationResult) -> float: + rms_a = _audio_rms(a.audio) + rms_b = _audio_rms(b.audio) + return rms_b / max(rms_a, 1e-6) + + +@hardware_test(res={"cuda": "H100"}) +def test_dreamid_omni_cache_dit_e2e() -> None: + model_path = _require_model_path() + example_case = _load_example_case() + + baseline = _run_generation(model_path=model_path, example_case=example_case, use_cache_dit=False) + cached = _run_generation(model_path=model_path, example_case=example_case, use_cache_dit=True) + + video_mae = _video_mae(baseline.video, cached.video) + audio_rms_ratio = _audio_rms_ratio(baseline, cached) + speedup = baseline.elapsed_s / cached.elapsed_s if cached.elapsed_s > 0 else float("inf") + + assert video_mae <= 0.15, f"video drift too much: {video_mae:.6f}" + assert 0.4 <= audio_rms_ratio <= 2.5, f"audio quality regressed too much: rms_ratio={audio_rms_ratio:.6f}" + assert speedup >= 1.05, f"cache-dit improve not enough: {speedup:.3f}x" diff --git a/vllm_omni/diffusion/cache/cache_dit_backend.py b/vllm_omni/diffusion/cache/cache_dit_backend.py index 3f8229722..7c27c31eb 100644 --- a/vllm_omni/diffusion/cache/cache_dit_backend.py +++ b/vllm_omni/diffusion/cache/cache_dit_backend.py @@ -1320,6 +1320,74 @@ def enable_cache_for_glm_image(pipeline: Any, cache_config: Any) -> Callable[[in return refresh_cache_context +def enable_cache_for_dreamid_omni(pipeline: Any, cache_config: Any) -> Callable[[int], None]: + """Enable cache-dit for DreamID-Omni fused pipeline. + + Args: + pipeline: The DreamIDOmni pipeline instance. + cache_config: DiffusionCacheConfig instance with cache configuration. + + Returns: + A refresh function that can be called with a new ``num_inference_steps`` + to update the cache context for the pipeline. + """ + transformer = getattr(pipeline, "transformer", None) + fused_blocks = getattr(transformer, "fused_blocks", None) + if transformer is None or fused_blocks is None: + raise ValueError("DreamIDOmniPipeline cache-dit expects pipeline.transformer.fused_blocks.") + + db_cache_config = _build_db_cache_config(cache_config) + + calibrator = None + if cache_config.enable_taylorseer: + taylorseer_order = cache_config.taylorseer_order + calibrator = TaylorSeerCalibratorConfig(taylorseer_order=taylorseer_order) + logger.info(f"TaylorSeer enabled with order={taylorseer_order}") + + modifier = ParamsModifier( + cache_config=db_cache_config, + calibrator_config=calibrator, + ) + + logger.info( + f"Enabling cache-dit on DreamID-Omni fused blocks: " + f"Fn={db_cache_config.Fn_compute_blocks}, " + f"Bn={db_cache_config.Bn_compute_blocks}, " + f"W={db_cache_config.max_warmup_steps}, " + ) + + cache_dit.enable_cache( + BlockAdapter( + transformer=transformer, + blocks=fused_blocks, + forward_pattern=ForwardPattern.Pattern_0, + params_modifiers=[modifier], + has_separate_cfg=True, + ), + cache_config=db_cache_config, + ) + + def refresh_cache_context(pipeline: Any, num_inference_steps: int, verbose: bool = True) -> None: + transformer = pipeline.transformer + if cache_config.scm_steps_mask_policy is None: + cache_dit.refresh_context(transformer, num_inference_steps=num_inference_steps, verbose=verbose) + else: + cache_dit.refresh_context( + transformer, + cache_config=DBCacheConfig().reset( + num_inference_steps=num_inference_steps, + steps_computation_mask=cache_dit.steps_mask( + mask_policy=cache_config.scm_steps_mask_policy, + total_steps=num_inference_steps, + ), + steps_computation_policy=cache_config.scm_steps_policy, + ), + verbose=verbose, + ) + + return refresh_cache_context + + def enable_cache_for_ernie_image(pipeline: Any, cache_config: Any) -> Callable[[int], None]: """Enable cache-dit for ERNIE-Image pipeline. @@ -1521,6 +1589,7 @@ CUSTOM_DIT_ENABLERS.update( "BagelPipeline": enable_cache_for_bagel, "GlmImagePipeline": enable_cache_for_glm_image, "Flux2Pipeline": enable_cache_for_flux2, + "DreamIDOmniPipeline": enable_cache_for_dreamid_omni, "ErnieImagePipeline": enable_cache_for_ernie_image, "HunyuanVideo15Pipeline": enable_cache_for_hunyuan_video_15, "HunyuanVideo15I2VPipeline": enable_cache_for_hunyuan_video_15, diff --git a/vllm_omni/diffusion/models/dreamid_omni/fusion.py b/vllm_omni/diffusion/models/dreamid_omni/fusion.py index a7ba745e9..d0b00bc5b 100644 --- a/vllm_omni/diffusion/models/dreamid_omni/fusion.py +++ b/vllm_omni/diffusion/models/dreamid_omni/fusion.py @@ -128,8 +128,8 @@ class FusedBlock(nn.Module): def forward( self, - vid, - audio, + hidden_states, + encoder_hidden_states, attn: Attention, vid_e, vid_seq_lens, @@ -148,6 +148,8 @@ class FusedBlock(nn.Module): audio_ref_lengths, audio_freqs_scaling, ): + vid = hidden_states + audio = encoder_hidden_states vid_block = self.vid_block audio_block = self.audio_block From 748470c1fb2db8fba95eb7d4b2498656097865b2 Mon Sep 17 00:00:00 2001 From: amy-why-3459 Date: Sun, 24 May 2026 00:05:55 +0800 Subject: [PATCH 016/112] [BugFix] code2wav supports disabling CUDA graph. (#3732) Signed-off-by: amy-why-3459 Co-authored-by: Hongsheng Liu --- tests/test_config_factory.py | 2 +- vllm_omni/deploy/qwen3_omni_moe.yaml | 2 +- vllm_omni/deploy/qwen3_tts.yaml | 6 +- .../models/qwen3_omni/qwen3_omni.py | 43 ++++++---- .../models/qwen3_omni/qwen3_omni_code2wav.py | 4 +- .../models/qwen3_tts/qwen3_tts_code2wav.py | 80 +++++++++++++------ 6 files changed, 91 insertions(+), 46 deletions(-) diff --git a/tests/test_config_factory.py b/tests/test_config_factory.py index be3a012cc..9ac3d859c 100644 --- a/tests/test_config_factory.py +++ b/tests/test_config_factory.py @@ -1453,7 +1453,7 @@ class TestPlatformOverrides: assert deploy.stages[0].tensor_parallel_size == 2 assert deploy.stages[0].devices == "0,1" # Stage 2 unaffected fields stay at base - assert deploy.stages[2].enforce_eager is True + assert deploy.stages[2].enforce_eager is False def test_xpu_overrides(self): deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "qwen3_omni_moe.yaml" diff --git a/vllm_omni/deploy/qwen3_omni_moe.yaml b/vllm_omni/deploy/qwen3_omni_moe.yaml index 445437c0f..b77f4caf2 100644 --- a/vllm_omni/deploy/qwen3_omni_moe.yaml +++ b/vllm_omni/deploy/qwen3_omni_moe.yaml @@ -56,7 +56,7 @@ stages: max_num_batched_tokens: 51200 max_num_seqs: 64 gpu_memory_utilization: 0.1 - enforce_eager: true + enforce_eager: false trust_remote_code: true enable_prefix_caching: false async_scheduling: false diff --git a/vllm_omni/deploy/qwen3_tts.yaml b/vllm_omni/deploy/qwen3_tts.yaml index 80c316849..0c976d090 100644 --- a/vllm_omni/deploy/qwen3_tts.yaml +++ b/vllm_omni/deploy/qwen3_tts.yaml @@ -9,8 +9,8 @@ # * Stage 1 (code2wav) sets max_num_batched_tokens=65536 for correctness: # codec prefill length (Q * num_frames) exceeds the 32k default. # * Stage 0 does not set enforce_eager — talker runs cudagraph by default. -# Stage 1 sets true because its codec generation loop is not -# cudagraph-compatible. NPU platform flips stage 0 to true where +# Stage 1 defaults to false so Code2Wav can use its inner cudagraph; +# set true to disable that path in eager mode. NPU platform flips stage 0 to true where # cudagraph is not yet verified. async_chunk: true @@ -56,7 +56,7 @@ stages: - stage_id: 1 max_num_seqs: 10 gpu_memory_utilization: 0.3 - enforce_eager: true + enforce_eager: false trust_remote_code: true enable_prefix_caching: false async_scheduling: true diff --git a/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py b/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py index 3c658fc3f..ba924a1ea 100644 --- a/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py +++ b/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py @@ -420,13 +420,16 @@ class Qwen3OmniMoeForConditionalGeneration( else: codes = input_ids.reshape(1, 16, -1) else: - logger.warning( - ( - "Input_ids length: %s is not divisible by 16, padding " - "with zeros. This should only happen in warm up." - ), - input_ids.shape[0], - ) + if seq_token_counts is None: + logger.debug( + "Code2Wav warmup input length %s is not divisible by 16; padding with zeros.", + input_ids.shape[0], + ) + else: + logger.warning_once( + "Code2Wav input length is not divisible by 16; padding with zeros. " + "This is expected only during cudagraph warmup." + ) input_ids_flatten = input_ids.reshape(-1) input_ids_flatten = torch.cat( [ @@ -1283,6 +1286,22 @@ class Qwen3OmniMoeForConditionalGeneration( left_frames = int(extra.get("codec_left_context_frames", 0) or 0) return chunk_frames, left_frames + def _maybe_enable_code2wav_cudagraph(self) -> None: + """Enable the inner Code2Wav CUDA graph unless this stage runs in eager mode.""" + if not self.code2wav or not hasattr(self.code2wav, "enable_cudagraph"): + return + + model_cfg = getattr(self.vllm_config, "model_config", None) + if getattr(model_cfg, "enforce_eager", False): + logger.info("Code2Wav CUDA Graph disabled because enforce_eager is set") + return + + chunk_frames, left_frames = self._get_codec_frame_config() + self.code2wav.enable_cudagraph( + codec_chunk_frames=chunk_frames, + codec_left_context_frames=left_frames, + ) + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: """Load weights for all components of the omni model.""" loaded_weights = set() @@ -1319,15 +1338,11 @@ class Qwen3OmniMoeForConditionalGeneration( code2wav_loaded = add_prefix_to_loaded_weights(code2wav_loaded, "code2wav") loaded_weights.update(code2wav_loaded) - # Precompute SnakeBeta caches and enable CUDA graph for Code2Wav decoder + # Precompute SnakeBeta caches; Code2Wav CUDA graph follows the stage's + # enforce_eager setting, the same switch vLLM uses for outer graphs. try: self.code2wav.precompute_snake_caches() - if hasattr(self.code2wav, "enable_cudagraph"): - chunk_frames, left_frames = self._get_codec_frame_config() - self.code2wav.enable_cudagraph( - codec_chunk_frames=chunk_frames, - codec_left_context_frames=left_frames, - ) + self._maybe_enable_code2wav_cudagraph() except Exception: logger.warning( "Failed to enable CUDA Graph for Code2Wav; falling back to eager.", diff --git a/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_code2wav.py b/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_code2wav.py index 8bcba4340..2b1a7ccf2 100644 --- a/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_code2wav.py +++ b/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_code2wav.py @@ -297,9 +297,9 @@ class Qwen3OmniMoeCode2Wav(nn.Module): single tensor with shape ``[1, waveform_len]``. """ if not (left_context_size and seq_token_counts and len(left_context_size) == len(seq_token_counts)): - logger.warning( + logger.warning_once( "chunked_decode_streaming: missing/invalid left_context_size or seq_token_counts; " - "defaulting to left_context_size=zeros(len=codes.shape[0])." + "defaulting to left_context_size=zeros(len(codes)). This is expected during cudagraph warmup." ) left_context_size = [0] * codes.shape[0] # Decode chunk diff --git a/vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_code2wav.py b/vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_code2wav.py index 8c200141a..b46018e36 100644 --- a/vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_code2wav.py +++ b/vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_code2wav.py @@ -111,6 +111,56 @@ class Qwen3TTSCode2Wav(nn.Module): return [ids[boundaries[i] : boundaries[i + 1]] for i in range(len(boundaries) - 1)] return [ids] + def _maybe_enable_decoder_cudagraph( + self, + *, + device: torch.device, + codec_chunk_frames: int, + codec_left_context_frames: int, + decode_cudagraph_capture_sizes: list[int] | None, + decode_cudagraph_batch_sizes: list[int] | None, + decode_cudagraph_extra_capture_shapes: list[tuple[int, int]] | None, + decode_compile_shapes: list[tuple[int, int]] | None, + ) -> None: + """Enable inner Code2Wav CUDA graph unless stage is enforce_eager.""" + if not hasattr(self.decoder, "enable_cudagraph") or device.type != "cuda": + return + + model_cfg = getattr(self.vllm_config, "model_config", None) + if getattr(model_cfg, "enforce_eager", False): + logger.info("Qwen3-TTS Code2Wav CUDA Graph disabled because enforce_eager is set") + return + + if ( + codec_chunk_frames > 0 + and codec_left_context_frames > 0 + and self._decoder_sliding_window + and codec_left_context_frames < self._decoder_sliding_window + ): + logger.warning( + "Qwen3-TTS streaming codec_left_context_frames=%d " + "is smaller than decoder sliding_window=%d; " + "chunk-boundary distortion may occur. " + "Increase codec_left_context_frames to at least " + "%d for streaming.", + codec_left_context_frames, + self._decoder_sliding_window, + self._decoder_sliding_window, + ) + + self.decoder.enable_cudagraph( + capture_sizes=decode_cudagraph_capture_sizes, + capture_batch_sizes=decode_cudagraph_batch_sizes, + extra_capture_shapes=decode_cudagraph_extra_capture_shapes, + compile_shapes=decode_compile_shapes, + device=device, + codec_chunk_frames=codec_chunk_frames, + codec_left_context_frames=codec_left_context_frames, + decode_chunk_size=self._decode_chunk_frames, + decode_left_context=self._decode_left_context_frames, + ) + logger.info("Code2Wav decoder CUDA Graph enabled") + def _get_decode_batch_bucket_frames(self, actual_frames: int) -> int: for bucket_frames in self._decode_batch_bucket_frames: if actual_frames <= bucket_frames: @@ -581,35 +631,15 @@ class Qwen3TTSCode2Wav(nn.Module): if hasattr(self.decoder, "enable_cudagraph") and device.type == "cuda": try: - if ( - codec_chunk_frames > 0 - and codec_left_context_frames > 0 - and self._decoder_sliding_window - and codec_left_context_frames < self._decoder_sliding_window - ): - logger.warning( - "Qwen3-TTS streaming codec_left_context_frames=%d " - "is smaller than decoder sliding_window=%d; " - "chunk-boundary distortion may occur. " - "Increase codec_left_context_frames to at least " - "%d for streaming.", - codec_left_context_frames, - self._decoder_sliding_window, - self._decoder_sliding_window, - ) - - self.decoder.enable_cudagraph( - capture_sizes=decode_cudagraph_capture_sizes, - capture_batch_sizes=decode_cudagraph_batch_sizes, - extra_capture_shapes=decode_cudagraph_extra_capture_shapes, - compile_shapes=decode_compile_shapes, + self._maybe_enable_decoder_cudagraph( device=device, codec_chunk_frames=codec_chunk_frames, codec_left_context_frames=codec_left_context_frames, - decode_chunk_size=self._decode_chunk_frames, - decode_left_context=self._decode_left_context_frames, + decode_cudagraph_capture_sizes=decode_cudagraph_capture_sizes, + decode_cudagraph_batch_sizes=decode_cudagraph_batch_sizes, + decode_cudagraph_extra_capture_shapes=decode_cudagraph_extra_capture_shapes, + decode_compile_shapes=decode_compile_shapes, ) - logger.info("Code2Wav decoder CUDA Graph enabled") except Exception: logger.warning( "Failed to enable CUDA Graph for Code2Wav decoder", From 28ce618f0e61a564447d9a97323edfc55d5dc585 Mon Sep 17 00:00:00 2001 From: BeatSeat <137616995+BeatSeat@users.noreply.github.com> Date: Sun, 24 May 2026 05:42:26 +0800 Subject: [PATCH 017/112] [Model] Add GLM-TTS text-to-speech model support (#3141) Signed-off-by: BeatSeat Signed-off-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com> Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com> --- .buildkite/test-merge.yml | 41 + docs/models/supported_models.md | 1 + .../offline_inference/text_to_speech.md | 34 + .../examples/online_serving/text_to_speech.md | 56 + .../text_to_speech/README.md | 30 + .../text_to_speech/glm_tts/end2end.py | 194 +++ .../online_serving/text_to_speech/README.md | 40 + .../text_to_speech/glm_tts/gradio_demo.py | 291 ++++ .../glm_tts/openai_speech_client.py | 173 ++ .../text_to_speech/glm_tts/run_gradio_demo.sh | 48 + .../text_to_speech/glm_tts/run_server.sh | 38 + recipes/README.md | 1 + recipes/zai-org/GLM-TTS.md | 105 ++ tests/assets/glm_tts/jiayan_zh.wav | Bin 0 -> 356734 bytes tests/e2e/offline_inference/test_glm_tts.py | 148 ++ tests/e2e/online_serving/test_glm_tts.py | 131 ++ .../openai_api/test_serving_speech.py | 78 + .../models/test_glm_tts_prompt_len.py | 351 ++++ .../test_glm_tts_async_chunk.py | 278 ++++ vllm_omni/config/pipeline_registry.py | 4 + vllm_omni/deploy/glm_tts.yaml | 83 + vllm_omni/engine/arg_utils.py | 11 + .../entrypoints/openai/serving_speech.py | 192 ++- vllm_omni/entrypoints/utils.py | 15 +- .../models/common/nucleus_ras_sampling.py | 118 ++ .../models/common/whisper_vq.py | 291 ++++ .../model_executor/models/glm_tts/__init__.py | 18 + .../model_executor/models/glm_tts/glm_tts.py | 1423 +++++++++++++++++ .../models/glm_tts/glm_tts_dit.py | 650 ++++++++ .../models/glm_tts/glm_tts_dit_wrapper.py | 1301 +++++++++++++++ .../model_executor/models/glm_tts/pipeline.py | 46 + .../models/glm_tts/text_frontend.py | 393 +++++ .../model_executor/models/glm_tts/vocoder.py | 228 +++ .../models/glm_tts/voice_clone.py | 299 ++++ vllm_omni/model_executor/models/registry.py | 6 + .../stage_input_processors/glm_tts.py | 362 +++++ .../transformers_utils/configs/__init__.py | 3 + .../transformers_utils/configs/glm_tts.py | 94 ++ 38 files changed, 7568 insertions(+), 7 deletions(-) create mode 100644 examples/offline_inference/text_to_speech/glm_tts/end2end.py create mode 100644 examples/online_serving/text_to_speech/glm_tts/gradio_demo.py create mode 100644 examples/online_serving/text_to_speech/glm_tts/openai_speech_client.py create mode 100644 examples/online_serving/text_to_speech/glm_tts/run_gradio_demo.sh create mode 100644 examples/online_serving/text_to_speech/glm_tts/run_server.sh create mode 100644 recipes/zai-org/GLM-TTS.md create mode 100644 tests/assets/glm_tts/jiayan_zh.wav create mode 100644 tests/e2e/offline_inference/test_glm_tts.py create mode 100644 tests/e2e/online_serving/test_glm_tts.py create mode 100644 tests/model_executor/models/test_glm_tts_prompt_len.py create mode 100644 tests/model_executor/stage_input_processors/test_glm_tts_async_chunk.py create mode 100644 vllm_omni/deploy/glm_tts.yaml create mode 100644 vllm_omni/model_executor/models/common/nucleus_ras_sampling.py create mode 100644 vllm_omni/model_executor/models/common/whisper_vq.py create mode 100644 vllm_omni/model_executor/models/glm_tts/__init__.py create mode 100644 vllm_omni/model_executor/models/glm_tts/glm_tts.py create mode 100644 vllm_omni/model_executor/models/glm_tts/glm_tts_dit.py create mode 100644 vllm_omni/model_executor/models/glm_tts/glm_tts_dit_wrapper.py create mode 100644 vllm_omni/model_executor/models/glm_tts/pipeline.py create mode 100644 vllm_omni/model_executor/models/glm_tts/text_frontend.py create mode 100644 vllm_omni/model_executor/models/glm_tts/vocoder.py create mode 100644 vllm_omni/model_executor/models/glm_tts/voice_clone.py create mode 100644 vllm_omni/model_executor/stage_input_processors/glm_tts.py create mode 100644 vllm_omni/transformers_utils/configs/glm_tts.py diff --git a/.buildkite/test-merge.yml b/.buildkite/test-merge.yml index 9e5027272..5b8d7e104 100644 --- a/.buildkite/test-merge.yml +++ b/.buildkite/test-merge.yml @@ -248,6 +248,47 @@ steps: path: /mnt/hf-cache type: DirectoryOrCreate + - label: "TTS · GLM-TTS Test" + timeout_in_minutes: 20 + commands: + - | + timeout 20m bash -c " + export VLLM_LOGGING_LEVEL=DEBUG + pytest -s -v tests/e2e/online_serving/test_glm_tts.py tests/e2e/offline_inference/test_glm_tts.py -m 'advanced_model and cuda' --run-level 'advanced_model' + " + agents: + queue: "mithril-h100-pool" + plugins: + - kubernetes: + podSpec: + containers: + - image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT + resources: + limits: + nvidia.com/gpu: 1 + volumeMounts: + - name: devshm + mountPath: /dev/shm + - name: hf-cache + mountPath: /root/.cache/huggingface + env: + - name: HF_HOME + value: /root/.cache/huggingface + - name: HF_TOKEN + valueFrom: + secretKeyRef: + name: hf-token-secret + key: token + nodeSelector: + node.kubernetes.io/instance-type: gpu-h100-sxm + volumes: + - name: devshm + emptyDir: + medium: Memory + - name: hf-cache + hostPath: + path: /mnt/hf-cache + type: DirectoryOrCreate - label: "Omni · Qwen3-Omni Test" timeout_in_minutes: 30 diff --git a/docs/models/supported_models.md b/docs/models/supported_models.md index b0637a5d7..d26a579ed 100644 --- a/docs/models/supported_models.md +++ b/docs/models/supported_models.md @@ -58,6 +58,7 @@ th { | `Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-CustomVoice | `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | | `Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-VoiceDesign | `Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | | `Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-Base | `Qwen/Qwen3-TTS-12Hz-0.6B-Base` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | +| `GLMTTSForConditionalGeneration` | GLM-TTS | `zai-org/GLM-TTS` | ✅︎ | | | | | `NextStep11Pipeline` | NextStep-1.1 | `stepfun-ai/NextStep-1.1` | ✅︎ | ✅︎ | | ✅︎ | | `MiMoAudioModel` | MiMo-Audio-7B-Instruct | `XiaomiMiMo/MiMo-Audio-7B-Instruct` | ✅︎ | ✅︎ | | | | `MiMoV2ASRForCausalLM` | MiMo-V2.5-ASR | `XiaomiMiMo/MiMo-V2.5-ASR` | ✅︎ | ✅︎ | | | diff --git a/docs/user_guide/examples/offline_inference/text_to_speech.md b/docs/user_guide/examples/offline_inference/text_to_speech.md index 7ad3d2d40..7937b1180 100644 --- a/docs/user_guide/examples/offline_inference/text_to_speech.md +++ b/docs/user_guide/examples/offline_inference/text_to_speech.md @@ -19,6 +19,7 @@ list of supported architectures across all modalities, see | VoxCPM2 | `openbmb/VoxCPM2` | single (native AR) | ✓ | — | continuation (`--ref-audio` + `--ref-text`) | 48 kHz | | CosyVoice3 | `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` | 2 (talker + code2wav) | ✓ | ✓ (`async_chunk: true` default) | — | 22.05 kHz | | Fish Speech S2 Pro | `fishaudio/s2-pro` | dual-AR | ✓ | ✓ (`--streaming`) | — | 44.1 kHz | +| GLM-TTS | `zai-org/GLM-TTS` | 2 (AR + DiT) | ✓ (required) | ✓ | — | 24 kHz | | OmniVoice | `k2-fsa/OmniVoice` | 2 (gen + dec) | ✓ | — | voice design (`--instruct`), language (`--lang`) | 24 kHz | | Qwen3-TTS | `Qwen/Qwen3-TTS-12Hz-1.7B-{CustomVoice,VoiceDesign,Base}` | 2 (talker + code2wav) | ✓ (Base) | ✓ | 3 task variants (`--query-type`) | 24 kHz | | Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | varies | ✓ | ✓ | voice presets (`--voice`) | 24 kHz | @@ -158,6 +159,35 @@ Streaming requires `async_chunk: true` in the stage config. --- +## GLM-TTS + +2-stage TTS pipeline (AR + DiT flow-matching) at 24 kHz. Every request requires reference audio and its transcript for zero-shot voice cloning. + +### Quick start +```bash +python examples/offline_inference/text_to_speech/glm_tts/end2end.py \ + --model zai-org/GLM-TTS \ + --text "你好,这是语音合成测试。" \ + --ref-audio /path/to/reference.wav \ + --ref-text "这是参考音频的文本内容。" \ + --output-dir ./output +``` + +### Architecture +``` +Text → [Stage 0: AR] → Speech Tokens → [Stage 1: DiT + HiFT] → Audio (24 kHz) + (Llama-based) (32k vocab) (Flow Matching) +``` + +### Notes +- `--ref-audio` and `--ref-text` are **required** together; GLM-TTS does not support text-only synthesis. +- Reference audio should be 3-10 seconds. +- First run may be slow due to lazy loading of WhisperVQ tokenizer and CampPlus ONNX speaker embedder. +- Default sampling: temperature=1.0, top_k=25, top_p=0.8 (RAS method). +- The `--model` path should point to the repository root (not `llm/` subdirectory). + +--- + ## OmniVoice Zero-shot multilingual TTS supporting 600+ languages, with three modes (auto / clone / design). @@ -313,6 +343,10 @@ Available voice presets are listed on the HF model card (`mistralai/Voxtral-4B-T ``````py --8<-- "examples/offline_inference/text_to_speech/fish_speech/end2end.py" `````` +??? abstract "glm_tts/end2end.py" + ``````py + --8<-- "examples/offline_inference/text_to_speech/glm_tts/end2end.py" + `````` ??? abstract "omnivoice/end2end.py" ``````py --8<-- "examples/offline_inference/text_to_speech/omnivoice/end2end.py" diff --git a/docs/user_guide/examples/online_serving/text_to_speech.md b/docs/user_guide/examples/online_serving/text_to_speech.md index ff22dd0fd..37bfde7ad 100644 --- a/docs/user_guide/examples/online_serving/text_to_speech.md +++ b/docs/user_guide/examples/online_serving/text_to_speech.md @@ -19,6 +19,7 @@ For the full list of supported architectures across all modalities, see |---|---|---|---|---|---| | CosyVoice3 | `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | — | | Fish Speech S2 Pro | `fishaudio/s2-pro` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | ✓ | +| GLM-TTS | `zai-org/GLM-TTS` | ✓ (`ref_audio`+`ref_text`, required) | ✓ (PCM stream) | — | ✓ | | OmniVoice | `k2-fsa/OmniVoice` | (offline only) | — | — | — | | Qwen3-TTS | `Qwen/Qwen3-TTS-12Hz-1.7B-{CustomVoice,VoiceDesign,Base}` | ✓ (Base) | ✓ (PCM + WebSocket) | ✓ (presets + `/v1/audio/voices` upload) | ✓ (standard + FastRTC) | | VoxCPM2 | `openbmb/VoxCPM2` | ✓ | ✓ (AudioWorklet via gradio) | — | ✓ | @@ -192,6 +193,45 @@ python fish_speech/gradio_demo.py --api-base http://localhost:8091 # if server --- +## GLM-TTS + +2-stage TTS (AR + DiT flow-matching) at 24 kHz. Every request requires `ref_audio` + `ref_text`. + +### Launch +```bash +vllm serve zai-org/GLM-TTS --omni --trust-remote-code --port 8091 +# or: +bash examples/online_serving/text_to_speech/glm_tts/run_server.sh /path/to/GLM-TTS +``` + +### Sending requests +```bash +# Voice cloning (required) +python examples/online_serving/text_to_speech/glm_tts/openai_speech_client.py \ + --text "你好,这是语音克隆测试。" \ + --ref-audio file:///path/to/ref.wav \ + --ref-text "这是参考音频的文本内容。" + +# Custom format +python examples/online_serving/text_to_speech/glm_tts/openai_speech_client.py \ + --text "Hello, this is a voice cloning test." \ + --ref-audio file:///path/to/ref.wav \ + --ref-text "Transcript of the reference audio." \ + --response-format mp3 -o output.mp3 +``` + +### Gradio demo +```bash +bash examples/online_serving/text_to_speech/glm_tts/run_gradio_demo.sh +``` + +### Notes +- Output: 24 kHz mono WAV via HiFT vocoder. +- `ref_audio` + `ref_text` are **required** together on every request. Reference audio should be 3-10 seconds. +- Voice cloning feature extraction (WhisperVQ, CampPlus, mel) runs on the model side — no external dependency on the serving layer. + +--- + ## OmniVoice Zero-shot multilingual TTS (600+ languages). Online serving currently exposes **auto voice** only; voice cloning and voice design are available offline. @@ -419,6 +459,22 @@ The demo handles voice-preset selection and reference-audio upload. `voxtral_tts ``````py --8<-- "examples/online_serving/text_to_speech/fish_speech/speech_client.py" `````` +??? abstract "glm_tts/gradio_demo.py" + ``````py + --8<-- "examples/online_serving/text_to_speech/glm_tts/gradio_demo.py" + `````` +??? abstract "glm_tts/openai_speech_client.py" + ``````py + --8<-- "examples/online_serving/text_to_speech/glm_tts/openai_speech_client.py" + `````` +??? abstract "glm_tts/run_gradio_demo.sh" + ``````sh + --8<-- "examples/online_serving/text_to_speech/glm_tts/run_gradio_demo.sh" + `````` +??? abstract "glm_tts/run_server.sh" + ``````sh + --8<-- "examples/online_serving/text_to_speech/glm_tts/run_server.sh" + `````` ??? abstract "omnivoice/run_server.sh" ``````sh --8<-- "examples/online_serving/text_to_speech/omnivoice/run_server.sh" diff --git a/examples/offline_inference/text_to_speech/README.md b/examples/offline_inference/text_to_speech/README.md index 722b9eeb3..0bf0efc4e 100644 --- a/examples/offline_inference/text_to_speech/README.md +++ b/examples/offline_inference/text_to_speech/README.md @@ -16,6 +16,7 @@ list of supported architectures across all modalities, see |---|---|---|---|---|---|---| | CosyVoice3 | `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` | 2 (talker + code2wav) | ✓ | ✓ | — | 24 kHz | | Fish Speech S2 Pro | `fishaudio/s2-pro` | dual-AR | ✓ | ✓ | — | 44.1 kHz | +| GLM-TTS | `zai-org/GLM-TTS` | 2 (AR + DiT) | ✓ (required) | ✓ | — | 24 kHz | | Ming-flash-omni-TTS | `Jonathan1909/Ming-flash-omni-2.0` | single (talker only) | — (caption-controlled) | — | style / IP / basic captions | 44.1 kHz | | MOSS-TTS-Nano | `OpenMOSS-Team/MOSS-TTS-Nano` | single (AR + codec) | ✓ (required) | ✓ | voice_clone, continuation | 48 kHz | | OmniVoice | `k2-fsa/OmniVoice` | 2 (gen + dec) | ✓ | — | voice design, language hint | 24 kHz | @@ -92,6 +93,35 @@ Streaming is enabled by default via `async_chunk: true` in `vllm_omni/deploy/cos --- +## GLM-TTS + +2-stage TTS pipeline (AR + DiT flow-matching) at 24 kHz. Every request requires reference audio and its transcript for zero-shot voice cloning. + +### Quick start +```bash +python examples/offline_inference/text_to_speech/glm_tts/end2end.py \ + --model zai-org/GLM-TTS \ + --text "你好,这是语音合成测试。" \ + --ref-audio /path/to/reference.wav \ + --ref-text "这是参考音频的文本内容。" \ + --output-dir ./output +``` + +### Architecture +``` +Text → [Stage 0: AR] → Speech Tokens → [Stage 1: DiT + HiFT] → Audio (24 kHz) + (Llama-based) (32k vocab) (Flow Matching) +``` + +### Notes +- `--ref-audio` and `--ref-text` are **required** together; GLM-TTS does not support text-only synthesis. +- Reference audio should be 3-10 seconds. +- First run may be slow due to lazy loading of WhisperVQ tokenizer and CampPlus ONNX speaker embedder. +- Default sampling: temperature=1.0, top_k=25, top_p=0.8 (RAS method). +- The `--model` path should point to the repository root (not `llm/` subdirectory). + +--- + ## Fish Speech S2 Pro 4B dual-AR text-to-speech model from FishAudio with the DAC codec at 44.1 kHz. diff --git a/examples/offline_inference/text_to_speech/glm_tts/end2end.py b/examples/offline_inference/text_to_speech/glm_tts/end2end.py new file mode 100644 index 000000000..d1f19f8a5 --- /dev/null +++ b/examples/offline_inference/text_to_speech/glm_tts/end2end.py @@ -0,0 +1,194 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""GLM-TTS End-to-End Offline Inference Example. + +GLM-TTS is a two-stage TTS system: + - Stage 0 (AR): Llama-based model generates speech tokens from text + - Stage 1 (DiT): Flow matching model converts speech tokens to audio + +Usage: + # Sync two-stage (default) + python examples/offline_inference/text_to_speech/glm_tts/end2end.py \ + --model /path/to/GLM-TTS \ + --text "你好,这是一个语音合成测试。" \ + --ref-audio /path/to/reference.wav \ + --ref-text "参考音频的转录文本。" \ + --output-dir ./output + + # Async chunk mode (streaming DiT) + python examples/offline_inference/text_to_speech/glm_tts/end2end.py \ + --model /path/to/GLM-TTS --async-chunk \ + --text "你好,这是一个语音合成测试。" \ + --ref-audio /path/to/reference.wav \ + --ref-text "参考音频的转录文本。" \ + --output-dir ./output +""" + +import base64 +import io +import logging +import os +import tempfile +import time +from typing import Any +from urllib.request import urlopen + +import numpy as np +import soundfile as sf +import torch +import yaml + +os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" + +from vllm.utils.argparse_utils import FlexibleArgumentParser + +from vllm_omni import Omni +from vllm_omni.model_executor.models.glm_tts.glm_tts import build_glm_tts_prefill_metadata + +logger = logging.getLogger(__name__) + +DEFAULT_DEPLOY_CONFIG = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "..", + "..", + "vllm_omni", + "deploy", + "glm_tts.yaml", +) +SAMPLE_RATE = 24000 + + +def _load_ref_audio(ref_audio: str) -> tuple[torch.Tensor, int]: + """Load reference audio from file path, URL, or data URI.""" + if ref_audio.startswith(("http://", "https://")): + with urlopen(ref_audio, timeout=60) as response: + audio_obj: Any = io.BytesIO(response.read()) + elif ref_audio.startswith("data:"): + _, _, encoded = ref_audio.partition(",") + audio_obj = io.BytesIO(base64.b64decode(encoded)) + else: + audio_obj = ref_audio + wav_np, sr = sf.read(audio_obj, dtype="float32") + if wav_np.ndim > 1: + wav_np = wav_np.mean(axis=1) + return torch.from_numpy(wav_np), int(sr) + + +def _concat_audio(audio_val: Any) -> np.ndarray: + """Concatenate audio tensors from multimodal output.""" + if isinstance(audio_val, list): + tensors = [torch.as_tensor(t).float().reshape(-1) for t in audio_val if t is not None] + if not tensors: + return np.zeros((0,), dtype=np.float32) + return torch.cat(tensors, dim=-1).cpu().numpy().astype(np.float32, copy=False) + if isinstance(audio_val, torch.Tensor): + return audio_val.float().cpu().numpy().reshape(-1) + return np.asarray(audio_val, dtype=np.float32).reshape(-1) + + +def _extract_sample_rate(audio_mm: dict) -> int: + """Extract sample rate from multimodal output dict.""" + sr_raw = audio_mm.get("sr", SAMPLE_RATE) + if isinstance(sr_raw, list): + sr_raw = sr_raw[-1] if sr_raw else SAMPLE_RATE + if hasattr(sr_raw, "item"): + return int(sr_raw.item()) + return int(sr_raw) + + +def _modify_deploy_config(base_path: str, async_chunk: bool) -> str: + """Build deploy config with explicit sync/async mode and eager execution. + + Mirrors the logic in ``tests/e2e/offline_inference/test_glm_tts.py`` + (``_get_deploy_config``) so that example runs match CI behavior. + """ + with open(base_path) as f: + cfg = yaml.safe_load(f) + cfg["async_chunk"] = async_chunk + for stage in cfg.get("stages", []): + stage["enforce_eager"] = True + if stage.get("stage_id") == 0: + stage["async_scheduling"] = bool(async_chunk) + tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, prefix="glm_tts_") + yaml.dump(cfg, tmp) + tmp.close() + return tmp.name + + +def main(args): + """Run offline GLM-TTS inference.""" + os.makedirs(args.output_dir, exist_ok=True) + base_deploy_config = args.deploy_config or DEFAULT_DEPLOY_CONFIG + deploy_config_path = _modify_deploy_config(base_deploy_config, args.async_chunk) + + ref_audio_wav, ref_audio_sr = _load_ref_audio(args.ref_audio) + if not args.ref_text: + raise ValueError("GLM-TTS requires --ref-audio and --ref-text.") + + inputs = [ + { + "prompt": args.text, + "multi_modal_data": { + "audio": (ref_audio_wav.float().cpu().numpy(), ref_audio_sr), + }, + "modalities": ["audio"], + "mm_processor_kwargs": {"prompt_text": args.ref_text}, + "additional_information": build_glm_tts_prefill_metadata( + args.model, + args.text, + args.ref_text, + ), + } + ] + + omni = Omni( + model=args.model, + stage_configs_path=deploy_config_path, + log_stats=args.log_stats, + stage_init_timeout=args.stage_init_timeout, + ) + + t_start = time.perf_counter() + outputs = omni.generate(inputs) + elapsed = (time.perf_counter() - t_start) * 1000 + + assert outputs, "No outputs returned" + audio_mm = outputs[0].multimodal_output + assert "audio" in audio_mm, "No audio output found" + + audio = _concat_audio(audio_mm["audio"]) + sr = _extract_sample_rate(audio_mm) + out_path = os.path.join(args.output_dir, "output.wav") + sf.write(out_path, audio, samplerate=sr, format="WAV") + + logger.info("Saved %s (%.2fs @ %dHz)", out_path, len(audio) / sr, sr) + logger.info("Total inference: %.1f ms", elapsed) + + +def parse_args(): + parser = FlexibleArgumentParser(description="GLM-TTS Text-to-Speech Example") + parser.add_argument("--model", type=str, required=True, help="Model path") + parser.add_argument("--text", type=str, default="你好,这是一个语音合成测试。") + parser.add_argument("--output-dir", type=str, default="./output") + parser.add_argument("--ref-audio", type=str, required=True, help="Reference WAV path/URL") + parser.add_argument("--ref-text", type=str, required=True, help="Transcript of ref audio") + parser.add_argument("--deploy-config", type=str, default=None) + parser.add_argument( + "--async-chunk", + action="store_true", + default=False, + help="Enable async_chunk mode (streaming DiT). Default: sync two-stage.", + ) + parser.add_argument("--log-stats", action="store_true") + parser.add_argument("--stage-init-timeout", type=int, default=600) + return parser.parse_args() + + +if __name__ == "__main__": + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + main(parse_args()) diff --git a/examples/online_serving/text_to_speech/README.md b/examples/online_serving/text_to_speech/README.md index 75b85580d..922156d4c 100644 --- a/examples/online_serving/text_to_speech/README.md +++ b/examples/online_serving/text_to_speech/README.md @@ -15,6 +15,7 @@ For the full list of supported architectures across all modalities, see | Model | HuggingFace repo | Voice cloning | Streaming | Voice presets / upload | Gradio demo | |---|---|---|---|---|---| | Fish Speech S2 Pro | `fishaudio/s2-pro` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | ✓ | +| GLM-TTS | `zai-org/GLM-TTS` | ✓ (`ref_audio`+`ref_text`, required) | ✓ (PCM stream) | — | ✓ | | Ming-flash-omni-TTS | `Jonathan1909/Ming-flash-omni-2.0` | — (caption-controlled) | — | caption fields (`instructions`) | — | | MOSS-TTS-Nano | `OpenMOSS-Team/MOSS-TTS-Nano` | ✓ (`ref_audio` required) | ✓ (PCM stream) | — | ✓ | | OmniVoice | `k2-fsa/OmniVoice` | (offline only) | — | — | — | @@ -94,6 +95,45 @@ For full request-shape documentation (all parameters, response formats, error co --- +## GLM-TTS + +2-stage TTS (AR + DiT flow-matching) at 24 kHz. Every request requires `ref_audio` + `ref_text`. + +### Launch +```bash +vllm serve zai-org/GLM-TTS --omni --trust-remote-code --port 8091 +# or: +bash examples/online_serving/text_to_speech/glm_tts/run_server.sh /path/to/GLM-TTS +``` + +### Sending requests +```bash +# Voice cloning (required) +python examples/online_serving/text_to_speech/glm_tts/openai_speech_client.py \ + --text "你好,这是语音克隆测试。" \ + --ref-audio file:///path/to/ref.wav \ + --ref-text "这是参考音频的文本内容。" + +# Custom format +python examples/online_serving/text_to_speech/glm_tts/openai_speech_client.py \ + --text "Hello, this is a voice cloning test." \ + --ref-audio file:///path/to/ref.wav \ + --ref-text "Transcript of the reference audio." \ + --response-format mp3 -o output.mp3 +``` + +### Gradio demo +```bash +bash examples/online_serving/text_to_speech/glm_tts/run_gradio_demo.sh +``` + +### Notes +- Output: 24 kHz mono WAV via HiFT vocoder. +- `ref_audio` + `ref_text` are **required** together on every request. Reference audio should be 3-10 seconds. +- Voice cloning feature extraction (WhisperVQ, CampPlus, mel) runs on the model side — no external dependency on the serving layer. + +--- + ## Fish Speech S2 Pro 4B dual-AR TTS at 44.1 kHz. Server uses the DAC codec. diff --git a/examples/online_serving/text_to_speech/glm_tts/gradio_demo.py b/examples/online_serving/text_to_speech/glm_tts/gradio_demo.py new file mode 100644 index 000000000..12e5d4b11 --- /dev/null +++ b/examples/online_serving/text_to_speech/glm_tts/gradio_demo.py @@ -0,0 +1,291 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Gradio demo for GLM-TTS online serving via /v1/audio/speech API. + +Supports: + - Voice-cloned text-to-speech synthesis from reference audio (upload or URL) + - Streaming (progressive PCM chunks) and non-streaming modes + +Usage: + # Start the server first (see run_server.sh), then: + python gradio_demo.py --api-base http://localhost:8091 + + # Or use run_gradio_demo.sh to start both server and demo together. +""" + +import argparse +import base64 +import io + +try: + import gradio as gr +except ImportError: + raise ImportError("gradio is required to run this demo. Install it with: pip install 'vllm-omni[demo]'") from None +import httpx +import numpy as np +import soundfile as sf + +PCM_SAMPLE_RATE = 24000 +DEFAULT_API_BASE = "http://localhost:8091" + + +def encode_audio_to_base64(audio_data: tuple) -> str: + """Encode Gradio audio input (sample_rate, numpy_array) to base64 data URL.""" + sample_rate, audio_np = audio_data + if audio_np.dtype != np.int16: + if audio_np.dtype in (np.float32, np.float64): + audio_np = np.clip(audio_np, -1.0, 1.0) + audio_np = (audio_np * 32767).astype(np.int16) + else: + audio_np = audio_np.astype(np.int16) + buf = io.BytesIO() + sf.write(buf, audio_np, sample_rate, format="WAV") + wav_b64 = base64.b64encode(buf.getvalue()).decode("utf-8") + return f"data:audio/wav;base64,{wav_b64}" + + +def build_payload( + text: str, + ref_audio: tuple | None, + ref_audio_url: str, + ref_text: str, + response_format: str = "wav", + stream: bool = False, + max_new_tokens: int = 4096, +) -> dict: + """Build the /v1/audio/speech request payload.""" + if not text or not text.strip(): + raise gr.Error("Please enter text to synthesize.") + + payload: dict = { + "input": text.strip(), + "voice": "default", + "response_format": "pcm" if stream else response_format, + "stream": stream, + "max_new_tokens": max_new_tokens, + } + + # Voice cloning: ref_audio takes priority over URL. + ref_url_stripped = ref_audio_url.strip() if ref_audio_url else "" + if ref_audio is not None: + payload["ref_audio"] = encode_audio_to_base64(ref_audio) + elif ref_url_stripped: + payload["ref_audio"] = ref_url_stripped + else: + raise gr.Error("GLM-TTS requires a reference audio file or URL.") + + if not ref_text or not ref_text.strip(): + raise gr.Error("GLM-TTS requires a transcript of the reference audio.") + payload["ref_text"] = ref_text.strip() + + return payload + + +def generate_speech(api_base: str, text: str, ref_audio, ref_audio_url, ref_text, response_format, max_new_tokens): + """Non-streaming: call /v1/audio/speech and return full audio.""" + payload = build_payload( + text, ref_audio, ref_audio_url, ref_text, response_format, stream=False, max_new_tokens=max_new_tokens + ) + + try: + with httpx.Client(timeout=300.0) as client: + resp = client.post( + f"{api_base}/v1/audio/speech", + json=payload, + headers={"Content-Type": "application/json", "Authorization": "Bearer EMPTY"}, + ) + except httpx.TimeoutException: + raise gr.Error("Request timed out. The server may be busy.") + except httpx.ConnectError: + raise gr.Error(f"Cannot connect to server at {api_base}. Is the server running?") + + if resp.status_code != 200: + raise gr.Error(f"Server error ({resp.status_code}): {resp.text}") + + content_type = resp.headers.get("content-type", "") + if "application/json" in content_type: + try: + raise gr.Error(f"Server error: {resp.json()}") + except ValueError: + pass + + try: + if response_format == "pcm": + audio_np = np.frombuffer(resp.content, dtype=np.int16).astype(np.float32) / 32767.0 + return (PCM_SAMPLE_RATE, audio_np) + audio_np, sample_rate = sf.read(io.BytesIO(resp.content)) + if audio_np.ndim > 1: + audio_np = audio_np[:, 0] + return (sample_rate, audio_np.astype(np.float32)) + except Exception as e: + raise gr.Error(f"Failed to decode audio: {e}") + + +def generate_speech_stream( + api_base, text, ref_audio, ref_audio_url, ref_text, response_format, max_new_tokens, chunk_seconds=0.5 +): + """Streaming: yield PCM chunks for progressive playback.""" + payload = build_payload( + text, ref_audio, ref_audio_url, ref_text, response_format, stream=True, max_new_tokens=max_new_tokens + ) + + min_chunk_samples = int(PCM_SAMPLE_RATE * chunk_seconds) + prebuffer_chunks = 2 + pending: list[np.ndarray] = [] + pending_len = 0 + chunks_yielded = 0 + prebuffer: list[np.ndarray] = [] + + try: + leftover = b"" + with httpx.Client(timeout=300.0) as client: + with client.stream( + "POST", + f"{api_base}/v1/audio/speech", + json=payload, + headers={"Content-Type": "application/json", "Authorization": "Bearer EMPTY"}, + ) as resp: + if resp.status_code != 200: + resp.read() + raise gr.Error(f"Server error ({resp.status_code}): {resp.text}") + for chunk in resp.iter_bytes(): + if not chunk: + continue + raw = leftover + chunk + usable = len(raw) - (len(raw) % 2) + leftover = raw[usable:] + if usable == 0: + continue + float_samples = np.frombuffer(raw[:usable], dtype=np.int16).copy().astype(np.float32) / 32767.0 + pending.append(float_samples) + pending_len += len(float_samples) + if pending_len >= min_chunk_samples: + audio_chunk = np.concatenate(pending) + pending.clear() + pending_len = 0 + if chunks_yielded < prebuffer_chunks: + prebuffer.append(audio_chunk) + chunks_yielded += 1 + if chunks_yielded == prebuffer_chunks: + yield (PCM_SAMPLE_RATE, np.concatenate(prebuffer)) + prebuffer.clear() + else: + yield (PCM_SAMPLE_RATE, audio_chunk) + remaining = prebuffer + pending + if remaining: + yield (PCM_SAMPLE_RATE, np.concatenate(remaining)) + except httpx.TimeoutException: + raise gr.Error("Request timed out. The server may be busy.") + except httpx.ConnectError: + raise gr.Error(f"Cannot connect to server at {api_base}. Is the server running?") + + +def on_stream_change(stream: bool): + """Lock format to PCM when streaming.""" + if stream: + return gr.update(value="pcm", interactive=False) + return gr.update(value="wav", interactive=True) + + +def build_interface(api_base: str, stream_chunk_seconds: float = 0.5): + """Build the Gradio interface.""" + with gr.Blocks(title="GLM-TTS Demo") as demo: + gr.Markdown("# GLM-TTS - Voice Cloning") + gr.Markdown(f"**Server:** `{api_base}` | **Model:** GLM-TTS | **Output:** 24kHz") + + with gr.Row(): + with gr.Column(scale=3): + text_input = gr.Textbox( + label="Text to Synthesize", + placeholder="Enter text here (supports Chinese and English)...", + lines=4, + ) + + with gr.Accordion("Reference Voice (required)", open=True): + gr.Markdown("Upload or link a short reference audio (3-10s) and provide its transcript.") + ref_audio = gr.Audio( + label="Reference Audio", + type="numpy", + sources=["upload", "microphone"], + ) + ref_audio_url = gr.Textbox( + label="Reference Audio URL (alternative to upload)", + placeholder="https://example.com/reference.wav", + lines=1, + ) + ref_text = gr.Textbox( + label="Reference Audio Transcript (required for cloning)", + placeholder="Exact transcript of the reference audio...", + lines=2, + ) + + with gr.Row(): + response_format = gr.Dropdown( + choices=["wav", "mp3", "flac", "pcm"], + value="wav", + label="Audio Format", + scale=1, + ) + stream_checkbox = gr.Checkbox( + label="Stream output", + value=False, + info="Progressive PCM streaming", + scale=1, + ) + max_new_tokens = gr.Slider( + minimum=256, + maximum=8192, + value=4096, + step=256, + label="Max New Tokens", + scale=1, + ) + + generate_btn = gr.Button("Generate Speech", variant="primary", size="lg") + + with gr.Column(scale=2): + audio_output = gr.Audio( + label="Generated Audio", + interactive=False, + streaming=True, + autoplay=True, + ) + gr.Markdown( + "### About\n" + "- **GLM-TTS** by Zhipu AI: Llama-based AR + Flow Matching DiT\n" + "- **Voice cloning**: upload 3-10s reference + transcript\n" + "- **Streaming**: real-time PCM output\n" + "- **24kHz** output via Vocos vocoder" + ) + + stream_checkbox.change(fn=on_stream_change, inputs=[stream_checkbox], outputs=[response_format]) + + all_inputs = [text_input, ref_audio, ref_audio_url, ref_text, response_format, max_new_tokens] + + def dispatch(stream_enabled, *args): + if stream_enabled: + yield from generate_speech_stream(api_base, *args, chunk_seconds=stream_chunk_seconds) + else: + yield generate_speech(api_base, *args) + + generate_btn.click(fn=dispatch, inputs=[stream_checkbox] + all_inputs, outputs=[audio_output]) + demo.queue() + return demo + + +def main(): + parser = argparse.ArgumentParser(description="Gradio demo for GLM-TTS") + parser.add_argument("--api-base", default=DEFAULT_API_BASE, help=f"API base URL (default: {DEFAULT_API_BASE})") + parser.add_argument("--host", default="0.0.0.0", help="Gradio host (default: 0.0.0.0)") + parser.add_argument("--port", type=int, default=7860, help="Gradio port (default: 7860)") + parser.add_argument("--share", action="store_true", help="Share publicly") + parser.add_argument("--stream-chunk-seconds", type=float, default=0.5, help="Seconds per streaming chunk") + args = parser.parse_args() + + print(f"Connecting to vLLM server at: {args.api_base}") + demo = build_interface(args.api_base, stream_chunk_seconds=args.stream_chunk_seconds) + demo.launch(server_name=args.host, server_port=args.port, share=args.share) + + +if __name__ == "__main__": + main() diff --git a/examples/online_serving/text_to_speech/glm_tts/openai_speech_client.py b/examples/online_serving/text_to_speech/glm_tts/openai_speech_client.py new file mode 100644 index 000000000..a0ff181c3 --- /dev/null +++ b/examples/online_serving/text_to_speech/glm_tts/openai_speech_client.py @@ -0,0 +1,173 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""OpenAI-compatible client for GLM-TTS via /v1/audio/speech endpoint. + +GLM-TTS is a two-stage TTS system (AR + DiT) that generates audio from text +conditioned on reference speech. Each request requires ref_audio + ref_text. + +Usage: + # Voice cloning + python openai_speech_client.py --text "你好" --ref-audio file:///path/to/ref.wav --ref-text "参考文本" + + # Streaming response, for async_chunk server mode + python openai_speech_client.py --text "你好" --stream --ref-audio file:///path/to/ref.wav --ref-text "参考文本" + + # Specify output format + python openai_speech_client.py --text "你好" --ref-audio file:///path/to/ref.wav \ + --ref-text "参考文本" --response-format mp3 -o output.mp3 +""" + +import argparse + +import httpx + +# Default server configuration +DEFAULT_API_BASE = "http://localhost:8091" +DEFAULT_API_KEY = "EMPTY" + + +def run_tts_generation(args) -> None: + """Run TTS generation via OpenAI-compatible /v1/audio/speech API.""" + if not args.ref_audio or not args.ref_text: + raise ValueError("GLM-TTS requires --ref-audio and --ref-text for voice cloning.") + + payload = { + "model": args.model, + "voice": "default", + "input": args.text, + "response_format": args.response_format, + "stream": bool(args.stream), + "ref_audio": args.ref_audio, + "ref_text": args.ref_text, + } + if args.max_new_tokens: + payload["max_new_tokens"] = args.max_new_tokens + + print(f"Model: {args.model}") + print(f"Text: {args.text}") + print(f"Voice cloning: ref_audio={args.ref_audio}, ref_text={args.ref_text}") + print(f"Stream: {args.stream}") + print("Generating audio...") + + api_url = f"{args.api_base}/v1/audio/speech" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {args.api_key}", + } + + if args.stream: + output_path = args.output or "tts_output.pcm" + with httpx.Client(timeout=300.0) as client, open(output_path, "wb") as f: + with client.stream("POST", api_url, json=payload, headers=headers) as response: + if response.status_code != 200: + print(f"Error: {response.status_code}") + response.read() + print(response.text) + return + for chunk in response.iter_bytes(): + f.write(chunk) + print(f"Streaming audio saved to: {output_path}") + else: + with httpx.Client(timeout=300.0) as client: + response = client.post(api_url, json=payload, headers=headers) + if response.status_code != 200: + print(f"Error: {response.status_code}") + print(response.text) + return + try: + text = response.content.decode("utf-8") + if text.startswith('{"error"'): + print(f"Error: {text}") + return + except UnicodeDecodeError: + pass + output_path = args.output or f"tts_output.{args.response_format}" + with open(output_path, "wb") as f: + f.write(response.content) + print(f"Audio saved to: {output_path}") + + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description="OpenAI-compatible client for GLM-TTS via /v1/audio/speech", + ) + + # Server configuration + parser.add_argument( + "--api-base", + type=str, + default=DEFAULT_API_BASE, + help=f"API base URL (default: {DEFAULT_API_BASE})", + ) + parser.add_argument( + "--api-key", + type=str, + default=DEFAULT_API_KEY, + help="API key (default: EMPTY)", + ) + parser.add_argument( + "--model", + "-m", + type=str, + default="glm-tts", + help="Model name/path", + ) + + # Input text + parser.add_argument( + "--text", + type=str, + required=True, + help="Text to synthesize", + ) + + # Generation parameters + parser.add_argument( + "--max-new-tokens", + type=int, + default=None, + help="Maximum new tokens to generate (default: model default)", + ) + + # Output + parser.add_argument( + "--response-format", + type=str, + default="wav", + choices=["wav", "mp3", "flac", "pcm", "aac", "opus"], + help="Audio output format (default: wav)", + ) + parser.add_argument( + "--stream", + action="store_true", + help="Request a streaming audio response (use with async_chunk server mode).", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output audio file path (default: tts_output.)", + ) + + # Voice cloning parameters + parser.add_argument( + "--ref-audio", + type=str, + default=None, + help="Reference audio URL, file:// URI, or base64 data URL for voice cloning", + ) + parser.add_argument( + "--ref-text", + type=str, + default=None, + help="Transcript of the reference audio (required with --ref-audio)", + ) + + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + run_tts_generation(args) diff --git a/examples/online_serving/text_to_speech/glm_tts/run_gradio_demo.sh b/examples/online_serving/text_to_speech/glm_tts/run_gradio_demo.sh new file mode 100644 index 000000000..75cecdf9e --- /dev/null +++ b/examples/online_serving/text_to_speech/glm_tts/run_gradio_demo.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Launch GLM-TTS server + Gradio demo together. +# +# Usage: +# ./run_gradio_demo.sh +# CUDA_VISIBLE_DEVICES=0 PORT=8091 GRADIO_PORT=7860 ./run_gradio_demo.sh + +set -e + +MODEL="${MODEL:-zai-org/GLM-TTS}" +PORT="${PORT:-8091}" +GRADIO_PORT="${GRADIO_PORT:-7860}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" + +echo "Starting GLM-TTS server (port $PORT)..." +FLASHINFER_DISABLE_VERSION_CHECK=1 \ +vllm-omni serve "$MODEL" \ + --deploy-config "$REPO_ROOT/vllm_omni/deploy/glm_tts.yaml" \ + --host 0.0.0.0 \ + --port "$PORT" \ + --gpu-memory-utilization 0.9 \ + --trust-remote-code \ + --enforce-eager \ + --omni & +SERVER_PID=$! + +cleanup() { + echo "Stopping server (PID $SERVER_PID)..." + kill $SERVER_PID 2>/dev/null + wait $SERVER_PID 2>/dev/null +} +trap cleanup EXIT + +# Wait for server to be ready. +echo "Waiting for server to start..." +for i in $(seq 1 120); do + if curl -s "http://localhost:$PORT/health" > /dev/null 2>&1; then + echo "Server ready." + break + fi + sleep 2 +done + +echo "Starting Gradio demo (port $GRADIO_PORT)..." +python "$SCRIPT_DIR/gradio_demo.py" \ + --api-base "http://localhost:$PORT" \ + --port "$GRADIO_PORT" diff --git a/examples/online_serving/text_to_speech/glm_tts/run_server.sh b/examples/online_serving/text_to_speech/glm_tts/run_server.sh new file mode 100644 index 000000000..f5cc3a1f8 --- /dev/null +++ b/examples/online_serving/text_to_speech/glm_tts/run_server.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Launch vLLM-Omni server for GLM-TTS models +# +# Usage: +# ./run_server.sh # Default model path, async_chunk mode +# ./run_server.sh /path/to/GLM-TTS # Custom model path, async_chunk mode +# ./run_server.sh /path/to/GLM-TTS sync # Sync two-stage mode +# +# NOTE: The model path should point to the repo ROOT (not llm/ subdirectory). +# model_subdir/tokenizer_subdir in the pipeline config resolve subdirectories. + +set -e + +MODEL="${1:-zai-org/GLM-TTS}" +MODE="${2:-async}" + +EXTRA_ARGS=() +case "$MODE" in + async|async_chunk) + ;; + sync|no_async_chunk) + EXTRA_ARGS+=("--no-async-chunk") + ;; + *) + echo "Unknown mode: $MODE (expected async or sync)" >&2 + exit 1 + ;; +esac + +echo "Starting GLM-TTS server with model: $MODEL (mode: $MODE)" + +vllm-omni serve "$MODEL" \ + --deploy-config vllm_omni/deploy/glm_tts.yaml \ + --host 0.0.0.0 \ + --port 8091 \ + --trust-remote-code \ + --omni \ + "${EXTRA_ARGS[@]}" diff --git a/recipes/README.md b/recipes/README.md index 8ddb72943..d2e1eeff3 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -43,6 +43,7 @@ recipes/ | [`Tencent/HunyuanImage-3.0-Instruct.md`](./Tencent/HunyuanImage-3.0-Instruct.md) | DiT-only text-to-image serving and benchmark, including ModelOpt mixed FP8/NVFP4 | 4x H100/H800 80GB / 2x B200 | | [`Wan-AI/Wan2.2-I2V.md`](./Wan-AI/Wan2.2-I2V.md) | Image-to-video serving (Wan2.2 14B) | 8x Ascend NPU (A2/A3) | | [`Wan-AI/Wan2.2-S2V.md`](./Wan-AI/Wan2.2-S2V.md) | Speech-to-video serving (Wan2.2 14B) | 2x A100/H100 80GB | +| [`zai-org/GLM-TTS.md`](./zai-org/GLM-TTS.md) | Online serving for Chinese/English zero-shot voice-cloned TTS | 1x A40 48GB | Within a single recipe file, include different hardware support sections such as `GPU`, `ROCm`, and `NPU`, and add concrete tested configurations like diff --git a/recipes/zai-org/GLM-TTS.md b/recipes/zai-org/GLM-TTS.md new file mode 100644 index 000000000..2bf1a202a --- /dev/null +++ b/recipes/zai-org/GLM-TTS.md @@ -0,0 +1,105 @@ +# GLM-TTS for Chinese/English TTS on 1x GPU + +## Summary + +- Vendor: zai-org +- Model: `zai-org/GLM-TTS` +- Task: Zero-shot voice-cloned text-to-speech synthesis +- Mode: Online serving with the OpenAI-compatible `/v1/audio/speech` API +- Maintainer: Community + +## When to use this recipe + +Use this recipe to serve GLM-TTS as a two-stage TTS system (AR + DiT +flow-matching) for Chinese and English speech synthesis. Every request is +conditioned on reference audio and its transcript. + +## References + +- Upstream or canonical docs: + [zai-org/GLM-TTS on HuggingFace](https://huggingface.co/zai-org/GLM-TTS) +- GitHub repository: + [zai-org/GLM-TTS](https://github.com/zai-org/GLM-TTS) +- Related example under `examples/`: + [`examples/online_serving/text_to_speech/README.md#glm-tts`](../../examples/online_serving/text_to_speech/README.md#glm-tts) +- Offline inference example: + [`examples/offline_inference/text_to_speech/README.md#glm-tts`](../../examples/offline_inference/text_to_speech/README.md#glm-tts) + +## Hardware Support + +### GPU + +### 1x A40 48GB + +#### Environment + +- OS: Linux +- Python: 3.10+ +- Driver / runtime: NVIDIA CUDA environment with A40 48GB GPU +- vLLM version: Match the repository requirements for your checkout +- vLLM-Omni version or commit: Use the commit you are deploying from + +#### Command + +Start the server from the repository root: + +```bash +vllm-omni serve zai-org/GLM-TTS --omni --trust-remote-code --port 8091 +``` + +Async chunking is enabled by default in the bundled deployment config. For +the sync (non-streaming) path: + +```bash +vllm-omni serve zai-org/GLM-TTS --omni --trust-remote-code --port 8091 --no-async-chunk +``` + +Use a custom deploy config for advanced cases: + +```bash +vllm-omni serve zai-org/GLM-TTS --omni --trust-remote-code --port 8091 \ + --deploy-config /path/to/your_glm_tts_overrides.yaml +``` + +#### Verification + +Run the bundled OpenAI-compatible client with reference audio: + +```bash +python examples/online_serving/text_to_speech/glm_tts/openai_speech_client.py \ + --text "你好,这是一个语音合成测试。" \ + --ref-audio file:///path/to/ref.wav \ + --ref-text "这是参考音频的文本内容。" +``` + +For a quick API smoke test: + +```bash +curl http://localhost:8091/v1/audio/speech \ + -H "Content-Type: application/json" \ + -d '{ + "model": "zai-org/GLM-TTS", + "input": "你好,这是一个语音合成测试。", + "response_format": "wav", + "ref_audio": "file:///path/to/ref.wav", + "ref_text": "这是参考音频的文本内容。" + }' --output test.wav +``` + +Voice cloning with reference audio: + +```bash +python examples/online_serving/text_to_speech/glm_tts/openai_speech_client.py \ + --text "你好,这是语音克隆测试。" \ + --ref-audio file:///path/to/ref.wav \ + --ref-text "这是参考音频的文本内容。" +``` + +#### Notes + +- Hardware scope: the default bundled config is CUDA-only and verified on 1x A40 48GB (~16.6 GiB peak); fits 24GB cards. Split stages across GPUs for higher concurrency. +- Memory usage: ~18-20GB total (AR ~10GB, DiT ~8GB); both stages share GPU 0 by default. +- Audio output: 24kHz mono WAV via HiFT vocoder (Vocos2D 32kHz fallback with resampling). +- Key flags: `--omni` is required; `--trust-remote-code` is needed for the GLM-TTS phoneme tokenizer; the DiT stage enables bucketed model-internal CUDA graphs with eager fallback. +- Voice cloning: requires `ref_audio` + `ref_text` together. Reference audio should be 3-10 seconds. Feature extraction (WhisperVQ tokenizer, CampPlus ONNX, mel) runs on the model side. +- Known limitations: First request may be slow due to lazy model loading (WhisperVQ, CampPlus ONNX). Warm-cache RTF is approximately 0.6-0.7x on A40. diff --git a/tests/assets/glm_tts/jiayan_zh.wav b/tests/assets/glm_tts/jiayan_zh.wav new file mode 100644 index 0000000000000000000000000000000000000000..1dd5287471534ccf19eedf3b6c95af32024c46a2 GIT binary patch literal 356734 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%0Dis2 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 tuple[np.ndarray, int]: + audio, sr = sf.read(str(REFERENCE_PROMPT_WAV_PATH), dtype="float32", always_2d=False) + if isinstance(audio, np.ndarray) and audio.ndim > 1: + audio = np.mean(audio, axis=-1) + return np.asarray(audio, dtype=np.float32), int(sr) + + +def _concat_audio(audio_val) -> np.ndarray: + import torch + + if isinstance(audio_val, list): + tensors = [torch.as_tensor(t).float().reshape(-1) for t in audio_val if t is not None] + if not tensors: + return np.zeros((0,), dtype=np.float32) + return torch.cat(tensors, dim=-1).cpu().numpy().astype(np.float32, copy=False) + if isinstance(audio_val, torch.Tensor): + return audio_val.float().cpu().numpy().reshape(-1) + return np.asarray(audio_val, dtype=np.float32).reshape(-1) + + +def _extract_sample_rate(audio_mm: dict) -> int: + sr_raw = audio_mm.get("sr", 24000) + if isinstance(sr_raw, list): + sr_raw = sr_raw[-1] if sr_raw else 24000 + if hasattr(sr_raw, "item"): + return int(sr_raw.item()) + return int(sr_raw) + + +def _get_deploy_config(*, async_chunk: bool) -> str: + """Build deploy config with explicit sync/async mode and eager execution.""" + return modify_stage_config( + DEPLOY_CONFIG, + updates={ + "async_chunk": async_chunk, + "stages": { + 0: { + "enforce_eager": True, + "async_scheduling": bool(async_chunk), + }, + 1: { + "enforce_eager": True, + }, + }, + }, + ) + + +@pytest.mark.advanced_model +@pytest.mark.omni +@hardware_test(res={"cuda": "H100"}, num_cards=1) +@pytest.mark.parametrize("async_chunk", ASYNC_CHUNK_MODES) +def test_offline_voice_clone_zh(async_chunk: bool) -> None: + """ + Test voice cloning offline inference. + Deploy Setting: glm_tts.yaml with explicit sync/async mode, enforce_eager + Input Modal: text + ref_audio + ref_text + Output Modal: audio + + Uses the official jiayan_zh.wav reference audio from the upstream + GLM-TTS repository (real human speech matching REF_TEXT). + """ + synth_text = "今天天气真不错,适合出去散散步。" + prompt_audio = _load_ref_audio() + from tests.helpers.runtime import OmniRunner + + with OmniRunner( + MODEL, + stage_configs_path=_get_deploy_config(async_chunk=async_chunk), + stage_init_timeout=600, + ) as omni_runner: + outputs = omni_runner.omni.generate( + [ + { + "prompt": synth_text, + "multi_modal_data": {"audio": prompt_audio}, + "modalities": ["audio"], + "mm_processor_kwargs": {"prompt_text": REF_TEXT}, + "additional_information": build_glm_tts_prefill_metadata( + MODEL, + synth_text, + REF_TEXT, + trust_remote_code=True, + ), + } + ], + omni_runner.get_default_sampling_params_list(), + ) + + assert outputs, "No outputs returned" + audio_mm = outputs[0].multimodal_output + assert "audio" in audio_mm, "No audio output found" + audio = _concat_audio(audio_mm["audio"]) + assert audio.size > 0, "Generated audio is empty" + + with tempfile.NamedTemporaryFile(suffix=".wav") as out_file: + sf.write(out_file.name, audio, samplerate=_extract_sample_rate(audio_mm), format="WAV") + transcript = convert_audio_file_to_text(out_file.name) + audio_bytes = open(out_file.name, "rb").read() + + assert_audio_speech_response( + OmniResponse( + success=True, + audio_bytes=audio_bytes, + audio_content=transcript, + audio_format="audio/wav", + ), + {"input": synth_text, "response_format": "wav"}, + run_level="advanced_model", + ) diff --git a/tests/e2e/online_serving/test_glm_tts.py b/tests/e2e/online_serving/test_glm_tts.py new file mode 100644 index 000000000..7c6ad9764 --- /dev/null +++ b/tests/e2e/online_serving/test_glm_tts.py @@ -0,0 +1,131 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +""" +E2E Online tests for GLM-TTS model with text input and audio output. + +These tests verify the /v1/audio/speech endpoint works correctly with +the GLM-TTS two-stage pipeline (AR + DiT). +""" + +import os + +os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" +os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0" + +import pytest + +from tests.helpers.mark import hardware_test +from tests.helpers.media import load_test_audio_data_url +from tests.helpers.runtime import OmniServerParams +from tests.helpers.stage_config import get_deploy_config_path + +MODEL = os.environ.get("GLM_TTS_MODEL_PATH", "zai-org/GLM-TTS") +REF_TEXT = "他当时还跟线下其他的站姐吵架,然后,打架进局子了。" + +DEPLOY_CONFIG = get_deploy_config_path("glm_tts.yaml") + +# Official GLM-TTS reference audio and transcript. Vendored under tests/assets/ +# so Buildkite does not depend on raw.githubusercontent.com during E2E. +REF_AUDIO_URL = load_test_audio_data_url("glm_tts/jiayan_zh.wav") + + +SYNC_EXTRA_ARGS = [ + "--trust-remote-code", + "--disable-log-stats", + "--no-async-chunk", +] + +ASYNC_EXTRA_ARGS = [ + "--trust-remote-code", + "--disable-log-stats", +] + +tts_sync_server_params = [ + pytest.param( + OmniServerParams( + model=MODEL, + stage_config_path=DEPLOY_CONFIG, + server_args=SYNC_EXTRA_ARGS, + stage_init_timeout=600, + ), + id="glm_tts_sync", + ) +] + +tts_async_chunk_server_params = [ + pytest.param( + OmniServerParams( + model=MODEL, + stage_config_path=DEPLOY_CONFIG, + server_args=ASYNC_EXTRA_ARGS, + stage_init_timeout=600, + ), + id="glm_tts_async_chunk", + ) +] + + +# GLM-TTS is a zero-shot voice cloning model (always requires ref_audio). +# There is no text-only / non-clone path — the official implementation +# asserts prompt_feat.shape[1] != 0 in token2wav_with_cache and every +# example prompt includes a reference audio. CosyVoice3 follows the same +# pattern: all tests are voice_clone_*. + + +@pytest.mark.advanced_model +@pytest.mark.omni +@hardware_test(res={"cuda": "H100"}, num_cards=1) +@pytest.mark.parametrize("omni_server", tts_sync_server_params, indirect=True) +def test_voice_clone_zh_sync(omni_server, openai_client) -> None: + """ + Test voice cloning TTS with Chinese text. + Deploy Setting: glm_tts.yaml with ``--no-async-chunk`` (sync two-stage) + Input Modal: text + ref_audio + ref_text + Output Modal: audio + Input Setting: stream=False + + Uses the official jiayan_zh.wav reference audio from the upstream + GLM-TTS repository (real human speech matching REF_TEXT). + """ + request_config = { + "model": omni_server.model, + "input": "今天天气真不错,适合出去散散步。", + "stream": False, + "response_format": "wav", + "ref_audio": REF_AUDIO_URL, + "ref_text": REF_TEXT, + } + openai_client.send_audio_speech_request(request_config) + + +@pytest.mark.advanced_model +@pytest.mark.omni +@hardware_test(res={"cuda": "H100"}, num_cards=1) +@pytest.mark.parametrize("omni_server", tts_async_chunk_server_params, indirect=True) +def test_voice_clone_zh_async_chunk(omni_server, openai_client) -> None: + """ + Test voice cloning TTS with Chinese text via async_chunk streaming. + Deploy Setting: glm_tts.yaml default ``async_chunk: true`` + Input Modal: text + ref_audio + ref_text + Output Modal: audio + Input Setting: stream=True + """ + request_config = { + "model": omni_server.model, + "input": "今天天气真不错,适合出去散散步。", + "stream": True, + "response_format": "wav", + "ref_audio": REF_AUDIO_URL, + "ref_text": REF_TEXT, + } + openai_client.send_audio_speech_request(request_config) + + +@pytest.mark.advanced_model +@pytest.mark.omni +@hardware_test(res={"cuda": "H100"}, num_cards=1) +@pytest.mark.parametrize("omni_server", tts_sync_server_params, indirect=True) +def test_models_endpoint(omni_server, openai_client) -> None: + """Test the /v1/models endpoint returns loaded model.""" + models = openai_client.client.models.list() + assert len(models.data) > 0 diff --git a/tests/entrypoints/openai_api/test_serving_speech.py b/tests/entrypoints/openai_api/test_serving_speech.py index 2440fe586..872410aaa 100644 --- a/tests/entrypoints/openai_api/test_serving_speech.py +++ b/tests/entrypoints/openai_api/test_serving_speech.py @@ -2335,6 +2335,84 @@ class TestCosyVoice3Serving: cosyvoice3_server._build_cosyvoice3_prompt.assert_awaited_once() +# ---- GLM-TTS Serving Tests ---- + + +@pytest.fixture +def glm_tts_server(mocker: MockerFixture): + mocker.patch.object(OmniOpenAIServingSpeech, "_load_supported_speakers", return_value=set()) + mocker.patch.object(OmniOpenAIServingSpeech, "_load_codec_frame_rate", return_value=None) + + mock_engine_client = mocker.MagicMock() + mock_engine_client.errored = False + mock_engine_client.model_config = mocker.MagicMock( + model="zai-org/GLM-TTS", + hf_config=SimpleNamespace(min_token_text_ratio=2, max_token_text_ratio=20), + ) + mock_engine_client.default_sampling_params_list = [ + SimpleNamespace(max_tokens=2048, min_tokens=None, extra_args=None) + ] + mock_engine_client.tts_batch_max_items = 32 + mock_engine_client.generate = mocker.MagicMock(return_value="generator") + mock_engine_client.stage_configs = [ + SimpleNamespace( + engine_args=SimpleNamespace(model_stage="glm_tts"), + tts_args={}, + ) + ] + + mock_models = mocker.MagicMock() + mock_models.is_base_model.return_value = True + + return OmniOpenAIServingSpeech( + engine_client=mock_engine_client, + models=mock_models, + request_logger=mocker.MagicMock(), + ) + + +class TestGLMTTSServing: + def test_validate_glm_tts_requires_ref_audio(self, glm_tts_server): + request = OpenAICreateSpeechRequest(input="Hello", ref_text="Reference transcript") + error = glm_tts_server._validate_glm_tts_request(request) + assert error is not None + assert "ref_audio" in error + + def test_validate_glm_tts_requires_ref_text(self, glm_tts_server): + request = OpenAICreateSpeechRequest(input="Hello", ref_audio="data:audio/wav;base64,abc") + error = glm_tts_server._validate_glm_tts_request(request) + assert error is not None + assert "ref_text" in error + + def test_estimate_glm_tts_target_text_len_uses_tokenizer_tokens( + self, + glm_tts_server, + mocker: MockerFixture, + ): + class FakeTokenizer: + def encode(self, text): + assert text == "normalized target" + return [10, 20, 30] + + mocker.patch( + "vllm_omni.model_executor.models.glm_tts.glm_tts.resolve_glm_tts_tokenizer_path", + return_value="resolved-tokenizer", + ) + load_tokenizer = mocker.patch( + "vllm_omni.model_executor.models.glm_tts.glm_tts.load_glm_tts_tokenizer", + return_value=FakeTokenizer(), + ) + mocker.patch( + "vllm_omni.model_executor.models.glm_tts.text_frontend.GLMTTSTextFrontend.text_normalize", + return_value="normalized target", + ) + + text_token_len = glm_tts_server._estimate_glm_tts_text_token_len("abcdef") + + assert text_token_len == 3 + load_tokenizer.assert_called_once() + + class TestTTSAsyncOffloading: """Tests for event-loop-safe offloading of blocking TTS operations.""" diff --git a/tests/model_executor/models/test_glm_tts_prompt_len.py b/tests/model_executor/models/test_glm_tts_prompt_len.py new file mode 100644 index 000000000..df12078b4 --- /dev/null +++ b/tests/model_executor/models/test_glm_tts_prompt_len.py @@ -0,0 +1,351 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +from types import SimpleNamespace + +import pytest +import torch + +from vllm_omni.model_executor.models.common.nucleus_ras_sampling import ras_sample_one +from vllm_omni.model_executor.models.glm_tts import glm_tts as glm_tts_module +from vllm_omni.model_executor.models.glm_tts.glm_tts import GLMTTSForConditionalGeneration +from vllm_omni.model_executor.models.glm_tts.text_frontend import GLMTTSTextFrontend + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + + +def test_glm_tts_ras_fallback_matches_official_random_resample() -> None: + sampled = ras_sample_one( + torch.tensor([0.0, float("-inf")]), + decoded_tokens=[0] * 10, + top_p=1.0, + top_k=1, + win_size=10, + tau_r=0.1, + generator=None, + ) + + assert sampled == 0 + + +def test_glm_tts_text_frontend_applies_official_symbol_replacements() -> None: + frontend = GLMTTSTextFrontend() + + assert frontend.pre_replace("3-2 咯,") == "3减2 喽," + assert frontend.post_replace("α②∈A/B~C²") == "阿尔法二属于A每B到C平方" + + +def test_glm_tts_call_hf_processor_builds_prompt_text_text_boa( + monkeypatch: pytest.MonkeyPatch, +) -> None: + processor = object.__new__(glm_tts_module.GLMTTSMultiModalProcessor) + processor.info = SimpleNamespace( + ctx=SimpleNamespace( + get_hf_config=lambda *_args, **_kwargs: SimpleNamespace(), + model_config=SimpleNamespace(model="fake-model"), + ) + ) + processor.text_frontend = object() + processor.special_ids = {"boa": 99} + processor.speech_tokenizer = object() + processor.processor_device = torch.device("cpu") + processor.campplus_session = object() + + monkeypatch.setattr( + processor, + "_ensure_cached_runtime_components", + lambda *_args, **_kwargs: None, + ) + monkeypatch.setattr( + processor, + "_encode_text", + lambda text: torch.tensor([[11, 12]]) if text.endswith(" ") else torch.tensor([[21, 22, 23]]), + ) + monkeypatch.setattr(processor, "_get_audio", lambda _mm_data: (torch.zeros(8), 24000)) + monkeypatch.setattr( + glm_tts_module, + "_normalize_glm_tts_processor_text", + lambda _frontend, text, add_trailing_space=False: text + (" " if add_trailing_space else ""), + ) + monkeypatch.setattr(glm_tts_module, "extract_prompt_speech_token", lambda *_args, **_kwargs: [7, 8, 9, 10]) + monkeypatch.setattr(glm_tts_module, "extract_prompt_feat", lambda *_args, **_kwargs: torch.ones(4, 3)) + monkeypatch.setattr(glm_tts_module, "extract_spk_embedding", lambda *_args, **_kwargs: [0.1, 0.2, 0.3]) + + out = processor._call_hf_processor( + prompt="target text", + mm_data={"audio": object()}, + mm_kwargs={"prompt_text": "reference text"}, + tok_kwargs={}, + ) + + assert out["input_ids"].tolist() == [[11, 12, 21, 22, 23, 99]] + assert out["prompt_speech_token"].tolist() == [[7, 8, 9, 10]] + assert out["prompt_speech_token_len"][0].item() == 4 + assert out["glm_tts_text_token_len"][0].item() == 3 + + +def test_glm_tts_prompt_updates_append_single_audio_suffix() -> None: + processor = object.__new__(glm_tts_module.GLMTTSMultiModalProcessor) + out_mm_kwargs = { + "audio": [{"prompt_speech_token_len": SimpleNamespace(data=[torch.tensor([5], dtype=torch.long)])}] + } + + updates = processor._get_prompt_updates( + mm_items=object(), + hf_processor_mm_kwargs={}, + out_mm_kwargs=out_mm_kwargs, + ) + + assert len(updates) == 1 + assert updates[0].modality == "audio" + assert updates[0].insertion(0) == [1, 1, 1, 1, 1] + + +def test_glm_tts_dummy_inputs_forbid_dummy_wav(monkeypatch: pytest.MonkeyPatch) -> None: + builder = object.__new__(glm_tts_module.GLMTTSDummyInputsBuilder) + monkeypatch.setattr( + builder, + "_get_dummy_audios", + lambda *_args, **_kwargs: pytest.fail("GLM-TTS dummy inputs must not fabricate wav audio"), + ) + + assert builder.get_dummy_mm_data(seq_len=16, mm_counts={"audio": 1}) == {} + + +def test_glm_tts_processing_info_declares_audio_token_budget() -> None: + info = object.__new__(glm_tts_module.GLMTTSMultiModalProcessingInfo) + + assert info.get_mm_max_tokens_per_item(seq_len=4096, mm_counts={"audio": 1}) == {"audio": 1024} + assert info.get_mm_max_tokens_per_item(seq_len=512, mm_counts={"audio": 1}) == {"audio": 512} + assert info.get_mm_max_tokens_per_item(seq_len=4096, mm_counts={}) == {} + + +def test_glm_tts_text_only_processor_skips_voice_clone(monkeypatch: pytest.MonkeyPatch) -> None: + class FakeTokenizer: + def encode(self, text: str) -> list[int]: + return [11, 12, 13] + + processor = object.__new__(glm_tts_module.GLMTTSMultiModalProcessor) + processor.info = SimpleNamespace( + ctx=SimpleNamespace( + get_hf_config=lambda *_args, **_kwargs: SimpleNamespace(), + model_config=SimpleNamespace(model="fake-model", tokenizer=None, trust_remote_code=True), + ) + ) + + monkeypatch.setattr(glm_tts_module, "resolve_glm_tts_model_dir", lambda *_args, **_kwargs: "/fake-root") + monkeypatch.setattr(glm_tts_module, "load_glm_tts_tokenizer", lambda *_args, **_kwargs: FakeTokenizer()) + monkeypatch.setattr(glm_tts_module, "get_glm_tts_special_token_ids", lambda _tokenizer: {"boa": 99}) + monkeypatch.setattr(glm_tts_module, "GLMTTSTextFrontend", lambda: object()) + monkeypatch.setattr( + glm_tts_module, + "_normalize_glm_tts_processor_text", + lambda _frontend, text, add_trailing_space=False: text, + ) + monkeypatch.setattr( + glm_tts_module, + "load_voice_clone_frontend", + lambda *_args, **_kwargs: pytest.fail("text-only dummy path should not load voice clone frontend"), + ) + + out = processor._call_hf_processor(prompt="target text", mm_data={}, mm_kwargs={}, tok_kwargs={}) + + assert out["input_ids"].tolist() == [[11, 12, 13, 99]] + assert out["prompt_speech_token"].shape == (1, 1024) + assert out["prompt_speech_token_len"][0].item() == 1024 + assert out["glm_tts_prompt_text_token_len"][0].item() == 0 + assert out["glm_tts_text_token_len"][0].item() == 3 + + fields = processor._get_mm_fields_config(out, {}) + assert {key: value.modality for key, value in fields.items()} == { + "prompt_speech_token": "audio", + "prompt_speech_token_len": "audio", + "glm_tts_prompt_text_token_len": "audio", + "glm_tts_text_token_len": "audio", + } + + updates = processor._get_prompt_updates( + mm_items=object(), + hf_processor_mm_kwargs={}, + out_mm_kwargs={ + "audio": [{"prompt_speech_token_len": SimpleNamespace(data=[out["prompt_speech_token_len"][0]])}] + }, + ) + assert len(updates) == 1 + assert len(updates[0].insertion(0)) == 1024 + + +def test_glm_tts_embed_replaces_multimodal_positions_per_request() -> None: + class FakeModel(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.marker = torch.nn.Parameter(torch.zeros(())) + + def embed_tokens(self, input_ids: torch.Tensor) -> torch.Tensor: + return input_ids.to(torch.float32).reshape(-1, 1) + + model = object.__new__(GLMTTSForConditionalGeneration) + torch.nn.Module.__init__(model) + model.model_stage = "glm_tts" + model.model = FakeModel() + + input_ids = torch.tensor([101, 102, 1, 1, 201, 1, 1, 1]) + is_multimodal = torch.tensor([False, False, True, True, False, True, True, True]) + mm_embeds = [ + torch.tensor([[1001.0], [1002.0]]), + torch.tensor([[2001.0], [2002.0], [2003.0]]), + ] + + out = model.embed_input_ids(input_ids, multimodal_embeddings=mm_embeds, is_multimodal=is_multimodal) + + assert out.squeeze(-1).tolist() == [ + 101.0, + 102.0, + 1001.0, + 1002.0, + 201.0, + 2001.0, + 2002.0, + 2003.0, + ] + + +def test_glm_tts_embed_multimodal_keeps_all_batched_requests() -> None: + class FakeModel(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.marker = torch.nn.Parameter(torch.zeros(())) + + def embed_tokens(self, input_ids: torch.Tensor) -> torch.Tensor: + return input_ids.to(torch.float32).reshape(*input_ids.shape, 1) + + model = object.__new__(GLMTTSForConditionalGeneration) + torch.nn.Module.__init__(model) + model.model_stage = "glm_tts" + model.model = FakeModel() + model._ats = 1000 + + out = model.embed_multimodal( + prompt_speech_token=[ + torch.tensor([[1, 2]]), + torch.tensor([[3, 4, 5]]), + ] + ) + + assert out is not None + assert len(out) == 2 + assert out[0].squeeze(-1).tolist() == [1001.0, 1002.0] + assert out[1].squeeze(-1).tolist() == [1003.0, 1004.0, 1005.0] + + +def test_glm_tts_embed_multimodal_returns_one_item_for_single_request() -> None: + class FakeModel(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.marker = torch.nn.Parameter(torch.zeros(())) + + def embed_tokens(self, input_ids: torch.Tensor) -> torch.Tensor: + return input_ids.to(torch.float32).reshape(*input_ids.shape, 1) + + model = object.__new__(GLMTTSForConditionalGeneration) + torch.nn.Module.__init__(model) + model.model_stage = "glm_tts" + model.model = FakeModel() + model._ats = 1000 + + out = model.embed_multimodal(prompt_speech_token=torch.tensor([[1, 2, 3]])) + + assert out is not None + assert len(out) == 1 + assert out[0].squeeze(-1).tolist() == [1001.0, 1002.0, 1003.0] + + +def test_glm_tts_embed_multimodal_treats_scalar_tensor_list_as_one_item() -> None: + class FakeModel(torch.nn.Module): + def __init__(self) -> None: + super().__init__() + self.marker = torch.nn.Parameter(torch.zeros(())) + + def embed_tokens(self, input_ids: torch.Tensor) -> torch.Tensor: + return input_ids.to(torch.float32).reshape(*input_ids.shape, 1) + + model = object.__new__(GLMTTSForConditionalGeneration) + torch.nn.Module.__init__(model) + model.model_stage = "glm_tts" + model.model = FakeModel() + model._ats = 1000 + + out = model.embed_multimodal(prompt_speech_token=[torch.tensor(1), torch.tensor(2)]) + + assert out is not None + assert len(out) == 1 + assert out[0].squeeze(-1).tolist() == [1001.0, 1002.0] + + +def test_glm_tts_preprocess_treats_one_token_text_tail_as_prefill() -> None: + model = object.__new__(GLMTTSForConditionalGeneration) + model._pad = 0 + model._ats = 1000 + model._ate = 2000 + model._eoa = 3000 + model._model_dtype = lambda: torch.float32 + + input_ids, input_embeds, info = model.preprocess( + torch.tensor([123]), + torch.tensor([[1.0, 2.0]]), + glm_tts_mm_prefill_done=True, + ) + + assert input_ids.tolist() == [0] + assert input_embeds.tolist() == [[1.0, 2.0]] + assert info["glm_tts_mm_prefill_done"] is True + assert info["speech_tokens"].tolist() == [[-1]] + + +def test_glm_tts_preprocess_initializes_generation_bounds_before_sampling() -> None: + model = object.__new__(GLMTTSForConditionalGeneration) + model._pad = 0 + model._ats = 1000 + model._ate = 2000 + model._eoa = 3000 + model._model_dtype = lambda: torch.float32 + model.config = SimpleNamespace(min_token_text_ratio=2.0, max_token_text_ratio=20.0) + + _, _, info = model.preprocess( + torch.tensor([11, 12]), + torch.tensor([[1.0], [2.0]]), + glm_tts_text_token_len=[torch.tensor([27])], + ) + + assert info["glm_tts_text_token_len"][0].item() == 27 + + +def test_glm_tts_make_omni_output_emits_prompt_speech_len_once() -> None: + model = object.__new__(GLMTTSForConditionalGeneration) + + out = model.make_omni_output( + torch.randn(2, 4), + model_intermediate_buffer=[{}], + prompt_speech_token_len=[torch.tensor([9])], + glm_tts_text_token_len=[torch.tensor([5])], + ) + + assert out.multimodal_outputs is not None + assert out.multimodal_outputs["prompt_speech_token_len"][0].item() == 9 + assert out.multimodal_outputs["glm_tts_text_token_len"][0].item() == 5 + + +def test_glm_tts_postprocess_caches_prompt_speech_token_len() -> None: + model = object.__new__(GLMTTSForConditionalGeneration) + model.config = SimpleNamespace(min_token_text_ratio=2.0, max_token_text_ratio=20.0) + + update = model.postprocess( + torch.randn(2, 4), + multimodal_outputs={ + "prompt_speech_token_len": [torch.tensor([13])], + "glm_tts_text_token_len": [torch.tensor([6])], + }, + ) + + assert update["prompt_speech_token_len"].item() == 13 + assert update["glm_tts_text_token_len"].item() == 6 diff --git a/tests/model_executor/stage_input_processors/test_glm_tts_async_chunk.py b/tests/model_executor/stage_input_processors/test_glm_tts_async_chunk.py new file mode 100644 index 000000000..19bdaf7fe --- /dev/null +++ b/tests/model_executor/stage_input_processors/test_glm_tts_async_chunk.py @@ -0,0 +1,278 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +from types import SimpleNamespace + +import pytest +import torch + +from vllm_omni.engine.serialization import serialize_additional_information +from vllm_omni.model_executor.models.glm_tts.glm_tts_dit_wrapper import ( + split_request_ids, + valid_speech_tokens, +) +from vllm_omni.model_executor.stage_input_processors.glm_tts import ( + ar_to_dit, + ar_to_dit_async_chunk, +) + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + + +class _Request: + external_req_id = "req-1" + additional_information = None + + def is_finished(self) -> bool: + return False + + +def _transfer_manager() -> SimpleNamespace: + connector = SimpleNamespace( + config={ + "extra": { + "codec_chunk_frames": 25, + "codec_left_context_frames": 25, + } + } + ) + return SimpleNamespace(connector=connector) + + +def _progressive_transfer_manager() -> SimpleNamespace: + connector = SimpleNamespace( + config={ + "extra": { + "codec_chunk_frames": [25, 50, 200], + "codec_left_context_frames": 25, + } + } + ) + return SimpleNamespace(connector=connector) + + +def _source_output(mm: dict, request_id: str = "req-0") -> SimpleNamespace: + return SimpleNamespace( + request_id=request_id, + outputs=[ + SimpleNamespace( + multimodal_output=mm, + ) + ], + ) + + +def _audio_codes(payload) -> list[int]: + assert payload.codes is not None + assert payload.codes.audio is not None + return payload.codes.audio.tolist() + + +def test_ar_to_dit_filters_invalid_tokens_and_preserves_conditioning() -> None: + prompt_token = torch.tensor([[1, 2, 3]]) + prompt_feat = torch.randn(1, 4, 80) + embedding = torch.randn(1, 192) + source_outputs = [ + _source_output( + { + "speech_tokens": torch.tensor([[-1, 0, 7, -1, 32767]]), + "prompt_token": prompt_token, + "prompt_feat": prompt_feat, + "embedding": embedding, + } + ) + ] + + outputs = ar_to_dit(source_outputs) + + assert len(outputs) == 1 + assert outputs[0]["prompt_token_ids"] == [0, 7, 32767] + additional_info = outputs[0]["additional_information"] + assert additional_info["speech_tokens"] == [0, 7, 32767] + # Voice clone data stored in nested embed dict (matches async_chunk path) + embed = additional_info["embed"] + assert embed["speech_token"] is prompt_token + assert embed["speech_feat"] is prompt_feat + assert embed["embedding"] is embedding + # Meta dict enables block-causal attention in DiT wrapper + meta = additional_info["meta"] + assert meta["req_id"] == ["req-0"] + assert meta["left_context_size"] == 0 + assert meta["stream_finished"] is True + private = additional_info["kv_metadata"]["glm_tts"] + assert private["block_pattern"] == [3] # len([0, 7, 32767]) + assert private["chunk_sizes_history"] == [3] + + +def test_dit_explicit_empty_speech_tokens_override_scheduler_placeholder() -> None: + placeholder = torch.tensor([0], dtype=torch.long) + + token = valid_speech_tokens([], device=placeholder.device, fallback=placeholder) + + assert token.numel() == 0 + + +def test_ar_to_dit_async_chunk_restores_serialized_conditioning_payload() -> None: + transfer = _transfer_manager() + request = _Request() + prompt_token = torch.tensor([[1, 2, 3]]) + prompt_feat = torch.randn(1, 4, 80) + embedding = torch.randn(1, 192) + request.additional_information = serialize_additional_information( + { + "prompt_speech_token": prompt_token, + "prompt_feat": prompt_feat, + "embedding": embedding, + } + ) + + payload = ar_to_dit_async_chunk( + transfer, + {"speech_tokens": torch.tensor([[0]])}, + request, + is_finished=False, + ) + + assert payload is None + state = transfer.request_payload["req-1"]["_glm_tts_async_state"] + prompt_payload = state["prompt_payload"] + assert torch.equal(prompt_payload["prompt_speech_token"], prompt_token) + assert torch.equal(prompt_payload["prompt_feat"], prompt_feat) + assert torch.equal(prompt_payload["embedding"], embedding) + + +def test_split_request_ids_uses_seq_token_counts() -> None: + ids = torch.tensor([1, 2, 3, 4, 5, 6], dtype=torch.long) + + split = split_request_ids(ids, seq_token_counts=[2, 3, 1]) + + assert [part.tolist() for part in split] == [[1, 2], [3, 4, 5], [6]] + + +def test_ar_to_dit_async_chunk_emits_cumulative_prefixes() -> None: + transfer = _transfer_manager() + request = _Request() + payloads = [] + + for token in range(75): + payload = ar_to_dit_async_chunk( + transfer, + {"speech_tokens": torch.tensor([[token]])}, + request, + is_finished=False, + ) + if payload is not None: + payloads.append(payload) + + assert len(payloads) == 3 + assert _audio_codes(payloads[0]) == list(range(25)) + assert payloads[0].meta.left_context_size == 0 + assert payloads[0].kv_metadata["glm_tts"]["block_pattern"] == [25] + assert _audio_codes(payloads[1]) == list(range(50)) + assert payloads[1].meta.left_context_size == 25 + assert payloads[1].kv_metadata["glm_tts"]["block_pattern"] == [25] + assert _audio_codes(payloads[2]) == list(range(75)) + assert payloads[2].meta.left_context_size == 50 + assert payloads[2].kv_metadata["glm_tts"]["block_pattern"] == [25] + + +def test_ar_to_dit_async_chunk_keeps_progressive_block_pattern_fixed() -> None: + transfer = _progressive_transfer_manager() + request = _Request() + payloads = [] + + for token in range(30): + payload = ar_to_dit_async_chunk( + transfer, + {"speech_tokens": torch.tensor([[token]])}, + request, + is_finished=False, + ) + if payload is not None: + payloads.append(payload) + + final_payload = ar_to_dit_async_chunk( + transfer, + None, + request, + is_finished=True, + ) + + assert len(payloads) == 1 + assert payloads[0].kv_metadata["glm_tts"]["chunk_sizes_history"] == [25] + assert payloads[0].kv_metadata["glm_tts"]["block_pattern"] == [25, 50, 200] + assert final_payload is not None + assert final_payload.kv_metadata["glm_tts"]["chunk_sizes_history"] == [25, 5] + assert final_payload.kv_metadata["glm_tts"]["block_pattern"] == [25, 50, 200] + + +def test_ar_to_dit_async_chunk_terminal_boundary_is_metadata_only() -> None: + transfer = _transfer_manager() + request = _Request() + payloads = [] + + for token in range(50): + payload = ar_to_dit_async_chunk( + transfer, + {"speech_tokens": torch.tensor([[token]])}, + request, + is_finished=False, + ) + if payload is not None: + payloads.append(payload) + + final_payload = ar_to_dit_async_chunk( + transfer, + None, + request, + is_finished=True, + ) + + assert len(payloads) == 2 + assert final_payload is not None + assert _audio_codes(final_payload) == [] + assert final_payload.meta.left_context_size == 50 + assert final_payload.meta.req_id == ["req-1"] + assert bool(final_payload.meta.stream_finished.item()) + + +def test_ar_to_dit_async_chunk_finished_flushes_partial_unsent_tail() -> None: + transfer = _transfer_manager() + request = _Request() + payloads = [] + + for token in range(30): + payload = ar_to_dit_async_chunk( + transfer, + {"speech_tokens": torch.tensor([[token]])}, + request, + is_finished=False, + ) + if payload is not None: + payloads.append(payload) + + final_payload = ar_to_dit_async_chunk( + transfer, + None, + request, + is_finished=True, + ) + + assert len(payloads) == 1 + assert _audio_codes(payloads[0]) == list(range(25)) + assert payloads[0].meta.left_context_size == 0 + assert final_payload is not None + assert _audio_codes(final_payload) == list(range(30)) + assert final_payload.meta.left_context_size == 25 + assert final_payload.meta.req_id == ["req-1"] + assert bool(final_payload.meta.finished.item()) + assert bool(final_payload.meta.stream_finished.item()) + assert ( + ar_to_dit_async_chunk( + transfer, + None, + request, + is_finished=True, + ) + is None + ) diff --git a/vllm_omni/config/pipeline_registry.py b/vllm_omni/config/pipeline_registry.py index 555f35e17..1474e325c 100644 --- a/vllm_omni/config/pipeline_registry.py +++ b/vllm_omni/config/pipeline_registry.py @@ -97,6 +97,10 @@ _OMNI_PIPELINES: dict[str, tuple[str, str]] = { "vllm_omni.model_executor.models.voxtral_tts.pipeline", "VOXTRAL_TTS_PIPELINE", ), + "glm_tts": ( + "vllm_omni.model_executor.models.glm_tts.pipeline", + "GLM_TTS_PIPELINE", + ), "fish_qwen3_omni": ( "vllm_omni.model_executor.models.fish_speech.pipeline", "FISH_SPEECH_PIPELINE", diff --git a/vllm_omni/deploy/glm_tts.yaml b/vllm_omni/deploy/glm_tts.yaml new file mode 100644 index 000000000..1600b6152 --- /dev/null +++ b/vllm_omni/deploy/glm_tts.yaml @@ -0,0 +1,83 @@ +# GLM-TTS deploy: 2-stage AR -> DiT flow-matching. +# +# Default mode is async-chunk streaming through SharedMemoryConnector. +# Stage 0 (AR LLM) streams speech token chunks to Stage 1 (DiT + vocoder) +# via the shared-memory connector. Stage 1 runs as LLM_GENERATION type +# to support chunk_transfer_adapter consumption. +# +# Stage 1 keeps vLLM eager scheduling but enables the GLM-TTS bucketed DiT +# CUDA graph wrapper via hf_overrides.use_dit_cuda_graphs. Graph capture falls +# back to eager if a dynamic shape exceeds the captured buckets. +# +# Verified on 1x A40 48GB (both stages share GPU 0). +# Memory footprint ~16.6 GiB peak; fits 24GB cards with the settings below. +async_chunk: true +trust_remote_code: true + +connectors: + glm_tts_shm: + name: SharedMemoryConnector + extra: + codec_streaming: true + connector_get_sleep_s: 0.002 + connector_get_max_wait_first_chunk: 3000 + connector_get_max_wait: 300 + # Progressive chunk sizes matching original GLM-TTS defaults. + codec_chunk_frames: [25, 50, 200] + codec_left_context_frames: 25 + crossfade_sec: 0.1 + +stages: + - stage_id: 0 + max_num_seqs: 1 + gpu_memory_utilization: 0.55 + # BF16 for AR decode: memory-bandwidth bound on single-batch decode, + # bf16 halves HBM reads per step (~1.7x throughput on L4/A40). + dtype: bfloat16 + enforce_eager: false + async_scheduling: true + enable_prefix_caching: false + max_num_batched_tokens: 512 + max_model_len: 2048 + devices: "0" + output_connectors: + to_stage_1: glm_tts_shm + default_sampling_params: + temperature: 1.0 + top_k: 25 + top_p: 0.8 + max_tokens: 2048 + min_tokens: 1 + seed: 42 + detokenize: false + hf_overrides: + sample_method: ras + ras_top_p: 0.8 + ras_top_k: 25 + ras_win_size: 10 + ras_tau_r: 0.1 + min_token_text_ratio: 2.0 + max_token_text_ratio: 20.0 + skip_mm_profiling: true + + - stage_id: 1 + max_num_seqs: 1 + gpu_memory_utilization: 0.25 + enforce_eager: true + max_model_len: 8192 + max_num_batched_tokens: 2048 + # Force float32 for the flow-matching DiT stage. The Euler ODE solver + # (10 steps × 2 forward passes each) accumulates rounding errors in half + # precision that manifest as audible hiss on H20/H100. float32 matches + # the reference implementation and eliminates the noise floor. + dtype: float32 + devices: "0" + enable_prefix_caching: false + input_connectors: + from_stage_0: glm_tts_shm + default_sampling_params: + max_tokens: 2048 + hf_overrides: + use_dit_cuda_graphs: true + disable_hybrid_kv_cache_manager: true + skip_mm_profiling: true diff --git a/vllm_omni/engine/arg_utils.py b/vllm_omni/engine/arg_utils.py index 12a5a450a..c46222899 100644 --- a/vllm_omni/engine/arg_utils.py +++ b/vllm_omni/engine/arg_utils.py @@ -20,6 +20,7 @@ logger = init_logger(__name__) # Used when auto-injecting hf_overrides for models with missing config.json. _ARCH_TO_MODEL_TYPE: dict[str, str] = { "CosyVoice3Model": "cosyvoice3", + "GLMTTSForConditionalGeneration": "glm_tts", "OmniVoiceModel": "omnivoice", "VoxCPM2TalkerForConditionalGeneration": "voxcpm2", } @@ -27,6 +28,7 @@ _ARCH_TO_MODEL_TYPE: dict[str, str] = { # Maps model architecture names to tokenizer subfolder paths within HF repos. _TOKENIZER_SUBFOLDER_MAP: dict[str, str] = { "CosyVoice3Model": "CosyVoice-BlankEN", + "GLMTTSForConditionalGeneration": "vq32k-phoneme-tokenizer", } @@ -37,6 +39,10 @@ def _register_omni_hf_configs() -> None: from vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts import ( Qwen3TTSConfig, ) + from vllm_omni.transformers_utils.configs.cosyvoice3 import CosyVoice3Config + from vllm_omni.transformers_utils.configs.glm_tts import GLMTTSConfig + from vllm_omni.transformers_utils.configs.omnivoice import OmniVoiceConfig + from vllm_omni.transformers_utils.configs.voxcpm2 import VoxCPM2Config except Exception as exc: # pragma: no cover - best-effort optional registration logger.warning("Skipping omni HF config registration due to import error: %s", exc) return @@ -51,6 +57,10 @@ def _register_omni_hf_configs() -> None: for model_type, config_cls in [ ("qwen3_tts", Qwen3TTSConfig), + ("cosyvoice3", CosyVoice3Config), + ("glm_tts", GLMTTSConfig), + ("omnivoice", OmniVoiceConfig), + ("voxcpm2", VoxCPM2Config), ]: try: AutoConfig.register(model_type, config_cls) @@ -298,6 +308,7 @@ class OmniEngineArgs(EngineArgs): model_path, allow_patterns=[ f"{subfolder}/tokenizer*", + f"{subfolder}/tokenization_*", f"{subfolder}/special_tokens*", f"{subfolder}/vocab*", f"{subfolder}/merges*", diff --git a/vllm_omni/entrypoints/openai/serving_speech.py b/vllm_omni/entrypoints/openai/serving_speech.py index 3b7a2e747..10036978e 100644 --- a/vllm_omni/entrypoints/openai/serving_speech.py +++ b/vllm_omni/entrypoints/openai/serving_speech.py @@ -68,6 +68,7 @@ _COVO_AUDIO_MODEL_STAGES = {"fused_thinker_talker"} _VOXCPM2_TTS_MODEL_STAGES = {"latent_generator"} _MING_TTS_MODEL_STAGES = {"ming_tts"} _MOSS_TTS_MODEL_STAGES = {"moss_tts_nano"} +_GLM_TTS_MODEL_STAGES = {"glm_tts"} _TTS_MODEL_STAGES: set[str] = ( _VOXTRAL_TTS_MODEL_STAGES | _QWEN3_TTS_MODEL_STAGES @@ -78,6 +79,7 @@ _TTS_MODEL_STAGES: set[str] = ( | _VOXCPM2_TTS_MODEL_STAGES | _MING_TTS_MODEL_STAGES | _MOSS_TTS_MODEL_STAGES + | _GLM_TTS_MODEL_STAGES ) _SAMPLING_MAX_TOKENS_TTS_MODEL_TYPES = {"fish_tts", "qwen3_tts", "voxtral_tts", "cosyvoice3", "voxcpm2"} _TTS_LANGUAGES: set[str] = { @@ -335,6 +337,10 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): # Determine TTS model type or None self._tts_model_type = self._detect_tts_model_type() + # GLM-TTS lazy-cached resources (populated on first GLM-TTS request) + self._glm_tts_text_tokenizer: object | None = None + self._glm_tts_text_frontend: object | None = None + # Cache TTS configuration values (computed once, reused per request) self._max_instructions_length = self._compute_max_instructions_length() @@ -499,6 +505,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): return "ming_flash_omni_tts" if model_stage in _MOSS_TTS_MODEL_STAGES: return "moss_tts_nano" + if model_stage in _GLM_TTS_MODEL_STAGES: + return "glm_tts" return None def _compute_max_instructions_length(self) -> int: @@ -528,6 +536,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): # is no static speaker list to surface here. return set() try: + if self._tts_model_type == "glm_tts": + return set() if self._tts_model_type == "voxcpm2": return {"default"} if self._tts_model_type == "voxtral_tts": @@ -785,12 +795,13 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): voice_lower = request.voice.lower() if voice_lower not in self.uploaded_speakers: - if self._tts_model_type in ("cosyvoice3", "fish_tts", "omnivoice", "moss_tts_nano"): + if self._tts_model_type in ("cosyvoice3", "fish_tts", "omnivoice", "moss_tts_nano", "glm_tts"): label = { "cosyvoice3": "CosyVoice3", "fish_tts": "Fish Speech", "omnivoice": "OmniVoice", "moss_tts_nano": "MOSS-TTS-Nano", + "glm_tts": "GLM-TTS", }.get(self._tts_model_type, self._tts_model_type) return ( f"Unknown voice '{request.voice}'. {label} has no " @@ -1135,6 +1146,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): return self._validate_ming_tts_request(request) if self._tts_model_type == "moss_tts_nano": return self._validate_moss_tts_request(request) + if self._tts_model_type == "glm_tts": + return self._validate_glm_tts_request(request) return self._validate_qwen_tts_request(request) def _voxcpm2_encode(self, text: str) -> list[int]: @@ -1383,6 +1396,26 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): params["prompt_audio_array"] = [[wav_list, sr]] return params + def _validate_glm_tts_request(self, request: OpenAICreateSpeechRequest) -> str | None: + """Validate GLM-TTS request — requires ref_audio for voice cloning.""" + if not request.input or not request.input.strip(): + return "Input text cannot be empty" + + if request.ref_audio is None: + return "GLM-TTS requires 'ref_audio' for zero-shot voice cloning" + fmt_err = self._validate_ref_audio_format(request.ref_audio) + if fmt_err: + return fmt_err + if not request.ref_text or not request.ref_text.strip(): + return "GLM-TTS voice cloning requires 'ref_text' (transcript of the reference audio)" + + if request.max_new_tokens is not None: + if request.max_new_tokens < _TTS_MAX_NEW_TOKENS_MIN: + return f"max_new_tokens must be >= {_TTS_MAX_NEW_TOKENS_MIN}" + if request.max_new_tokens > _TTS_MAX_NEW_TOKENS_MAX: + return f"max_new_tokens cannot exceed {_TTS_MAX_NEW_TOKENS_MAX}" + return None + def _validate_fish_tts_request(self, request: OpenAICreateSpeechRequest) -> str | None: """Validate Fish Speech request parameters. Returns error message or None.""" if not request.input or not request.input.strip(): @@ -1898,6 +1931,105 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): ) return sampling_params_list + # ---- GLM-TTS helpers ---- + + async def _build_glm_tts_prompt( + self, + request: OpenAICreateSpeechRequest, + *, + has_inline_ref_audio: bool = False, + ) -> dict[str, Any]: + """Build prompt for GLM-TTS. + + Uses the multimodal processor path (same as CosyVoice3): + - prompt: synthesis text + - multi_modal_data["audio"]: (wav_samples, sr) reference audio + - mm_processor_kwargs["prompt_text"]: reference text transcript + + AR preprocess() builds [PromptText | Text | BOA | PromptSpeechTokens + ATS]. + DiT receives prompt_token, prompt_feat, embedding for conditioning. + """ + # Voice cloning requires ref_audio + ref_text + if request.ref_audio is not None and request.ref_text: + wav_samples, sr = await self._resolve_ref_audio(request.ref_audio) + audio_data = (np.asarray(wav_samples, dtype=np.float32), int(sr)) + + mm_kwargs: dict[str, Any] = { + "prompt_text": request.ref_text, + } + if request.voice: + voice_lower = request.voice.lower() + if voice_lower in self.uploaded_speakers and not has_inline_ref_audio: + mm_kwargs["voice_name"] = voice_lower + mm_kwargs["voice_created_at"] = self._voice_created_at(voice_lower) + + return { + "prompt": request.input, + "multi_modal_data": { + "audio": audio_data, + }, + "mm_processor_kwargs": mm_kwargs, + "additional_information": self._build_glm_tts_prefill_metadata( + request.input, + request.ref_text, + ), + } + + raise ValueError("GLM-TTS requires ref_audio and ref_text for voice cloning.") + + def _glm_tts_text_tokenizer_and_frontend(self): + from vllm_omni.model_executor.models.glm_tts.glm_tts import ( + load_glm_tts_tokenizer, + resolve_glm_tts_tokenizer_path, + ) + from vllm_omni.model_executor.models.glm_tts.text_frontend import GLMTTSTextFrontend + + cached = self._glm_tts_text_tokenizer + if cached is None: + model_name_or_path = self.engine_client.model_config.model + tokenizer_path = getattr(self.engine_client.model_config, "tokenizer", None) + if tokenizer_path is None: + tokenizer_path = resolve_glm_tts_tokenizer_path(model_name_or_path) + cached = load_glm_tts_tokenizer( + tokenizer_path, + model_name_or_path=model_name_or_path, + trust_remote_code=bool(getattr(self.engine_client.model_config, "trust_remote_code", False)), + ) + self._glm_tts_text_tokenizer = cached + + frontend = self._glm_tts_text_frontend + if frontend is None: + frontend = GLMTTSTextFrontend() + self._glm_tts_text_frontend = frontend + return cached, frontend + + def _estimate_glm_tts_text_token_len(self, text: str | None, *, add_trailing_space: bool = False) -> int: + """Estimate GLM-TTS normalized text length with the model tokenizer.""" + cached, frontend = self._glm_tts_text_tokenizer_and_frontend() + text = text or "" + normalized = frontend.text_normalize(text) or text + normalized = normalized.strip() + if add_trailing_space and normalized: + normalized = f"{normalized} " + return max(1, len(cached.encode(normalized))) + + def _build_glm_tts_prefill_metadata(self, text: str, prompt_text: str | None) -> dict[str, Any]: + """Build GLM-TTS processor length metadata for additional_information. + + The model preprocess hook runs before postprocess can mirror MM kwargs + into additional_information, so these scalar fields must originate from + the request payload rather than runner-level mm_features. + """ + text_len = self._estimate_glm_tts_text_token_len(text) + prompt_text_len = ( + self._estimate_glm_tts_text_token_len(prompt_text, add_trailing_space=True) if prompt_text else 0 + ) + return { + "glm_tts_text_token_len": [text_len], + "glm_tts_prompt_text_token_len": [prompt_text_len], + "input_len": [prompt_text_len + text_len + 1], + } + # ---- Ming-flash-omni standalone-talker (TTS) helpers ---- def _build_ming_prompt(self, request: OpenAICreateSpeechRequest) -> dict[str, Any]: @@ -1963,7 +2095,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): # Resolve uploaded voice for non-Qwen3 models. # Qwen3 TTS has its own uploaded voice handling in _build_tts_params(). has_inline_ref_audio = request.ref_audio is not None - if self._tts_model_type in ("fish_tts", "cosyvoice3", "moss_tts_nano"): + if self._tts_model_type in ("fish_tts", "cosyvoice3", "moss_tts_nano", "glm_tts"): err = self._apply_uploaded_speaker(request) if err: raise ValueError(err) @@ -2041,6 +2173,9 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): elif self._tts_model_type == "cosyvoice3": prompt = await self._build_cosyvoice3_prompt(request, has_inline_ref_audio=has_inline_ref_audio) tts_params = {} + elif self._tts_model_type == "glm_tts": + prompt = await self._build_glm_tts_prompt(request, has_inline_ref_audio=has_inline_ref_audio) + tts_params = {} elif self._tts_model_type == "ming_flash_omni_tts": prompt = self._build_ming_prompt(request) tts_params = {} @@ -2112,6 +2247,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): model_type = "ming_flash_omni_tts" elif self._tts_model_type == "moss_tts_nano": model_type = "moss_tts_nano" + elif self._tts_model_type == "glm_tts": + model_type = "glm_tts" elif self._is_tts: model_type = tts_params.get("task_type", ["unknown"])[0] else: @@ -2129,6 +2266,57 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): if self._tts_model_type == "cosyvoice3" and sampling_params_list: sampling_params_list = self._apply_cosyvoice3_dynamic_tokens(sampling_params_list, request) + # GLM-TTS: set dynamic min/max tokens based on text length. + if self._tts_model_type == "glm_tts" and sampling_params_list: + import copy + + sampling_params_list = copy.deepcopy(sampling_params_list) + glm_metadata = prompt.get("additional_information") if isinstance(prompt, dict) else None + text_len_value = None + if isinstance(glm_metadata, dict): + text_len_value = glm_metadata.get("glm_tts_text_token_len") + if isinstance(text_len_value, list) and text_len_value: + text_len_value = text_len_value[0] + text_token_len = ( + int(text_len_value) + if text_len_value is not None + else self._estimate_glm_tts_text_token_len(request.input) + ) + hf_cfg = self.model_config.hf_config + min_ratio = getattr(hf_cfg, "min_token_text_ratio", 2) + max_ratio = getattr(hf_cfg, "max_token_text_ratio", 20) + stage_min_tokens = getattr(sampling_params_list[0], "min_tokens", None) + stage_max_tokens = getattr(sampling_params_list[0], "max_tokens", None) + cap_candidates = [int(cap) for cap in (stage_max_tokens, request.max_new_tokens) if cap is not None] + hard_cap = min(cap_candidates) if cap_candidates else None + + min_tokens = max(1, int(text_token_len * min_ratio)) + if stage_min_tokens is not None: + min_tokens = max(min_tokens, int(stage_min_tokens)) + if hard_cap is not None: + min_tokens = min(min_tokens, hard_cap) + + max_tokens = max(min_tokens, int(text_token_len * max_ratio)) + if hard_cap is not None: + max_tokens = min(max_tokens, hard_cap) + sampling_params_list[0].min_tokens = min_tokens + sampling_params_list[0].max_tokens = max_tokens + seed = getattr(request, "seed", None) + if seed is not None: + sampling_params_list[0].seed = seed + logger.info( + "GLM-TTS dynamic tokens: text_tokens=%d, min_ratio=%s, max_ratio=%s, " + "stage_min=%s, stage_max=%s, request_max=%s, min_tokens=%d, max_tokens=%d", + text_token_len, + min_ratio, + max_ratio, + stage_min_tokens, + stage_max_tokens, + request.max_new_tokens, + min_tokens, + max_tokens, + ) + # Apply model-specific extra parameters if request.extra_params is not None and sampling_params_list: if not isinstance(request.extra_params, dict): diff --git a/vllm_omni/entrypoints/utils.py b/vllm_omni/entrypoints/utils.py index de7eb5f4c..86a0d8182 100644 --- a/vllm_omni/entrypoints/utils.py +++ b/vllm_omni/entrypoints/utils.py @@ -329,11 +329,16 @@ def resolve_model_config_path(model: str) -> str: except Exception as e: raise ValueError(f"Failed to read config.json for model: {model}. Error: {e}") from e else: - raise ValueError( - f"Could not determine model_type for model: {model}. " - f"Model is not in standard transformers format and does not have model_index.json. " - f"Please ensure the model has proper configuration files with 'model_type' field" - ) + # No config.json at repo root (e.g. GLM-TTS stores configs in + # subdirectories only). Try matching against registered deploy + # YAML filenames before giving up. + model_type = _try_resolve_omni_model_type(model) + if model_type is None: + raise ValueError( + f"Could not determine model_type for model: {model}. " + f"Model is not in standard transformers format and does not have model_index.json. " + f"Please ensure the model has proper configuration files with 'model_type' field" + ) default_config_path = current_omni_platform.get_default_stage_config_path() if model_type in _DIFFUSERS_CLASS_TO_CONFIG: diff --git a/vllm_omni/model_executor/models/common/nucleus_ras_sampling.py b/vllm_omni/model_executor/models/common/nucleus_ras_sampling.py new file mode 100644 index 000000000..34573ae7d --- /dev/null +++ b/vllm_omni/model_executor/models/common/nucleus_ras_sampling.py @@ -0,0 +1,118 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Shared TTS sampling primitives: nucleus (top-p/top-k) and RAS. + +Used by CosyVoice3 and GLM-TTS (and any future TTS model with RAS). +""" + +from __future__ import annotations + +from collections.abc import Sequence + +import torch + + +def multinomial_sample( + probs: torch.Tensor, + generator: torch.Generator | None = None, +) -> torch.Tensor: + """Draw one sample from a categorical distribution.""" + return torch.multinomial(probs, 1, replacement=True, generator=generator).reshape(()) + + +def nucleus_sample_one( + weighted_scores: torch.Tensor, + *, + top_p: float, + top_k: int, + generator: torch.Generator | None = None, +) -> int: + """Sample one token using nucleus (top-p + top-k) filtering. + + ``weighted_scores`` should be log-softmax-ed logits (for RAS callers) + or raw logits (softmax is applied internally). + + Vectorized via ``torch.cumsum`` — no Python loop over the vocabulary. + """ + # Apply top-k before sort. GLM-TTS and CosyVoice3 usually run with + # top_k=25, so sorting the whole audio vocabulary on every AR step is + # avoidable work in the hottest path. Keep probabilities normalized over + # the full vocabulary so the top-p cumulative mass matches the old path. + if top_k > 0 and int(top_k) < int(weighted_scores.numel()): + top_scores, sorted_idx = torch.topk( + weighted_scores, + k=int(top_k), + dim=0, + largest=True, + sorted=True, + ) + sorted_prob = (top_scores - torch.logsumexp(top_scores, dim=0)).exp() + else: + probs = weighted_scores.softmax(dim=0) + sorted_prob, sorted_idx = probs.sort(descending=True, stable=True) + + # Apply top-p (nucleus) filtering: keep the smallest prefix whose + # cumulative probability exceeds top_p, always including the first token. + # Use float32 for cumulative sum to match the original Python float64 + # accumulation loop. Low-precision (fp16/bf16) cumsum over ~32K vocab + # causes rounding drift that shifts the top-p boundary, changing which + # tokens are kept and ultimately producing different AR sequences. + sorted_prob_f32 = sorted_prob.float() + cum_prob = sorted_prob_f32.cumsum(dim=0) + # Mask: include tokens where cumsum *before* this token is still < top_p + mask = (cum_prob - sorted_prob_f32) < top_p + # Safety: ensure top token is always kept. With sorted descending + # probabilities, mask[0] is always True when top_p > 0 (since the + # cumsum-before-self of the first element is 0 < top_p). Setting it + # unconditionally avoids a GPU→CPU sync from the old `mask.any()` check. + mask[0] = True + + kept_probs = torch.nan_to_num(sorted_prob[mask], nan=0.0, posinf=0.0, neginf=0.0).clamp_min_(0.0) + kept_indices = sorted_idx[mask] + # ``torch.multinomial`` accepts unnormalized weights, but not an all-zero + # vector. Handle the extreme underflow case on-device by forcing the top + # retained token to be selectable without adding another CPU sync. + kept_probs[0] = torch.where( + kept_probs.sum() > 0, + kept_probs[0], + torch.ones_like(kept_probs[0]), + ) + + sample_pos = multinomial_sample(kept_probs, generator=generator) + return int(kept_indices[sample_pos].item()) + + +def ras_sample_one( + weighted_scores: torch.Tensor, + decoded_tokens: Sequence[int], + *, + top_p: float, + top_k: int, + win_size: int, + tau_r: float, + generator: torch.Generator | None = None, +) -> int: + """Repetition-Aware Sampling following GLM-TTS/CosyVoice. + + If the nucleus-sampled token appears too often in the recent window, + upstream samples once from the original full distribution instead of + masking the repeated token. This matters for GLM-TTS because masking the + dominant repeated token can artificially raise EOA probability right after + the minimum length guard is lifted. + """ + + def _random_sample_one() -> int: + return int(multinomial_sample(weighted_scores.softmax(dim=0), generator=generator).item()) + + top_id = nucleus_sample_one( + weighted_scores, + top_p=top_p, + top_k=top_k, + generator=generator, + ) + if win_size > 0 and decoded_tokens: + recent = decoded_tokens[-win_size:] + rep_num = sum(1 for token in recent if int(token) == top_id) + if rep_num >= win_size * tau_r: + top_id = _random_sample_one() + return top_id diff --git a/vllm_omni/model_executor/models/common/whisper_vq.py b/vllm_omni/model_executor/models/common/whisper_vq.py new file mode 100644 index 000000000..9d7af76b5 --- /dev/null +++ b/vllm_omni/model_executor/models/common/whisper_vq.py @@ -0,0 +1,291 @@ +# Copyright 2022 The OpenAI Authors and The HuggingFace Inc. team. All rights reserved. +# 2025 Zhipu AI Inc (authors: CogAudio Group Members) +# SPDX-License-Identifier: Apache-2.0 +"""WhisperVQEncoder: HF WhisperEncoder + VQ codebook + inter-layer pooling. + +Built on standard ``WhisperConfig``. VQ-specific parameters are patched onto +the config object after ``from_pretrained`` so that callers never need a +separate config class. GLM-TTS (and future VQ-based TTS models) only need:: + + from transformers import WhisperConfig + from vllm_omni.model_executor.models.common.whisper_vq import WhisperVQEncoder + + cfg = WhisperConfig.from_pretrained(checkpoint_dir) + cfg.pooling_kernel_size = None # passed through from checkpoint + cfg.pooling_type = "max" # or set by caller + cfg.pooling_position = 0 + cfg.quantize_vocab_size = 32768 + cfg.quantize_position = 16 + cfg.quantize_encoder_only = True + model = WhisperVQEncoder(cfg) +""" + +from __future__ import annotations + +import math +import re +from dataclasses import dataclass +from typing import Any + +import torch +import torch.nn.functional as F +from torch import Tensor, nn +from transformers import WhisperConfig +from transformers.modeling_outputs import BaseModelOutput +from transformers.models.whisper.modeling_whisper import WhisperEncoder + +# --------------------------------------------------------------------------- +# helpers +# --------------------------------------------------------------------------- + + +@dataclass +class QuantizedBaseModelOutput(BaseModelOutput): + quantized_token_ids: torch.LongTensor | None = None + + +def vector_quantize(inputs: Tensor, codebook: Tensor) -> tuple[Tensor, Tensor, Tensor]: + """Nearest-neighbour codebook lookup.""" + embedding_size = codebook.size(1) + inputs_flatten = inputs.reshape(-1, embedding_size) + codebook_sqr = torch.sum(codebook**2, dim=1) + inputs_sqr = torch.sum(inputs_flatten**2, dim=1, keepdim=True) + distances = torch.addmm( + codebook_sqr + inputs_sqr, + inputs_flatten, + codebook.t(), + alpha=-2.0, + beta=1.0, + ) + _, indices_flatten = torch.min(distances, dim=1) + codes_flatten = torch.index_select(codebook, dim=0, index=indices_flatten) + codes = codes_flatten.view_as(inputs) + return codes, indices_flatten, distances + + +_LEGACY_LAYER_REMAP = ( + (re.compile(r"^layers\.(\d+)\.attn\.query\."), r"layers.\1.self_attn.q_proj."), + (re.compile(r"^layers\.(\d+)\.attn\.key\."), r"layers.\1.self_attn.k_proj."), + (re.compile(r"^layers\.(\d+)\.attn\.value\."), r"layers.\1.self_attn.v_proj."), + (re.compile(r"^layers\.(\d+)\.attn\.out\."), r"layers.\1.self_attn.out_proj."), + (re.compile(r"^layers\.(\d+)\.attn_ln\."), r"layers.\1.self_attn_layer_norm."), + (re.compile(r"^layers\.(\d+)\.mlp_ln\."), r"layers.\1.final_layer_norm."), + (re.compile(r"^layers\.(\d+)\.mlp\.0\."), r"layers.\1.fc1."), + (re.compile(r"^layers\.(\d+)\.mlp\.2\."), r"layers.\1.fc2."), +) + + +def remap_legacy_whisper_vq_state_dict(state_dict: dict[str, Tensor]) -> dict[str, Tensor]: + """Map CogAudio/GLM-TTS fork parameter names onto HF WhisperEncoder names.""" + remapped: dict[str, Tensor] = {} + for name, tensor in state_dict.items(): + new_name = name + if new_name == "embed_positions": + new_name = "embed_positions.weight" + for pattern, replacement in _LEGACY_LAYER_REMAP: + new_name = pattern.sub(replacement, new_name) + remapped[new_name] = tensor + return remapped + + +# --------------------------------------------------------------------------- +# main encoder class +# --------------------------------------------------------------------------- + + +class WhisperVQEncoder(WhisperEncoder): + """HF Whisper encoder with optional VQ codebook and pooling. + + Uses a standard ``WhisperConfig`` with the following VQ-specific attrs + patched on by the caller (or loaded from the checkpoint's config.json): + + * ``pooling_kernel_size`` -- int | None + * ``pooling_type`` -- "max" | "avg" + * ``pooling_position`` -- int (0-based layer index) + * ``quantize_vocab_size`` -- int | None + * ``quantize_position`` -- int (0-based layer index) + * ``quantize_encoder_only`` -- bool + """ + + def __init__(self, config: WhisperConfig): + super().__init__(config) + embed_dim = config.d_model + max_source_positions = config.max_source_positions + + # Truncate layers when quantize_encoder_only is set. + qpos = int(getattr(config, "quantize_position", 0) or 0) + if getattr(config, "quantize_encoder_only", False): + self.layers = nn.ModuleList(list(self.layers[:qpos])) + self.layer_norm = None + else: + qpos = max(qpos, int(config.encoder_layers)) # never exceed total + + # Optional inter-layer pooling. + self.pooling_layer: nn.Module | None = None + ksize = getattr(config, "pooling_kernel_size", None) + if ksize is not None: + ptype = str(getattr(config, "pooling_type", "max") or "max") + self.pooling_layer = { + "max": nn.MaxPool1d(kernel_size=int(ksize)), + "avg": nn.AvgPool1d(kernel_size=int(ksize)), + }[ptype] + + # Optional vector-quantization codebook. + self.codebook: nn.Embedding | None = None + self.embed_positions2: nn.Embedding | None = None + vq_sz = getattr(config, "quantize_vocab_size", None) + if vq_sz is not None: + self.codebook = nn.Embedding(int(vq_sz), embed_dim) + pos2_len = max_source_positions + if ksize is not None: + pos2_len = math.ceil(max_source_positions / int(ksize)) + self.embed_positions2 = nn.Embedding(pos2_len, embed_dim) + + # cached for forward() + self._pooling_position = int(getattr(config, "pooling_position", 0) or 0) + self._quantize_position = qpos if vq_sz is not None else -1 + + # ------------------------------------------------------------------ + # forward / helpers + # ------------------------------------------------------------------ + + @property + def device(self) -> torch.device: # type: ignore[override] + return self.conv1.weight.device + + @property + def dtype(self) -> torch.dtype: # type: ignore[override] + return self.conv1.weight.dtype + + def load_state_dict( + self, + state_dict: dict[str, Tensor], + strict: bool = True, + assign: bool = False, + ): # type: ignore[override] + remapped = remap_legacy_whisper_vq_state_dict(state_dict) + try: + return super().load_state_dict(remapped, strict=strict, assign=assign) + except TypeError: + return super().load_state_dict(remapped, strict=strict) + + def forward( + self, + input_features: Tensor, + attention_mask: Tensor | None = None, + **_: Any, + ) -> QuantizedBaseModelOutput: + batch_size, _, _ = input_features.shape + hidden_states = F.gelu(self.conv1(input_features)) + hidden_states = F.gelu(self.conv2(hidden_states)) + hidden_states = hidden_states.permute(0, 2, 1) + seq_len = int(hidden_states.shape[1]) + + pos_embed = self._position_embeddings(seq_len, hidden_states.device, hidden_states.dtype) + hidden_states = hidden_states + pos_embed + + valid_mask = self._downsample_attention_mask(attention_mask, batch_size, seq_len, hidden_states.device) + hidden_states = hidden_states.masked_fill(~valid_mask.unsqueeze(-1), 0) + + quantized_token_ids = None + for idx, layer in enumerate(self.layers): + layer_mask = self._make_layer_attention_mask(valid_mask, hidden_states.dtype) + layer_outputs = layer( + hidden_states, + attention_mask=layer_mask, + layer_head_mask=None, + output_attentions=False, + ) + hidden_states = layer_outputs[0] if isinstance(layer_outputs, tuple) else layer_outputs + hidden_states = hidden_states.masked_fill(~valid_mask.unsqueeze(-1), 0) + + if idx + 1 == self._pooling_position and self.pooling_layer is not None: + hidden_states, valid_mask = self._apply_pooling(hidden_states, valid_mask) + + if idx + 1 == self._quantize_position and self.codebook is not None: + hidden_states, quantized_token_ids = self._apply_vq(hidden_states, valid_mask) + + if self.layer_norm is not None: + hidden_states = self.layer_norm(hidden_states) + hidden_states = hidden_states.masked_fill(~valid_mask.unsqueeze(-1), 0) + + return QuantizedBaseModelOutput( + last_hidden_state=hidden_states, + quantized_token_ids=quantized_token_ids, + ) + + def _position_embeddings(self, seq_len: int, device: torch.device, dtype: torch.dtype) -> Tensor: + ep = self.embed_positions + if isinstance(ep, nn.Embedding): + positions = torch.arange(seq_len, device=device) + return ep(positions).to(dtype=dtype).unsqueeze(0) + return ep[:seq_len].to(device=device, dtype=dtype).unsqueeze(0) + + def _downsample_attention_mask( + self, + attention_mask: Tensor | None, + batch_size: int, + seq_len: int, + device: torch.device, + ) -> Tensor: + if attention_mask is None: + return torch.ones(batch_size, seq_len, dtype=torch.bool, device=device) + conv_stride = int(self.conv1.stride[0]) * int(self.conv2.stride[0]) + downsampled = attention_mask[:, ::conv_stride].to(device=device, dtype=torch.bool) + if downsampled.shape[1] < seq_len: + pad = seq_len - int(downsampled.shape[1]) + downsampled = F.pad(downsampled, (0, pad), value=False) + return downsampled[:, :seq_len] + + @staticmethod + def _make_layer_attention_mask(valid_mask: Tensor, dtype: torch.dtype) -> Tensor: + min_value = torch.finfo(dtype).min + additive = torch.zeros(valid_mask.shape, dtype=dtype, device=valid_mask.device) + additive = additive.masked_fill(~valid_mask, min_value) + return additive[:, None, None, :] + + def _apply_pooling(self, hidden_states: Tensor, valid_mask: Tensor) -> tuple[Tensor, Tensor]: + k = int(self.config.pooling_kernel_size) # type: ignore[attr-defined] + pooled_segments: list[Tensor] = [] + pooled_masks: list[Tensor] = [] + for batch_idx in range(int(hidden_states.shape[0])): + length = int(valid_mask[batch_idx].sum().item()) + segment = hidden_states[batch_idx, :length] + if segment.numel() == 0: + pooled = hidden_states.new_zeros((0, hidden_states.shape[-1])) + else: + segment_t = segment.unsqueeze(0).transpose(1, 2) + if int(segment_t.shape[-1]) % k != 0: + segment_t = F.pad(segment_t, (0, k - int(segment_t.shape[-1]) % k)) + pooled = self.pooling_layer(segment_t).transpose(1, 2).squeeze(0) # type: ignore[misc] + pooled_segments.append(pooled) + pooled_masks.append(torch.ones(pooled.shape[0], dtype=torch.bool, device=hidden_states.device)) + + max_len = max((int(seg.shape[0]) for seg in pooled_segments), default=0) + output = hidden_states.new_zeros((hidden_states.shape[0], max_len, hidden_states.shape[-1])) + output_mask = torch.zeros(hidden_states.shape[0], max_len, dtype=torch.bool, device=hidden_states.device) + for i, segment in enumerate(pooled_segments): + if segment.shape[0] > 0: + output[i, : segment.shape[0]] = segment + output_mask[i, : segment.shape[0]] = pooled_masks[i] + return output, output_mask + + def _apply_vq(self, hidden_states: Tensor, valid_mask: Tensor) -> tuple[Tensor, Tensor | None]: + output = hidden_states.clone() + token_ids = torch.full( + (hidden_states.shape[0], hidden_states.shape[1]), + -1, + dtype=torch.long, + device=hidden_states.device, + ) + for batch_idx in range(int(hidden_states.shape[0])): + length = int(valid_mask[batch_idx].sum().item()) + if length <= 0: + continue + segment = hidden_states[batch_idx : batch_idx + 1, :length] + quantized, indices, _ = vector_quantize(segment, self.codebook.weight) # type: ignore[union-attr] + if self.embed_positions2 is not None: + quantized = quantized + self.embed_positions2.weight[:length].to(dtype=quantized.dtype).unsqueeze(0) + output[batch_idx, :length] = quantized.squeeze(0) + token_ids[batch_idx, :length] = indices.reshape(-1) + return output.masked_fill(~valid_mask.unsqueeze(-1), 0), token_ids diff --git a/vllm_omni/model_executor/models/glm_tts/__init__.py b/vllm_omni/model_executor/models/glm_tts/__init__.py new file mode 100644 index 000000000..9618227ca --- /dev/null +++ b/vllm_omni/model_executor/models/glm_tts/__init__.py @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""GLM-TTS model support for vLLM-Omni. + +GLM-TTS is a two-stage text-to-speech system: + - Stage 0 (LLM): Llama-based AR model generates speech tokens from text + - Stage 1 (DiT): Flow matching model converts speech tokens to mel-spectrogram + +Reference: https://github.com/zai-org/GLM-TTS +""" + +from vllm_omni.model_executor.models.glm_tts.glm_tts import ( + GLMTTSForConditionalGeneration, +) + +__all__ = [ + "GLMTTSForConditionalGeneration", +] diff --git a/vllm_omni/model_executor/models/glm_tts/glm_tts.py b/vllm_omni/model_executor/models/glm_tts/glm_tts.py new file mode 100644 index 000000000..6e14840e2 --- /dev/null +++ b/vllm_omni/model_executor/models/glm_tts/glm_tts.py @@ -0,0 +1,1423 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""GLM-TTS AR Model (Stage 0): Text → Speech Tokens. + +Based on Llama architecture, generates speech token sequences from input text. +Analogous to Fish Speech Slow AR and Qwen3-TTS Talker models. +""" + +from __future__ import annotations + +import os +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import replace +from typing import Any + +import torch +import torch.nn as nn +from transformers import AutoTokenizer +from transformers.feature_extraction_utils import BatchFeature +from transformers.utils.hub import cached_file +from vllm.config import VllmConfig +from vllm.config.multimodal import BaseDummyOptions +from vllm.distributed import get_pp_group +from vllm.inputs import MultiModalDataDict +from vllm.logger import init_logger +from vllm.model_executor.layers.logits_processor import LogitsProcessor +from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead +from vllm.model_executor.model_loader.weight_utils import default_weight_loader +from vllm.model_executor.models.interfaces import SupportsMultiModal +from vllm.model_executor.models.llama import LlamaModel +from vllm.model_executor.models.utils import ( + AutoWeightsLoader, + PPMissingLayer, + WeightsMapper, + maybe_prefix, +) +from vllm.multimodal import MULTIMODAL_REGISTRY +from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargsItems +from vllm.multimodal.parse import MultiModalDataItems, MultiModalDataParser +from vllm.multimodal.processing import ( + BaseDummyInputsBuilder, + BaseMultiModalProcessor, + BaseProcessingInfo, + ProcessorInputs, + PromptIndexTargets, + PromptInsertion, + PromptUpdate, +) +from vllm.sequence import IntermediateTensors +from vllm.v1.outputs import SamplerOutput +from vllm.v1.sample.metadata import SamplingMetadata +from vllm.v1.sample.sampler import Sampler + +from vllm_omni.model_executor.models.common.nucleus_ras_sampling import ras_sample_one as _ras_sample_one +from vllm_omni.model_executor.models.output_templates import OmniOutput +from vllm_omni.transformers_utils.configs.glm_tts import GLMTTSConfig +from vllm_omni.utils.speaker_cache import get_speaker_cache + +from .text_frontend import GLMTTSTextFrontend +from .voice_clone import ( + extract_prompt_feat, + extract_prompt_speech_token, + extract_spk_embedding, + load_voice_clone_frontend, +) + +logger = init_logger(__name__) + +_GLM_TTS_DEFAULT_REPO_ID = "zai-org/GLM-TTS" +_GLM_TTS_TOKENIZER_SUBDIR = "vq32k-phoneme-tokenizer" +_GLM_TTS_MAX_PROMPT_SPEECH_TOKENS = 1024 + + +def _req_float(param: torch.Tensor | None, req_idx: int, default: float) -> float: + if param is None or param.numel() == 0: + return default + index = min(req_idx, int(param.numel()) - 1) + return float(param.reshape(-1)[index].item()) + + +def _tensor_param_values( + param: torch.Tensor | None, + count: int, + default: float, +) -> list[float]: + if param is None or param.numel() == 0: + return [default] * count + values = param.reshape(-1).detach().cpu().tolist() + if not values: + return [default] * count + if len(values) < count: + values.extend([values[-1]] * (count - len(values))) + return [float(value) for value in values[:count]] + + +def resolve_glm_tts_tokenizer_path(model_name_or_path: Any) -> str: + """Resolve vq32k-phoneme-tokenizer directory to a local path. + + For local model dirs, scans for the tokenizer subfolder on disk. + For HF hub names, downloads the tokenizer subfolder via + ``snapshot_download`` (same pattern as CosyVoice3's + ``_ensure_cached_runtime_components`` and + ``arg_utils._TOKENIZER_SUBFOLDER_MAP``). + """ + model_path = os.fspath(model_name_or_path) + if os.path.exists(model_path): + for candidate in [ + os.path.join(model_path, _GLM_TTS_TOKENIZER_SUBDIR), + os.path.join(os.path.dirname(model_path), _GLM_TTS_TOKENIZER_SUBDIR), + ]: + if os.path.isdir(candidate): + return candidate + return model_path + + from huggingface_hub import snapshot_download + + local_dir = snapshot_download( + model_path, + allow_patterns=[ + f"{_GLM_TTS_TOKENIZER_SUBDIR}/tokenizer*", + f"{_GLM_TTS_TOKENIZER_SUBDIR}/tokenization_*", + f"{_GLM_TTS_TOKENIZER_SUBDIR}/special_tokens*", + f"{_GLM_TTS_TOKENIZER_SUBDIR}/vocab*", + f"{_GLM_TTS_TOKENIZER_SUBDIR}/merges*", + f"{_GLM_TTS_TOKENIZER_SUBDIR}/added_tokens*", + ], + ) + candidate = os.path.join(local_dir, _GLM_TTS_TOKENIZER_SUBDIR) + if os.path.isdir(candidate): + return candidate + return model_path + + +def resolve_glm_tts_model_dir( + model_name_or_path: Any, + *, + tokenizer_path: Any | None = None, + required_files: Sequence[str] = (), + optional_files: Sequence[str] = (), +) -> str: + """Resolve GLM-TTS repo root: local dir → tokenizer parent → cached_file → snapshot.""" + model_path = os.fspath(model_name_or_path) + if os.path.isdir(model_path): + return os.path.abspath(model_path) + + def _has_files(root: str) -> bool: + has_req = all(os.path.isfile(os.path.join(root, *f.split("/"))) for f in required_files) + has_opt = not optional_files or any(os.path.isfile(os.path.join(root, *f.split("/"))) for f in optional_files) + return has_req and has_opt + + # Try tokenizer parent directory + if tokenizer_path: + tp = os.path.abspath(os.path.normpath(os.fspath(tokenizer_path))) + root = os.path.dirname(tp) if os.path.basename(tp) == _GLM_TTS_TOKENIZER_SUBDIR else tp + if os.path.isdir(root) and _has_files(root): + return root + + # Try cached_file to locate individual files without full snapshot + for filename in list(required_files) + list(optional_files): + try: + resolved = cached_file(model_name_or_path, filename) + if resolved: + root = os.path.abspath(resolved) + for _ in filename.split("/"): + root = os.path.dirname(root) + if _has_files(root): + return root + except Exception: + pass + + from huggingface_hub import snapshot_download + + return snapshot_download(model_name_or_path) + + +def _first_glm_tts_value(value: Any) -> Any: + return value[0] if isinstance(value, list) and value else value + + +def _glm_tts_int_value(value: Any) -> int | None: + """Extract a scalar integer from vLLM multimodal/runtime wrappers.""" + value = _first_glm_tts_value(value) + if isinstance(value, torch.Tensor): + if value.numel() == 0: + return None + return int(value.reshape(-1)[0].item()) + if isinstance(value, (int, float)): + return int(value) + return None + + +def _decode_glm_tts_audio_data(value: Any) -> tuple[torch.Tensor | None, int | None]: + """Extract (waveform_tensor, sample_rate) from multimodal input.""" + if isinstance(value, list) and len(value) == 1: + value = value[0] + if isinstance(value, torch.Tensor): + return value, None + if isinstance(value, (tuple, list)) and len(value) == 2 and isinstance(value[1], (int, float)): + return torch.as_tensor(value[0]), int(value[1]) + if hasattr(value, "shape"): # numpy array + return torch.as_tensor(value), None + return None, None + + +_glm_tts_tokenizer_cache: dict[str, Any] = {} + + +def load_glm_tts_tokenizer( + tokenizer_path: Any, + *, + model_name_or_path: Any | None = None, + trust_remote_code: bool = False, + **kwargs: Any, +) -> Any: + """Load GLM-TTS phoneme tokenizer (ChatGLM4Tokenizer, slow-only). + + Expects a resolved local directory (use ``resolve_glm_tts_tokenizer_path`` + to handle HF hub names first). Results are cached by *tokenizer_path*. + """ + cache_key = str(tokenizer_path) + cached = _glm_tts_tokenizer_cache.get(cache_key) + if cached is not None: + return cached + + tokenizer = AutoTokenizer.from_pretrained( + tokenizer_path, + use_fast=False, + trust_remote_code=trust_remote_code, + **kwargs, + ) + _glm_tts_tokenizer_cache[cache_key] = tokenizer + return tokenizer + + +def get_glm_tts_special_token_ids(tokenizer: Any) -> dict[str, int]: + """Resolve GLM-TTS special IDs from the phoneme/audio tokenizer.""" + special_tokens = { + "ats": "<|audio_0|>", + "ate": "<|audio_32767|>", + "boa": "<|begin_of_audio|>", + "eoa": "<|user|>", + "pad": "<|endoftext|>", + } + + result: dict[str, int] = {} + for key, token_str in special_tokens.items(): + token_ids = tokenizer.encode(token_str, add_special_tokens=False) + if len(token_ids) != 1: + raise ValueError(f"Token '{key}' ({token_str}) should encode to single ID, got: {token_ids}") + result[key] = int(token_ids[0]) + return result + + +def resolve_glm_tts_campplus_path(model_dir: str) -> str | None: + """Resolve ``campplus.onnx`` for GLM-TTS voice cloning.""" + local = os.path.join(model_dir, "frontend", "campplus.onnx") + if os.path.isfile(local): + return local + + try: + resolved = cached_file("FunAudioLLM/CosyVoice-300M", "campplus.onnx") + if resolved is not None: + logger.info("Resolved campplus.onnx from FunAudioLLM/CosyVoice-300M: %s", resolved) + return resolved + except Exception: + logger.debug("cached_file could not fetch campplus.onnx", exc_info=True) + + logger.warning( + "campplus.onnx not found locally or could not be downloaded. " + "Voice cloning speaker embedding will not be available.", + ) + return None + + +def _normalize_glm_tts_processor_text( + frontend: GLMTTSTextFrontend, + value: str | None, + *, + add_trailing_space: bool = False, +) -> str: + if value is None: + return "" + normalized = frontend.text_normalize(value) + normalized = (normalized or value).strip() + if add_trailing_space and normalized: + normalized = f"{normalized} " + return normalized + + +_glm_tts_text_frontend: GLMTTSTextFrontend | None = None + + +def _get_text_frontend() -> GLMTTSTextFrontend: + global _glm_tts_text_frontend + if _glm_tts_text_frontend is None: + _glm_tts_text_frontend = GLMTTSTextFrontend() + return _glm_tts_text_frontend + + +def build_glm_tts_prefill_metadata( + model_name_or_path: Any, + text: str, + prompt_text: str | None, + *, + tokenizer_path: Any | None = None, + trust_remote_code: bool = False, +) -> dict[str, list[int]]: + """Build request-local GLM-TTS length metadata for AR prefill. + + These scalars are mirrored into ``model_intermediate_buffer`` so the model + preprocess hook can initialize request state from the metadata alone. + """ + if tokenizer_path is None: + tokenizer_path = resolve_glm_tts_tokenizer_path(model_name_or_path) + tokenizer = load_glm_tts_tokenizer( + tokenizer_path, + model_name_or_path=model_name_or_path, + trust_remote_code=trust_remote_code, + ) + frontend = _get_text_frontend() + text_len = max(1, len(tokenizer.encode(_normalize_glm_tts_processor_text(frontend, text)))) + prompt_text_len = 0 + if prompt_text: + prompt_text_len = max( + 1, + len( + tokenizer.encode( + _normalize_glm_tts_processor_text( + frontend, + prompt_text, + add_trailing_space=True, + ) + ) + ), + ) + return { + "glm_tts_text_token_len": [text_len], + "glm_tts_prompt_text_token_len": [prompt_text_len], + "input_len": [prompt_text_len + text_len + 1], + } + + +class GLMTTSMultiModalProcessingInfo(BaseProcessingInfo): + def get_hf_config(self): + return self.ctx.get_hf_config(GLMTTSConfig) + + def get_supported_mm_limits(self) -> Mapping[str, int | None]: + return {"audio": 1} + + def get_mm_max_tokens_per_item( + self, + seq_len: int, + mm_counts: Mapping[str, int], + ) -> Mapping[str, int] | None: + if mm_counts.get("audio", 0) <= 0: + return {} + return {"audio": min(seq_len, _GLM_TTS_MAX_PROMPT_SPEECH_TOKENS)} + + def get_data_parser(self): + return MultiModalDataParser( + target_sr=24000, + expected_hidden_size=self._get_expected_hidden_size(), + ) + + +class GLMTTSMultiModalProcessor(BaseMultiModalProcessor[GLMTTSMultiModalProcessingInfo]): + """GLM-TTS voice-clone processor. + + Unlike CosyVoice3, GLM-TTS prompt speech tokens are normal Llama vocab IDs + (``<|audio_N|>``). The processor exposes them as multimodal embeddings for + the AR prompt and also carries WhisperVQ/CampPlus outputs to the AR->DiT + handoff. + """ + + def _ensure_cached_runtime_components( + self, + model_dir: str, + config: GLMTTSConfig, + *, + load_voice_clone: bool = True, + ) -> None: + # Skip if already loaded for this model_dir + mode + cache_key = (model_dir, load_voice_clone) + if getattr(self, "_loaded_cache_key", None) == cache_key: + return + + tokenizer_path = getattr(self.info.ctx.model_config, "tokenizer", None) + required = ("speech_tokenizer/config.json", "speech_tokenizer/model.safetensors") if load_voice_clone else () + model_dir = resolve_glm_tts_model_dir( + model_dir, + tokenizer_path=tokenizer_path, + required_files=required, + ) + + # Load text tokenizer + frontend (always needed) + if getattr(self, "_text_model_dir", None) != model_dir: + tok_path = tokenizer_path + if not tok_path or not os.path.isdir(os.fspath(tok_path)): + tok_path = os.path.join(model_dir, _GLM_TTS_TOKENIZER_SUBDIR) + if not os.path.isdir(tok_path): + tok_path = model_dir + trust_remote_code = bool(getattr(self.info.ctx.model_config, "trust_remote_code", False)) + self.tokenizer = load_glm_tts_tokenizer( + tok_path, + model_name_or_path=self.info.ctx.model_config.model, + trust_remote_code=trust_remote_code, + ) + self.special_ids = get_glm_tts_special_token_ids(self.tokenizer) + self.text_frontend = GLMTTSTextFrontend() + self._text_model_dir = model_dir + + # Load voice clone components (speech tokenizer + CampPlus) + if load_voice_clone: + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + self.processor_device = device + campplus_path = getattr(self, "_campplus_path", None) or resolve_glm_tts_campplus_path(model_dir) + self._campplus_path = campplus_path + self.speech_tokenizer, self.campplus_session = load_voice_clone_frontend( + model_dir, + device, + speech_tokenizer_cache=getattr(self, "speech_tokenizer", None), + campplus_cache=getattr(self, "campplus_session", None), + campplus_path=campplus_path, + ) + + if not hasattr(self, "_speaker_cache"): + self._speaker_cache = get_speaker_cache() + + self._loaded_cache_key = cache_key + + def _encode_text(self, text: str) -> torch.Tensor: + token_ids = self.tokenizer.encode(text) + return torch.tensor([token_ids], dtype=torch.long) + + def _get_audio(self, mm_data: Mapping[str, object]) -> tuple[torch.Tensor | None, int | None]: + audio = mm_data.get("audio") + if audio is None: + audios = mm_data.get("audios") + if isinstance(audios, (list, tuple)) and audios: + # Match CosyVoice3's compatibility behavior and accept the + # first item from the legacy plural field name. + audio = audios[0] + else: + audio = audios + wav, sr = _decode_glm_tts_audio_data(audio) + if wav is not None and wav.ndim > 1: + wav = wav.float() + wav = wav.mean(dim=0) if wav.shape[0] <= wav.shape[-1] else wav.mean(dim=-1) + return wav, int(sr or 24000) if wav is not None else sr + + def _call_hf_processor( + self, + prompt: str, + mm_data: Mapping[str, object], + mm_kwargs: Mapping[str, object], + tok_kwargs: Mapping[str, object], + ) -> BatchFeature: + config = self.info.ctx.get_hf_config() + model_dir = self.info.ctx.model_config.model + wav, sr = self._get_audio(mm_data) + if wav is None: + audio_supplied = mm_data.get("audio") is not None or mm_data.get("audios") is not None + assert not audio_supplied, ( + "GLM-TTS received an audio multimodal payload that could not be decoded. " + "Runtime voice cloning must provide decodable multi_modal_data['audio']; " + "the text-only processor path is reserved for profiling/cache." + ) + self._ensure_cached_runtime_components(model_dir, config, load_voice_clone=False) + normalized_text = _normalize_glm_tts_processor_text(self.text_frontend, prompt) + text_ids = self._encode_text(normalized_text) + dummy_audio_len = min( + _GLM_TTS_MAX_PROMPT_SPEECH_TOKENS, + max(1, int(getattr(config, "max_position_embeddings", _GLM_TTS_MAX_PROMPT_SPEECH_TOKENS))), + ) + boa_tensor = torch.tensor([[int(self.special_ids["boa"])]], dtype=torch.long) + input_ids = torch.cat([text_ids, boa_tensor], dim=1) + # vLLM profiling/cache setup may call the processor without an + # audio item. Do not fabricate a dummy wav here: for GLM-TTS that + # would exercise the real voice-clone frontend during profiling. + # This shape-only branch is not a supported runtime inference mode. + return BatchFeature( + { + "input_ids": input_ids, + "prompt_speech_token": torch.zeros((1, dummy_audio_len), dtype=torch.long), + "prompt_speech_token_len": [torch.tensor([dummy_audio_len], dtype=torch.long)], + "glm_tts_prompt_text_token_len": [torch.tensor([0], dtype=torch.long)], + "glm_tts_text_token_len": [torch.tensor([int(text_ids.shape[1])], dtype=torch.long)], + } + ) + + self._ensure_cached_runtime_components(model_dir, config) + normalized_text = _normalize_glm_tts_processor_text(self.text_frontend, prompt) + text_ids = self._encode_text(normalized_text) + prompt_text = mm_kwargs.get("prompt_text") + if not isinstance(prompt_text, str) or not prompt_text.strip(): + raise ValueError("GLM-TTS voice cloning requires mm_processor_kwargs['prompt_text'].") + + normalized_prompt_text = _normalize_glm_tts_processor_text( + self.text_frontend, + prompt_text, + add_trailing_space=True, + ) + prompt_text_ids = self._encode_text(normalized_prompt_text) + + # Speaker cache: skip WhisperVQ + mel + CampPlus on hit + voice_name = mm_kwargs.get("voice_name") + cache_key = None + if voice_name and isinstance(voice_name, str): + cache_key = self._speaker_cache.make_cache_key( + voice_name, + model_type="glm_tts", + created_at=int(mm_kwargs.get("voice_created_at") or 0), + ) + cached = self._speaker_cache.get(cache_key) + if cached is not None: + boa_tensor = torch.tensor([[int(self.special_ids["boa"])]], dtype=torch.long) + input_ids = torch.cat([prompt_text_ids, text_ids, boa_tensor], dim=1) + return BatchFeature( + { + "input_ids": input_ids, + "prompt_speech_token": cached["prompt_speech_token"].clone(), + "prompt_speech_token_len": [cached["prompt_speech_token_len"].clone()], + "glm_tts_prompt_text_token_len": [ + torch.tensor([int(prompt_text_ids.shape[1])], dtype=torch.long) + ], + "prompt_feat": cached["prompt_feat"].clone(), + "embedding": cached["embedding"].clone(), + "glm_tts_text_token_len": [torch.tensor([int(text_ids.shape[1])], dtype=torch.long)], + } + ) + + prompt_speech_token = extract_prompt_speech_token(wav, int(sr or 24000), self.speech_tokenizer) + if not prompt_speech_token: + raise RuntimeError("GLM-TTS failed to extract WhisperVQ prompt speech tokens from ref_audio.") + + prompt_speech_token_tensor = torch.tensor([prompt_speech_token], dtype=torch.long) + boa_tensor = torch.tensor([[int(self.special_ids["boa"])]], dtype=torch.long) + input_ids = torch.cat([prompt_text_ids, text_ids, boa_tensor], dim=1) + logger.info( + "GLM-TTS processor prompt: prompt_text_tokens=%d text_tokens=%d " + "prompt_speech_tokens=%d input_tokens_before_audio=%d expected_prefill_tokens=%d", + int(prompt_text_ids.shape[1]), + int(text_ids.shape[1]), + len(prompt_speech_token), + int(input_ids.shape[1]), + int(input_ids.shape[1]) + len(prompt_speech_token), + ) + + prompt_feat = extract_prompt_feat(wav, int(sr or 24000), self.processor_device) + if prompt_feat is None: + raise RuntimeError("GLM-TTS failed to extract prompt mel features from ref_audio.") + embedding = extract_spk_embedding(wav, int(sr or 24000), self.campplus_session) + if embedding is None: + raise RuntimeError("GLM-TTS failed to extract CampPlus speaker embedding from ref_audio.") + + prompt_speech_token_len = torch.tensor([len(prompt_speech_token)], dtype=torch.long) + prompt_feat_cpu = prompt_feat.detach().to("cpu").unsqueeze(0).contiguous() + embedding_tensor = torch.tensor([embedding], dtype=torch.float32) + + if cache_key is not None: + self._speaker_cache.put( + cache_key, + { + "prompt_speech_token": prompt_speech_token_tensor.detach().cpu(), + "prompt_speech_token_len": prompt_speech_token_len.detach().cpu(), + "prompt_feat": prompt_feat_cpu.detach().cpu(), + "embedding": embedding_tensor.detach().cpu(), + }, + ) + + return BatchFeature( + { + "input_ids": input_ids, + "prompt_speech_token": prompt_speech_token_tensor, + "prompt_speech_token_len": [prompt_speech_token_len], + "glm_tts_prompt_text_token_len": [torch.tensor([int(prompt_text_ids.shape[1])], dtype=torch.long)], + "prompt_feat": prompt_feat_cpu, + "embedding": embedding_tensor, + "glm_tts_text_token_len": [torch.tensor([int(text_ids.shape[1])], dtype=torch.long)], + } + ) + + def _get_mm_fields_config( + self, + hf_inputs: BatchFeature, + hf_processor_mm_kwargs: Mapping[str, object], + ) -> Mapping[str, MultiModalFieldConfig]: + return { + key: MultiModalFieldConfig.batched("audio") + for key in ( + "prompt_speech_token", + "prompt_speech_token_len", + "glm_tts_prompt_text_token_len", + "prompt_feat", + "embedding", + "glm_tts_text_token_len", + ) + if key in hf_inputs + } + + def _hf_processor_applies_updates( + self, + prompt_text: str, + mm_items: MultiModalDataItems, + hf_processor_mm_kwargs: Mapping[str, object], + tokenization_kwargs: Mapping[str, object], + ) -> bool: + return False + + def _cached_apply_hf_processor(self, inputs: ProcessorInputs, timing_ctx: Any): + # GLM-TTS builds the actual AR prompt from both the request text and + # mm_processor_kwargs["prompt_text"]. The base cache path separates text + # processing from audio processing, which drops the reference transcript + # and BOA token from the final prompt. + return self._apply_hf_processor(inputs, timing_ctx) + + def _get_prompt_updates( + self, + mm_items: MultiModalDataItems, + hf_processor_mm_kwargs: Mapping[str, object], + out_mm_kwargs: MultiModalKwargsItems, + ) -> Sequence[PromptUpdate]: + def insertion_end(item_idx: int) -> list[int]: + audio_items = out_mm_kwargs["audio"] + item = audio_items[item_idx] if item_idx < len(audio_items) else audio_items[0] + token_len = item["prompt_speech_token_len"].data[0].item() + return [1] * int(token_len) + + return [ + PromptInsertion( + modality="audio", + target=PromptIndexTargets.end(), + insertion=insertion_end, + ) + ] + + +class GLMTTSDummyInputsBuilder(BaseDummyInputsBuilder[GLMTTSMultiModalProcessingInfo]): + def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str: + return "This is a test of the GLM-TTS voice cloning system." + + def get_dummy_mm_data( + self, + seq_len: int, + mm_counts: Mapping[str, int], + mm_options: Mapping[str, BaseDummyOptions] | None = None, + ) -> MultiModalDataDict: + return {} + + def get_dummy_processor_inputs( + self, + seq_len: int, + mm_counts: Mapping[str, int], + mm_options: Mapping[str, BaseDummyOptions] | None = None, + ) -> ProcessorInputs: + inputs = super().get_dummy_processor_inputs(seq_len, mm_counts, mm_options) + inputs.hf_processor_mm_kwargs = {"prompt_text": "This is the reference voice."} + return inputs + + +@MULTIMODAL_REGISTRY.register_processor( + GLMTTSMultiModalProcessor, + info=GLMTTSMultiModalProcessingInfo, + dummy_inputs=GLMTTSDummyInputsBuilder, +) +class GLMTTSForConditionalGeneration(nn.Module, SupportsMultiModal): + """vLLM model for GLM-TTS. + + Handles both stages via model_stage branching: + - ``glm_tts`` (Stage 0): Text → Speech tokens (LLM AR, Llama backbone). + - ``glm_tts_dit`` (Stage 1): Speech tokens → Audio (DiT flow-matching + vocoder). + + Attributes: + have_multimodal_outputs: Signals scheduler to collect multimodal outputs. + has_preprocess: Model has preprocess hook for input preparation (stage 0 only). + has_postprocess: Model has postprocess hook for hidden state caching (stage 0 only). + """ + + supports_multimodal_raw_input_only = True + supports_multimodal = True + requires_raw_input_tokens = True + prefer_model_sampler = True + _sampling_eps = 1e-5 + hf_to_vllm_mapper = WeightsMapper( + orig_to_new_prefix={ + "llama_embedding.": "model.embed_tokens.", + "llama.model.": "model.", + "llama.": "model.", + } + ) + + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): + super().__init__() + self.vllm_config = vllm_config + self.model_path = vllm_config.model_config.model + self.model_dir = self.model_path + self.model_stage = getattr(vllm_config.model_config, "model_stage", "glm_tts") + + # Load configuration + config: GLMTTSConfig = vllm_config.model_config.hf_config # type: ignore[assignment] + self.config = config + + # ---- Stage 1 (DiT): delegate to GLMTTSDiTForGeneration ---- + if self.model_stage == "glm_tts_dit": + from .glm_tts_dit_wrapper import GLMTTSDiTForGeneration + + self._dit_gen = GLMTTSDiTForGeneration(vllm_config=vllm_config, prefix=prefix) + # Expose the DiT as self.model for weight loading routing + self.model = self._dit_gen + self.have_multimodal_outputs = True + self.enable_update_additional_information = True + # DiT stage does not use preprocess/postprocess/sample + self.has_preprocess = False + self.has_postprocess = False + # DiT loads all weights internally from flow/flow.pt etc. + # Point DefaultModelLoader to flow/flow.pt (a .pt file) so it can + # find and load it without triggering the vLLM#39699 bug where + # subdirectory safetensors patterns cause pt_weights_iterator + # to be used on .safetensors files. load_weights() ignores the + # yielded weights and loads flow/flow.pt itself. + self.allow_patterns_overrides = ["flow/flow.pt"] + self.fall_back_to_pt_during_load = False + return + sample_method = str(getattr(config, "sample_method", "ras")).lower() + if sample_method != "ras": + raise ValueError(f"Unsupported GLM-TTS sample_method={sample_method!r}; expected 'ras'.") + + # Resolve repo root to local path (CosyVoice3 pattern). + # Model weights are in llm/ subdirectory; tokenizer and other resources + # are siblings of llm/ under the repo root. + self.model_dir = resolve_glm_tts_model_dir( + self.model_dir, + tokenizer_path=getattr(vllm_config.model_config, "tokenizer", None), + ) + + # Stage 0 weights live under llm/model-*.safetensors and are loaded + # explicitly by _iter_llm_safetensors() during load_weights(). + # + # We still point DefaultModelLoader at flow/flow.pt as a bootstrap + # sentinel because the current upstream loader path does not reliably + # target subdirectory safetensors patterns here without hitting the + # vLLM#39699 iterator bug. Keep the real stage-0 source of truth in + # _iter_llm_safetensors(); this override is only to get through the + # generic loader bootstrap step. + self.allow_patterns_overrides = ["flow/flow.pt"] + self.fall_back_to_pt_during_load = False + + # Load tokenizer for special token ID resolution. + # Prefer the tokenizer path auto-detected by arg_utils + # (_TOKENIZER_SUBFOLDER_MAP). Fall back to vq32k-phoneme-tokenizer/ + # under model_dir. + tokenizer_path = getattr(vllm_config.model_config, "tokenizer", None) + if not (tokenizer_path and os.path.isdir(tokenizer_path)): + tokenizer_path = os.path.join(self.model_dir, _GLM_TTS_TOKENIZER_SUBDIR) + if not os.path.exists(tokenizer_path): + tokenizer_path = self.model_dir + trust_remote_code = bool(getattr(vllm_config.model_config, "trust_remote_code", False)) + self._tokenizer = load_glm_tts_tokenizer( + tokenizer_path, + model_name_or_path=self.model_path, + trust_remote_code=trust_remote_code, + ) + special_ids = get_glm_tts_special_token_ids(self._tokenizer) + self._ats = special_ids["ats"] + self._ate = special_ids["ate"] + self._boa = special_ids["boa"] + self._eoa = special_ids["eoa"] + self._pad = special_ids["pad"] + self._bos = self._tokenizer.bos_token_id or self._pad + + logger.info( + "GLM-TTS token IDs: ATS=%d, ATE=%d, BOA=%d, EOA=%d, PAD=%d, vocab_size=%d", + self._ats, + self._ate, + self._boa, + self._eoa, + self._pad, + config.vocab_size, + ) + + # Validate special token sanity with runtime exceptions (asserts can be + # stripped under python -O). + if self._ate - self._ats != 32767: + raise ValueError(f"Audio token range should be 32768, got {self._ate - self._ats + 1}") + if self._boa >= self._ats: + raise ValueError(f"BOA={self._boa} should be < ATS={self._ats} (BOA is text token)") + + # Validate vocab_size covers all special tokens + max_token = max(self._ats, self._ate, self._boa, self._eoa, self._pad) + if max_token >= config.vocab_size: + raise ValueError( + f"vocab_size ({config.vocab_size}) must be > max token ID ({max_token}). " + f"Check model's config.json has correct vocab_size." + ) + + # Update config with dynamic token IDs so vLLM uses correct eos_token + # This enables proper stop detection when EOA is sampled + config.eos_token_id = self._eoa + config.eoa_token_id = self._eoa + config.audio_token_start = self._ats + config.audio_token_end = self._ate + config.boa_token_id = self._boa + config.pad_token_id = self._pad + config.bos_token_id = self._bos + + # Model flags for AR scheduler + self.have_multimodal_outputs = True + self.has_preprocess = True + self.has_postprocess = True + # GLM-TTS async chunk transfer consumes speech_tokens plus voice-clone + self.gpu_resident_buffer_keys: set[tuple[str, str]] = {("last_hidden", "last")} + + self.model = LlamaModel(vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")) + + # LM head for speech token prediction + if get_pp_group().is_last_rank: + self.lm_head = ParallelLMHead( + config.vocab_size, + config.hidden_size, + quant_config=vllm_config.quant_config, + prefix=maybe_prefix(prefix, "lm_head"), + ) + else: + self.lm_head = PPMissingLayer() + + self.logits_processor = LogitsProcessor(config.vocab_size) + self.make_empty_intermediate_tensors = self.model.make_empty_intermediate_tensors + + # Runtime validation: dynamic EOA must match the hardcoded pipeline + # stop_token_ids constant (59253). A mismatch means the upstream + # tokenizer vocabulary has changed and pipeline.py needs updating. + _PIPELINE_EOA = 59253 + if self._eoa != _PIPELINE_EOA: + logger.warning( + "GLM-TTS EOA token mismatch: tokenizer resolved %d but " + "pipeline.py hardcodes stop_token_ids=[%d]. Update " + "pipeline.py to match the current checkpoint.", + self._eoa, + _PIPELINE_EOA, + ) + + # RAS sampling config — stored as attributes for sample() + self._ras_win_size = int(getattr(config, "ras_win_size", 10)) + self._ras_tau_r = float(getattr(config, "ras_tau_r", 0.1)) + self._ras_top_p = float(getattr(config, "ras_top_p", 0.8)) + self._ras_top_k = int(getattr(config, "ras_top_k", 25)) + + def _model_dtype(self) -> torch.dtype: + """Return the active parameter dtype for locally-created embeddings.""" + try: + return next(self.model.parameters()).dtype + except StopIteration: + dtype = getattr(self.vllm_config.model_config, "dtype", None) + if isinstance(dtype, torch.dtype): + return dtype + if isinstance(dtype, str): + _aliases = {"bf16": "bfloat16", "fp16": "float16", "half": "float16", "fp32": "float32"} + resolved = getattr(torch, _aliases.get(dtype.lower(), dtype.lower()), None) + if resolved is not None: + return resolved + return torch.get_default_dtype() + + def _resolve_prefill_target_text_len( + self, + span_len: int, + info_dict: Mapping[str, Any], + ) -> int | None: + """Resolve the target-text token count before the first AR sample. + + The request builder mirrors the processor's length metadata into + model_intermediate_buffer before the first prefill. Prefer lengths + inferred from that metadata, then fall back to the explicit target + scalar. Never silently use a prompt-text length as the target length. + """ + + provided_text_len = _glm_tts_int_value(info_dict.get("glm_tts_text_token_len")) + prompt_text_len = _glm_tts_int_value(info_dict.get("glm_tts_prompt_text_token_len")) + prompt_speech_len = _glm_tts_int_value(info_dict.get("prompt_speech_token_len")) + input_len = _glm_tts_int_value(info_dict.get("input_len")) + + inferred_text_len: int | None = None + if input_len is not None and prompt_text_len is not None: + inferred_text_len = input_len - prompt_text_len - 1 # strip prompt text and BOA. + elif prompt_text_len is not None and prompt_speech_len is not None: + inferred_text_len = span_len - prompt_text_len - prompt_speech_len - 1 + + if inferred_text_len is not None and inferred_text_len > 0: + if provided_text_len is not None and provided_text_len != inferred_text_len: + logger.warning( + "GLM-TTS target text token length mismatch: processor=%d inferred=%d; using inferred length.", + provided_text_len, + inferred_text_len, + ) + return inferred_text_len + + return provided_text_len if provided_text_len is not None and provided_text_len > 0 else None + + def embed_input_ids( + self, + input_ids: torch.Tensor, + multimodal_embeddings: Any | None = None, + is_multimodal: Any | None = None, + **kwargs: Any, + ) -> torch.Tensor: + if self.model_stage == "glm_tts_dit": + return self._dit_gen.embed_input_ids(input_ids, **kwargs) + embed_tokens = self.model.embed_tokens(input_ids) + if multimodal_embeddings is None or is_multimodal is None: + return embed_tokens + + mask = torch.as_tensor(is_multimodal, device=embed_tokens.device, dtype=torch.bool).reshape(-1) + if not bool(mask.any()): + return embed_tokens + + if isinstance(multimodal_embeddings, (list, tuple)): + tensors = [ + torch.as_tensor(item, device=embed_tokens.device, dtype=embed_tokens.dtype) + for item in multimodal_embeddings + if item is not None + ] + if not tensors: + return embed_tokens + mm_embeds = torch.cat([item.reshape(-1, item.shape[-1]) for item in tensors], dim=0) + else: + mm_embeds = multimodal_embeddings + if mm_embeds is None: + return embed_tokens + mm_embeds = torch.as_tensor(mm_embeds, device=embed_tokens.device, dtype=embed_tokens.dtype) + if mm_embeds.ndim == 3 and int(mm_embeds.shape[0]) == 1: + mm_embeds = mm_embeds.squeeze(0) + if mm_embeds.ndim != 2: + shape = tuple(mm_embeds.shape) + raise ValueError(f"GLM-TTS multimodal embeddings should be 2D, got shape={shape}") + + flat_tokens = embed_tokens.reshape(-1, embed_tokens.shape[-1]) + if int(flat_tokens.shape[0]) != int(mask.numel()): + raise ValueError( + "GLM-TTS multimodal mask/token length mismatch: " + f"mask={int(mask.numel())}, tokens={int(flat_tokens.shape[0])}" + ) + mm_len = int(mask.sum().item()) + if int(mm_embeds.shape[0]) < mm_len: + raise ValueError( + "GLM-TTS multimodal embedding length mismatch: " + f"embeddings={int(mm_embeds.shape[0])}, placeholders={mm_len}" + ) + + flat_tokens = flat_tokens.clone() + flat_tokens[mask] = mm_embeds[:mm_len] + return flat_tokens.reshape_as(embed_tokens) + + def embed_multimodal(self, **kwargs: Any) -> list[torch.Tensor] | None: + if self.model_stage != "glm_tts": + return None + prompt_speech_token = kwargs.get("prompt_speech_token") + if prompt_speech_token is None: + return None + + def embed_one(value: Any) -> torch.Tensor | None: + if value is None: + return None + speech_token = torch.as_tensor(value, device=next(self.model.parameters()).device) + if speech_token.numel() == 0: + return None + speech_token = speech_token.to(dtype=torch.long) + if speech_token.ndim == 0: + speech_token = speech_token.reshape(1, 1) + elif speech_token.ndim == 1: + speech_token = speech_token.unsqueeze(0) + elif speech_token.ndim > 2: + speech_token = speech_token.reshape(int(speech_token.shape[0]), -1) + if int(speech_token.min().item()) >= self._ats: + speech_ids = speech_token + else: + speech_ids = speech_token + self._ats + speech_embeds = self.model.embed_tokens(speech_ids) + return speech_embeds.reshape(-1, speech_embeds.shape[-1]) + + def is_flat_token_sequence(value: list[Any] | tuple[Any, ...]) -> bool: + if not value: + return False + try: + return all(torch.as_tensor(item).ndim == 0 for item in value) + except (TypeError, ValueError): + return False + + if isinstance(prompt_speech_token, (list, tuple)): + if is_flat_token_sequence(prompt_speech_token): + one_embed = embed_one(prompt_speech_token) + return [one_embed] if one_embed is not None else None + embeds = [embed_one(item) for item in prompt_speech_token] + embeds = [item for item in embeds if item is not None] + if not embeds: + return None + return embeds + + one_embed = embed_one(prompt_speech_token) + return [one_embed] if one_embed is not None else None + + def forward( + self, + input_ids: torch.Tensor, + positions: torch.Tensor, + intermediate_tensors: IntermediateTensors | None = None, + inputs_embeds: torch.Tensor | None = None, + **kwargs: Any, + ) -> torch.Tensor | IntermediateTensors | OmniOutput: + if self.model_stage == "glm_tts_dit": + return self._dit_gen.forward(input_ids, positions, **kwargs) + return self.model(input_ids, positions, intermediate_tensors, inputs_embeds) + + def compute_logits( + self, hidden_states: torch.Tensor | OmniOutput, sampling_metadata: Any = None + ) -> torch.Tensor | None: + if self.model_stage == "glm_tts_dit": + return self._dit_gen.compute_logits(hidden_states, sampling_metadata) + if isinstance(hidden_states, OmniOutput): + hidden_states = hidden_states.text_hidden_states + if hidden_states is None: + return None + logits = self.logits_processor(self.lm_head, hidden_states) + if logits is None: + return None + + return logits + + def _glm_tts_ras_enabled(self, sampling_metadata: SamplingMetadata) -> bool: + if self.model_stage != "glm_tts": + return False + if sampling_metadata.max_num_logprobs is not None: + return False + if sampling_metadata.temperature is None: + return False + if bool(sampling_metadata.bad_words_token_ids): + return False + if torch.any(sampling_metadata.frequency_penalties != 0): + return False + if torch.any(sampling_metadata.presence_penalties != 0): + return False + return True + + def _apply_allowed_mask(self, logits: torch.Tensor) -> torch.Tensor: + """Restrict logits to valid audio tokens [ATS..ATE] ∪ {EOA}.""" + allowed_mask = getattr(self, "_ar_allowed_mask", None) + if allowed_mask is None or allowed_mask.device != logits.device or allowed_mask.shape[0] != logits.shape[-1]: + allowed_mask = torch.full((logits.shape[-1],), float("-inf"), device=logits.device) + allowed_mask[self._ats : self._ate + 1] = 0.0 + allowed_mask[self._eoa] = 0.0 + self._ar_allowed_mask = allowed_mask + return logits + allowed_mask + + def _select_allowed_audio_logits(self, logits: torch.Tensor) -> torch.Tensor: + """Return compact logits for [audio tokens] plus EOA. + + The RAS path samples only from GLM-TTS audio tokens and EOA. Keeping a + compact view avoids top-k/logsumexp over the full LLM vocabulary on + every AR step; this is equivalent to sampling after ``_apply_allowed_mask``. + """ + audio_logits = logits[:, self._ats : self._ate + 1] + if self._ats <= self._eoa <= self._ate: + return audio_logits + return torch.cat((audio_logits, logits[:, self._eoa : self._eoa + 1]), dim=-1) + + def _allowed_local_to_token_id(self, local_id: int) -> int: + audio_vocab = self._ate - self._ats + 1 + return self._ats + local_id if local_id < audio_vocab else self._eoa + + def _recent_allowed_local_ids(self, decoded_tokens: Sequence[int], win_size: int) -> list[int]: + if win_size <= 0 or not decoded_tokens: + return [] + audio_vocab = self._ate - self._ats + 1 + recent: list[int] = [] + for token in decoded_tokens[-win_size:]: + token_id = int(token) + if self._ats <= token_id <= self._ate: + recent.append(token_id - self._ats) + elif token_id == self._eoa: + recent.append(audio_vocab) + else: + recent.append(-1) + return recent + + def sample( + self, + logits: torch.Tensor, + sampling_metadata: SamplingMetadata, + ) -> SamplerOutput | None: + """RAS sampler following CosyVoice3 pattern. + + Uses vLLM Sampler for logits processing (logit_bias_state handles + min_tokens/max_tokens/stop_token_ids). When RAS is enabled, applies + per-request nucleus+repetition-aware sampling; otherwise falls back + to standard vLLM sampling. + """ + if logits is None or logits.numel() == 0: + return None + if self.model_stage != "glm_tts": + return None + + sampler = getattr(self, "_ar_sampler", None) + if sampler is None: + sampler = Sampler() + self._ar_sampler = sampler + + if not self._glm_tts_ras_enabled(sampling_metadata): + logits = self._apply_allowed_mask(logits) + return sampler(logits=logits, sampling_metadata=sampling_metadata) + + logits = logits.to(torch.float32) + sampling_for_processors = replace(sampling_metadata, no_penalties=True) + logits = sampler.apply_logits_processors(logits, sampling_for_processors, predict_bonus_token=False) + allowed_logits = self._select_allowed_audio_logits(logits) + + default_top_p = self._ras_top_p + default_top_k = self._ras_top_k + ws = self._ras_win_size + tr = self._ras_tau_r + + num_reqs = int(allowed_logits.shape[0]) + t_data = sampling_metadata.temperature + tp_data = sampling_metadata.top_p + tk_data = sampling_metadata.top_k + cache_key = ( + t_data.data_ptr() if t_data is not None else 0, + tp_data.data_ptr() if tp_data is not None else 0, + tk_data.data_ptr() if tk_data is not None else 0, + num_reqs, + ) + _sp_cache = getattr(self, "_sampling_param_cache", None) + if _sp_cache is not None and _sp_cache[0] == cache_key: + temperatures, top_ps, top_ks = _sp_cache[1] + else: + temperatures = _tensor_param_values(t_data, num_reqs, 1.0) + top_ps = _tensor_param_values(tp_data, num_reqs, default_top_p) + top_ks = _tensor_param_values(tk_data, num_reqs, default_top_k) + self._sampling_param_cache = (cache_key, (temperatures, top_ps, top_ks)) + + sampled_ids: list[int] = [] + for req_idx in range(num_reqs): + row_logits = allowed_logits[req_idx] + temperature = temperatures[req_idx] + if temperature < self._sampling_eps: + local_id = int(torch.argmax(row_logits).item()) + sampled_ids.append(self._allowed_local_to_token_id(local_id)) + continue + + top_p = top_ps[req_idx] + top_k = int(top_ks[req_idx]) + generator = sampling_metadata.generators.get(req_idx) + weighted_scores = row_logits / max(temperature, self._sampling_eps) + decoded_tokens = ( + sampling_metadata.output_token_ids[req_idx] if req_idx < len(sampling_metadata.output_token_ids) else [] + ) + local_id = _ras_sample_one( + weighted_scores, + self._recent_allowed_local_ids(decoded_tokens, ws), + top_p=top_p, + top_k=top_k, + win_size=ws, + tau_r=tr, + generator=generator, + ) + sampled_ids.append(self._allowed_local_to_token_id(local_id)) + + sampled = torch.tensor(sampled_ids, device=logits.device, dtype=torch.int32) + return SamplerOutput(sampled_token_ids=sampled.unsqueeze(-1), logprobs_tensors=None) + + def make_omni_output(self, model_outputs: torch.Tensor | OmniOutput, **kwargs: Any) -> OmniOutput: + """Package hidden states, speech tokens, and voice clone data into OmniOutput. + + Streaming contract: **delta**. Each decode step emits exactly one + speech token (or a prefill placeholder). The engine's output + processor concatenates per-step deltas into the final tensor. + """ + if isinstance(model_outputs, OmniOutput): + return model_outputs + + hidden = model_outputs + info_dicts = kwargs.get("model_intermediate_buffer") or [] + if isinstance(info_dicts, dict): + info_dicts = [info_dicts] + + speech_tokens_list: list[torch.Tensor] = [] + multimodal_extras: dict[str, Any] = {} + mm_conditioning_keys = ( + "prompt_speech_token", + "prompt_speech_token_len", + "glm_tts_prompt_text_token_len", + "prompt_feat", + "embedding", + "glm_tts_text_token_len", + ) + info_for_flags = next((info for info in info_dicts if isinstance(info, dict)), None) + if info_for_flags is None or not info_for_flags.get("_voice_clone_emitted"): + copied_from_kwargs = False + for key in mm_conditioning_keys: + val = kwargs.get(key) + if val is not None: + multimodal_extras[key] = val + copied_from_kwargs = True + if copied_from_kwargs and info_for_flags is not None: + info_for_flags["_voice_clone_emitted"] = True + + for info in info_dicts: + if not isinstance(info, dict): + continue + tokens = info.get("speech_tokens") + if isinstance(tokens, torch.Tensor) and tokens.numel() > 0: + speech_tokens_list.append(tokens) + # Propagate voice cloning features from preprocess info_update. + # IMPORTANT: Only emit once — the output processor accumulates + # every decode step and concatenates tensors, which would corrupt + # constant data (e.g. embedding [192] → [N*192]). + if not info.get("_voice_clone_emitted"): + for key in mm_conditioning_keys: + val = info.get(key) + if val is not None and key not in multimodal_extras: + multimodal_extras[key] = val + if any(info.get(k) is not None for k in mm_conditioning_keys): + info["_voice_clone_emitted"] = True + + if not speech_tokens_list: + return OmniOutput(text_hidden_states=hidden, multimodal_outputs=multimodal_extras) + + speech_tokens = torch.cat(speech_tokens_list, dim=0) + span_len = int(speech_tokens.shape[0]) + hidden = hidden[:span_len] + multimodal_extras["speech_tokens"] = speech_tokens + return OmniOutput( + text_hidden_states=hidden, + multimodal_outputs=multimodal_extras, + ) + + def preprocess( + self, + input_ids: torch.Tensor, + input_embeds: torch.Tensor | None, + **info_dict: Any, + ) -> tuple[torch.Tensor, torch.Tensor, dict[str, Any]]: + """Prepare inputs for GLM-TTS AR model. + + GLM-TTS only supports the multimodal processor path: + text prompt + ``multi_modal_data["audio"]`` + ``mm_processor_kwargs["prompt_text"]``. + Legacy placeholder prompts via ``additional_information`` are rejected. + """ + if info_dict.get("additional_information") is not None: + raise ValueError( + "GLM-TTS no longer accepts legacy additional_information prompts; " + "use prompt + multi_modal_data['audio'] + mm_processor_kwargs['prompt_text']." + ) + + span_len = int(input_ids.shape[0]) + logger.debug("preprocess: span_len=%d, input_ids.shape=%s", span_len, input_ids.shape) + if span_len <= 0: + return input_ids, input_embeds if input_embeds is not None else self.embed_input_ids(input_ids), {} + + device = input_ids.device + + if isinstance(info_dict.get("text"), list) and info_dict["text"]: + raise ValueError( + "GLM-TTS no longer accepts legacy text/additional_information prefill payloads; " + "use the multimodal processor path." + ) + + mm_prefill_done = bool(info_dict.get("glm_tts_mm_prefill_done")) + sampled_token: int | None = None + if input_embeds is not None and span_len == 1: + sampled_token = int(input_ids[0].item()) + one_token_prefill_tail = ( + input_embeds is not None + and span_len == 1 + and mm_prefill_done + and sampled_token is not None + and sampled_token != self._eoa + and not (self._ats <= sampled_token <= self._ate) + ) + is_prefill_span = span_len > 1 or not mm_prefill_done or one_token_prefill_tail + if input_embeds is None and is_prefill_span: + raise ValueError("Missing GLM-TTS multimodal input embeddings.") + if input_embeds is not None and is_prefill_span: + input_ids_out = input_ids.clone() + input_ids_out[:] = self._pad + info_update: dict[str, Any] = { + "glm_tts_mm_prefill_done": True, + "speech_tokens": torch.full((span_len, 1), -1, device=device, dtype=torch.long), + } + # One-token prefill tails re-enter the prefill branch but + # text_token_len was already resolved during the initial prefill. + if not one_token_prefill_tail: + text_token_len = self._resolve_prefill_target_text_len(span_len, info_dict) + if text_token_len is not None: + info_update["glm_tts_text_token_len"] = torch.tensor( + [text_token_len], device=device, dtype=torch.long + ) + prompt_text_len = info_dict.get("glm_tts_prompt_text_token_len") + if prompt_text_len is not None: + info_update["glm_tts_prompt_text_token_len"] = prompt_text_len + prompt_speech_len = info_dict.get("prompt_speech_token_len") + if prompt_speech_len is None: + input_len = _glm_tts_int_value(info_dict.get("input_len")) + if input_len is not None: + prompt_speech_len = torch.tensor( + [max(0, span_len - input_len)], device=device, dtype=torch.long + ) + if prompt_speech_len is not None: + info_update["prompt_speech_token_len"] = prompt_speech_len + return input_ids_out, input_embeds.to(dtype=self._model_dtype()), info_update + + # Decode: span_len == 1 + # Standard autoregressive decode - use input_ids directly + if input_embeds is not None and int(input_embeds.shape[0]) == 1: + inputs_embeds_out = input_embeds.reshape(1, -1).to(dtype=self._model_dtype()) + else: + inputs_embeds_out = self.embed_input_ids(input_ids.reshape(1, 1).to(torch.long)) + inputs_embeds_out = inputs_embeds_out.reshape(1, -1).to(dtype=self._model_dtype()) + + # Convert sampled token to speech token (relative to ATS) + # -1 = invalid/EOA, valid range = [0, ATE-ATS] + if sampled_token is None: + sampled_token = int(input_ids[0].item()) + if self._ats <= sampled_token <= self._ate: + speech_token = sampled_token - self._ats + else: + # EOA or other non-audio token → mark as invalid (-1) + speech_token = -1 + logger.debug("GLM-TTS decode: non-audio token %d (EOA=%d)", sampled_token, self._eoa) + speech_tokens = torch.tensor([[speech_token]], device=device, dtype=torch.long) + + info_update = {"speech_tokens": speech_tokens} + return input_ids, inputs_embeds_out, info_update + + def postprocess(self, hidden_states: torch.Tensor, **kwargs: Any) -> dict[str, Any]: + """Cache last hidden state for next decode step.""" + if hidden_states.numel() == 0: + return {} + last = hidden_states[-1, :].detach() + update: dict[str, Any] = {"last_hidden": {"last": last}} + + multimodal_outputs = kwargs.get("multimodal_outputs") + if isinstance(multimodal_outputs, dict): + copied_conditioning = False + for key in ("prompt_speech_token", "prompt_speech_token_len", "prompt_feat", "embedding"): + val = multimodal_outputs.get(key) + if val is not None: + update[key] = _first_glm_tts_value(val) + copied_conditioning = True + + prompt_text_len = multimodal_outputs.get("glm_tts_prompt_text_token_len") + if prompt_text_len is not None: + update["glm_tts_prompt_text_token_len"] = _first_glm_tts_value(prompt_text_len) + + text_token_len = _glm_tts_int_value(multimodal_outputs.get("glm_tts_text_token_len")) + if text_token_len is not None: + update["glm_tts_text_token_len"] = torch.tensor([int(text_token_len)], dtype=torch.long) + + if copied_conditioning: + update["_voice_clone_emitted"] = True + + return update + + def _iter_llm_safetensors(self) -> Iterable[tuple[str, torch.Tensor]]: + """Yield (name, tensor) pairs from llm/model-*.safetensors.""" + import glob as glob_module + + from safetensors.torch import load_file + + llm_dir = os.path.join(self.model_dir, "llm") + if os.path.isdir(llm_dir): + sf_files = sorted(glob_module.glob(os.path.join(llm_dir, "model-*.safetensors"))) + if sf_files: + for sf_path in sf_files: + yield from load_file(sf_path, device="cpu").items() + return + + from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific + + model_root = download_weights_from_hf_specific( + self.model_path, + self.vllm_config.load_config.download_dir, + allow_patterns=["llm/model-*.safetensors"], + ) + llm_dir = os.path.join(model_root, "llm") + sf_files = sorted(glob_module.glob(os.path.join(llm_dir, "model-*.safetensors"))) + if not sf_files: + raise RuntimeError(f"No LLM safetensors found under {model_root}. Expected llm/model-*.safetensors.") + for sf_path in sf_files: + yield from load_file(sf_path, device="cpu").items() + + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: + """Load weights from checkpoint. + + Stage 0 (glm_tts): HuggingFace Llama-format checkpoint from llm/ subdir. + Stage 1 (glm_tts_dit): DiT flow.pt + vocoder. + """ + if self.model_stage == "glm_tts_dit": + return self._dit_gen.load_weights(weights) + + def _glm_tts_weights() -> Iterable[tuple[str, torch.Tensor]]: + for name, loaded_weight in self._iter_llm_safetensors(): + if "rotary_emb.inv_freq" in name: + continue + if "rotary_emb.cos_cached" in name or "rotary_emb.sin_cached" in name: + continue + yield name, loaded_weight + + loader = AutoWeightsLoader(self) + loaded_params = loader.load_weights(_glm_tts_weights(), mapper=self.hf_to_vllm_mapper) + + params_dict = dict(self.named_parameters()) + # Handle tie_word_embeddings: copy embed_tokens to lm_head if not loaded. + lm_head_key = "lm_head.weight" + embed_key = "model.embed_tokens.weight" + if lm_head_key not in loaded_params and embed_key in loaded_params: + if lm_head_key in params_dict and embed_key in params_dict: + lm_head_param = params_dict[lm_head_key] + embed_param = params_dict[embed_key] + weight_loader = getattr(lm_head_param, "weight_loader", default_weight_loader) + weight_loader(lm_head_param, embed_param.data) + loaded_params.add(lm_head_key) + logger.info("Tied lm_head.weight to embed_tokens.weight") + + logger.info("Loaded %d weights for GLMTTSForConditionalGeneration", len(loaded_params)) + return loaded_params diff --git a/vllm_omni/model_executor/models/glm_tts/glm_tts_dit.py b/vllm_omni/model_executor/models/glm_tts/glm_tts_dit.py new file mode 100644 index 000000000..5cda6ec45 --- /dev/null +++ b/vllm_omni/model_executor/models/glm_tts/glm_tts_dit.py @@ -0,0 +1,650 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""GLM-TTS DiT (Diffusion Transformer) Model. + +Flow Matching model that converts speech tokens to mel-spectrogram. +Ported from: https://github.com/zai-org/GLM-TTS/blob/main/flow/dit.py +""" + +from __future__ import annotations + +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F +from einops import rearrange +from x_transformers.x_transformers import RotaryEmbedding, apply_rotary_pos_emb + +from vllm_omni.diffusion.attention.layer import Attention as DiffusionAttention + + +def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0, theta_rescale_factor: float = 1.0): + """Precompute Rotary Positional Embeddings (RoPE).""" + theta *= theta_rescale_factor ** (dim / (dim - 2)) + freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim)) + t = torch.arange(end, device=freqs.device) + freqs = torch.outer(t, freqs).float() + freqs_cos = torch.cos(freqs) + freqs_sin = torch.sin(freqs) + return torch.cat([freqs_cos, freqs_sin], dim=-1) + + +def get_pos_embed_indices(start, length, max_pos, scale=1.0): + """Get position embedding indices.""" + scale = scale * torch.ones_like(start, dtype=torch.float32) + pos = ( + start.unsqueeze(1) + + (torch.arange(length, device=start.device, dtype=torch.float32).unsqueeze(0) * scale.unsqueeze(1)).long() + ) + pos = torch.where(pos < max_pos, pos, max_pos - 1) + return pos + + +class SinusPositionEmbedding(nn.Module): + """Sinusoidal position embedding for timesteps.""" + + def __init__(self, dim: int): + super().__init__() + self.dim = dim + + def forward(self, x: torch.Tensor, scale: float = 1000) -> torch.Tensor: + device = x.device + half_dim = self.dim // 2 + emb = math.log(10000) / (half_dim - 1) + emb = torch.exp(torch.arange(half_dim, device=device).float() * -emb) + emb = scale * x.unsqueeze(1) * emb.unsqueeze(0) + emb = torch.cat((emb.sin(), emb.cos()), dim=-1) + return emb + + +class TimestepEmbedding(nn.Module): + """Timestep embedding with MLP.""" + + def __init__(self, dim: int, freq_embed_dim: int = 256): + super().__init__() + self.time_embed = SinusPositionEmbedding(freq_embed_dim) + self.time_mlp = nn.Sequential(nn.Linear(freq_embed_dim, dim), nn.SiLU(), nn.Linear(dim, dim)) + + def forward(self, timestep: torch.Tensor) -> torch.Tensor: + time_hidden = self.time_embed(timestep) + # Cast to model weight dtype (bfloat16) not input dtype (may be float32) + weight_dtype = self.time_mlp[0].weight.dtype + time_hidden = time_hidden.to(weight_dtype) + return self.time_mlp(time_hidden) + + +class GRN(nn.Module): + """Global Response Normalization layer.""" + + def __init__(self, dim: int): + super().__init__() + self.gamma = nn.Parameter(torch.zeros(1, 1, dim)) + self.beta = nn.Parameter(torch.zeros(1, 1, dim)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + gx = torch.norm(x, p=2, dim=1, keepdim=True) + nx = gx / (gx.mean(dim=-1, keepdim=True) + 1e-6) + return self.gamma * (x * nx) + self.beta + x + + +class ConvNeXtV2Block(nn.Module): + """ConvNeXt-V2 Block for text embedding.""" + + def __init__(self, dim: int, intermediate_dim: int, dilation: int = 1): + super().__init__() + padding = (dilation * (7 - 1)) // 2 + self.dwconv = nn.Conv1d(dim, dim, kernel_size=7, padding=padding, groups=dim, dilation=dilation) + self.norm = nn.LayerNorm(dim, eps=1e-6) + self.pwconv1 = nn.Linear(dim, intermediate_dim) + self.act = nn.GELU() + self.grn = GRN(intermediate_dim) + self.pwconv2 = nn.Linear(intermediate_dim, dim) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + residual = x + x = x.transpose(1, 2) + x = self.dwconv(x) + x = x.transpose(1, 2) + x = self.norm(x) + x = self.pwconv1(x) + x = self.act(x) + x = self.grn(x) + x = self.pwconv2(x) + return residual + x + + +class ConvPositionEmbedding(nn.Module): + """Convolutional position embedding.""" + + def __init__(self, dim: int, kernel_size: int = 31, groups: int = 16): + super().__init__() + if kernel_size % 2 == 0: + raise ValueError(f"kernel_size must be odd, got {kernel_size}") + self.conv1d = nn.Sequential( + nn.Conv1d(dim, dim, kernel_size, groups=groups, padding=kernel_size // 2), + nn.Mish(), + nn.Conv1d(dim, dim, kernel_size, groups=groups, padding=kernel_size // 2), + nn.Mish(), + ) + + def forward(self, x: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor: + if mask is not None: + mask = mask[..., None] + x = x.masked_fill(~mask, 0.0) + + x = rearrange(x, "b n d -> b d n") + x = self.conv1d(x) + out = rearrange(x, "b d n -> b n d") + + if mask is not None: + out = out.masked_fill(~mask, 0.0) + return out + + +class TextEmbedding(nn.Module): + """Text/speech token embedding with optional ConvNeXt modeling.""" + + def __init__( + self, + text_num_embeds: int, + output_dim: int, + conv_layers: int = 0, + conv_mult: int = 2, + length_align: str = "fill", + ): + super().__init__() + self.pad_id = int(text_num_embeds) + self.text_embed = nn.Embedding(text_num_embeds + 1, output_dim, padding_idx=self.pad_id) + self.length_align = length_align + + if conv_layers > 0: + self.extra_modeling = True + self.precompute_max_pos = 4096 + self.register_buffer( + "freqs_cis", precompute_freqs_cis(output_dim, self.precompute_max_pos), persistent=False + ) + self.text_blocks = nn.Sequential( + *[ConvNeXtV2Block(output_dim, output_dim * conv_mult) for _ in range(conv_layers)] + ) + else: + self.extra_modeling = False + + def forward( + self, + text: torch.Tensor, + aim_seq_len: int, + text_lens: torch.Tensor | None = None, + ) -> torch.Tensor: + batch, text_len = text.shape[0], text.shape[1] + if self.length_align == "fill": + text = text[:, :aim_seq_len] + text = F.pad(text, (0, aim_seq_len - min(text_len, aim_seq_len)), value=self.pad_id) + elif self.length_align == "interpolate_token": + if text_lens is None: + text = ( + F.interpolate( + text.unsqueeze(1).float(), + size=aim_seq_len, + mode="nearest", + ) + .squeeze(1) + .long() + ) + else: + text_lens = text_lens.to(device=text.device, dtype=torch.long) + text_lens = torch.clamp(text_lens, min=1, max=text_len) + positions = torch.arange(aim_seq_len, device=text.device, dtype=torch.long) + src_idx = positions.unsqueeze(0) * text_lens.unsqueeze(1) // int(aim_seq_len) + src_idx = torch.minimum(src_idx, (text_lens - 1).unsqueeze(1)) + text = torch.gather(text, dim=1, index=src_idx) + elif self.length_align != "interpolate_feature": + raise ValueError(f"Unsupported length_align={self.length_align!r}") + + hidden = self.text_embed(text) + + if self.length_align == "interpolate_feature": + hidden = F.interpolate( + hidden.permute(0, 2, 1), + size=aim_seq_len, + mode="nearest", + ).permute(0, 2, 1) + + if self.extra_modeling: + # Get model dtype from text_blocks weights + model_dtype = self.text_blocks[0].dwconv.weight.dtype + hidden = hidden.to(model_dtype) + + batch_start = torch.zeros((batch,), dtype=torch.long, device=text.device) + pos_idx = get_pos_embed_indices(batch_start, aim_seq_len, max_pos=self.precompute_max_pos) + text_pos_embed = self.freqs_cis[pos_idx].to(model_dtype) + hidden = hidden + text_pos_embed + hidden = self.text_blocks(hidden) + + return hidden + + +class EmbeddingConcater(nn.Module): + """Concatenates and projects noisy audio, condition, and text embeddings.""" + + def __init__(self, in_dim: int, out_dim: int): + super().__init__() + self.proj = nn.Linear(in_dim, out_dim) + self.conv_pos_embed = ConvPositionEmbedding(dim=out_dim) + + def forward( + self, + x: torch.Tensor, + cond: torch.Tensor, + text_embed: torch.Tensor, + drop_audio_cond: bool = False, + ) -> torch.Tensor: + if drop_audio_cond: + cond = torch.zeros_like(cond) + x = self.proj(torch.cat((x, cond, text_embed), dim=-1)) + x = self.conv_pos_embed(x) + x + return x + + +class AdaLayerNormZero(nn.Module): + """Adaptive LayerNorm with modulation from time/condition embedding.""" + + def __init__(self, dim: int, additional_dim: int = 0): + super().__init__() + self.silu = nn.SiLU() + self.linear = nn.Linear(dim + additional_dim, dim * 6) + self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6) + + def forward(self, x: torch.Tensor, emb: torch.Tensor): + emb = self.linear(self.silu(emb)) + shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = torch.chunk(emb, 6, dim=1) + x = self.norm(x) * (1 + scale_msa[:, None]) + shift_msa[:, None] + return x, gate_msa, shift_mlp, scale_mlp, gate_mlp + + +class AdaLayerNormZeroFinal(nn.Module): + """Adaptive LayerNorm for final layer.""" + + def __init__(self, dim: int, additional_dim: int = 0): + super().__init__() + self.silu = nn.SiLU() + self.linear = nn.Linear(dim + additional_dim, dim * 2) + self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6) + + def forward(self, x: torch.Tensor, emb: torch.Tensor) -> torch.Tensor: + emb = self.linear(self.silu(emb)) + scale, shift = torch.chunk(emb, 2, dim=1) + x = self.norm(x) * (1 + scale)[:, None, :] + shift[:, None, :] + return x + + +class FeedForward(nn.Module): + """Feed-forward network with GELU activation. + + Matches original GLM-TTS structure with nested Sequential for checkpoint compatibility. + Weight paths: ff.ff.0.0.* (first Linear), ff.ff.2.* (second Linear) + """ + + def __init__(self, dim: int, dim_out: int | None = None, mult: int = 4, dropout: float = 0.0): + super().__init__() + inner_dim = int(dim * mult) + dim_out = dim_out if dim_out is not None else dim + + # Nested structure to match original checkpoint paths + project_in = nn.Sequential( + nn.Linear(dim, inner_dim), + nn.GELU(approximate="tanh"), + ) + self.ff = nn.Sequential( + project_in, + nn.Dropout(dropout), + nn.Linear(inner_dim, dim_out), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.ff(x) + + +class DiTAttention(nn.Module): + """Attention module using diffusion infrastructure.""" + + def __init__(self, dim: int, heads: int = 8, dim_head: int = 64, dropout: float = 0.0): + super().__init__() + self.dim = dim + self.heads = heads + self.dim_head = dim_head + self.inner_dim = dim_head * heads + self.scale = 1.0 / math.sqrt(dim_head) + + self.to_q = nn.Linear(dim, self.inner_dim) + self.to_k = nn.Linear(dim, self.inner_dim) + self.to_v = nn.Linear(dim, self.inner_dim) + self.to_out = nn.Sequential(nn.Linear(self.inner_dim, dim), nn.Dropout(dropout)) + + self.attn = DiffusionAttention( + num_heads=heads, + head_size=dim_head, + softmax_scale=self.scale, + causal=False, + ) + + def forward( + self, + x: torch.Tensor, + padding_mask: torch.Tensor | None = None, + rope=None, + attn_mask: torch.Tensor | None = None, + ) -> torch.Tensor: + batch_size, seq_len = x.shape[0], x.shape[1] + + query = self.to_q(x) + key = self.to_k(x) + value = self.to_v(x) + + # Apply RoPE + if rope is not None: + freqs, xpos_scale = rope + q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0) + query = apply_rotary_pos_emb(query, freqs, q_xpos_scale) + key = apply_rotary_pos_emb(key, freqs, k_xpos_scale) + + # Reshape for attention + query = query.view(batch_size, seq_len, self.heads, self.dim_head) + key = key.view(batch_size, seq_len, self.heads, self.dim_head) + value = value.view(batch_size, seq_len, self.heads, self.dim_head) + + # Block-causal mask requires arbitrary 2D attention mask which + # FlashAttention does not support. Fall back to PyTorch SDPA + # (matches official GLM-TTS flow/modules.py:272). + if attn_mask is not None: + q = query.permute(0, 2, 1, 3) # [B, H, T, D] + k = key.permute(0, 2, 1, 3) + v = value.permute(0, 2, 1, 3) + out = F.scaled_dot_product_attention( + q, + k, + v, + attn_mask=attn_mask, + dropout_p=0.0, + is_causal=False, + ) + out = out.permute(0, 2, 1, 3) # [B, T, H, D] + else: + out = self.attn(query, key, value, attn_metadata=None) + out = out.view(batch_size, seq_len, self.inner_dim) + out = out.to(query.dtype) + out = self.to_out(out) + + # Apply padding mask + if padding_mask is not None: + padding_mask = rearrange(padding_mask, "b 1 n -> b n 1").bool() + out = out.masked_fill(~padding_mask, 0.0) + + return out + + +class DiTBlock(nn.Module): + """DiT block with AdaLayerNorm modulation.""" + + def __init__( + self, + dim: int, + heads: int, + dim_head: int, + ff_mult: int = 4, + dropout: float = 0.1, + additional_condition_dim: int = 0, + ): + super().__init__() + self.attn_norm = AdaLayerNormZero(dim, additional_condition_dim) + self.attn = DiTAttention(dim=dim, heads=heads, dim_head=dim_head, dropout=dropout) + self.ff_norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6) + self.ff = FeedForward(dim=dim, mult=ff_mult, dropout=dropout) + + def forward( + self, + x: torch.Tensor, + t: torch.Tensor, + padding_mask: torch.Tensor | None = None, + rope=None, + attn_mask: torch.Tensor | None = None, + ) -> torch.Tensor: + # Pre-norm & modulation for attention + norm, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.attn_norm(x, emb=t) + + # Attention + attn_output = self.attn(x=norm, padding_mask=padding_mask, rope=rope, attn_mask=attn_mask) + x = x + gate_msa.unsqueeze(1) * attn_output + + # FeedForward + norm = self.ff_norm(x) * (1 + scale_mlp[:, None]) + shift_mlp[:, None] + ff_output = self.ff(norm) + x = x + gate_mlp.unsqueeze(1) * ff_output + + return x + + +class GLMTTSDiT(nn.Module): + """GLM-TTS Diffusion Transformer. + + Converts speech tokens to mel-spectrogram via flow matching. + """ + + _repeated_blocks = ["DiTBlock"] + _layerwise_offload_blocks_attrs = ["transformer_blocks"] + + def __init__( + self, + *, + trans_dim: int = 768, + depth: int = 18, + heads: int = 12, + dim_head: int = 64, + ff_mult: int = 2, + dropout: float = 0.1, + mel_dim: int = 80, + text_vocab_size: int = 100000, + text_emb_dim: int = 512, + conv_layers: int = 4, + condition_dim: int = 160, # mel_dim + spk_embed_dim + spkr_emb_adaln: bool = False, + spkr_dim: int = 192, + use_wavlm_emb: bool = False, + long_skip_connection: bool = False, + ): + super().__init__() + + self.time_embed = TimestepEmbedding(trans_dim) + self.text_emb_layer = TextEmbedding( + text_vocab_size, + text_emb_dim, + conv_layers=conv_layers, + length_align="interpolate_token", + ) + + in_dim = mel_dim + text_emb_dim + condition_dim + self.emb_concator = EmbeddingConcater(in_dim, trans_dim) + + self.rotary_embed = RotaryEmbedding(dim_head) + + self.dim = trans_dim + self.depth = depth + self.num_heads = heads + self.spkr_emb_adaln = spkr_emb_adaln + + # Frame rates for block-causal mask token→mel conversion + self._input_frame_rate: float = 25.0 + self._mel_framerate: float = 50.0 + + # Speaker embedding dimension for AdaLN + if spkr_emb_adaln: + additional_dim = spkr_dim + 256 if use_wavlm_emb else spkr_dim + else: + additional_dim = 0 + + self.transformer_blocks = nn.ModuleList( + [ + DiTBlock( + dim=trans_dim, + heads=heads, + dim_head=dim_head, + ff_mult=ff_mult, + dropout=dropout, + additional_condition_dim=additional_dim, + ) + for _ in range(depth) + ] + ) + + self.long_skip_connection = nn.Linear(trans_dim * 2, trans_dim, bias=False) if long_skip_connection else None + + self.norm_out = AdaLayerNormZeroFinal(trans_dim, additional_dim=additional_dim) + self.proj_out = nn.Linear(trans_dim, mel_dim) + + # ------------------------------------------------------------------ + # Block-causal attention mask (ported from GLM-TTS block_mask_util) + # ------------------------------------------------------------------ + + def _token_to_mel_size(self, token_count: int) -> int: + """Convert token block size to mel-frame block size for streaming mask. + + Matches upstream GLM-TTS ``flow/dit.py`` block-mask conversion. + This is intentionally separate from normal ``feat_len`` calculation, + which still follows ``input_frame_rate`` and ``mel_framerate``. + """ + return int(token_count / 12.5 * 22050 / 256) + + def _create_block_causal_mask( + self, + block_pattern: list[int], + seq_len: int, + device: torch.device, + dtype: torch.dtype, + ) -> torch.Tensor: + """Build block-causal attention mask [1, 1, T, T]. + + Ported from GLM-TTS utils/block_mask_util.py ``create()``. + Each position can attend to its own block plus a look-ahead into + the next block (50% for small blocks, 66% for large blocks). + + Vectorized via ``torch.searchsorted`` — no Python loop over T. + """ + import itertools + + mel_blocks = [self._token_to_mel_size(b) for b in block_pattern] + # Extend with copies of the last block (enough for any seq_len) + extended = mel_blocks + [mel_blocks[-1]] * ((seq_len // max(mel_blocks[-1], 1)) + 2) + accum = list(itertools.accumulate(extended)) + + # Vectorized: map each position to its block index via searchsorted + accum_t = torch.tensor(accum, dtype=torch.long) + extended_t = torch.tensor(extended, dtype=torch.long) + positions = torch.arange(seq_len, dtype=torch.long) + + # searchsorted(accum, positions, right=True) gives the block index + # for each position (accum is strictly increasing). + block_idx = torch.searchsorted(accum_t, positions, right=True) + + # Per-position: block start, block size, position within block + block_start = torch.where(block_idx > 0, accum_t[block_idx - 1], torch.zeros_like(block_idx)) + cur_block_size = extended_t[block_idx] + idx_in_block = positions - block_start + + # Look-ahead: positions near the end of a block can see into the next + look_future = cur_block_size - 1 - idx_in_block + min_future = torch.where( + cur_block_size <= 172, + cur_block_size // 2, + (cur_block_size.float() / 1.5).long(), + ) + delta = torch.clamp(min_future - look_future, min=0) + aim_len = torch.clamp(block_start + cur_block_size + delta, max=seq_len) + + # Build mask: position i can attend to columns [0, aim_len[i]) + col_idx = torch.arange(seq_len, device=device) + mask = col_idx.unsqueeze(0) < aim_len.unsqueeze(1).to(device) + + # [1, 1, T, T] for broadcast across batch and heads + return mask.unsqueeze(0).unsqueeze(0) + + def forward( + self, + middle_point: torch.Tensor, + condition: torch.Tensor, + text: torch.Tensor, + time_step: torch.Tensor, + padding_mask: torch.Tensor, + spkr_emb: torch.Tensor | None = None, + text_lens: torch.Tensor | None = None, + is_causal: bool = False, + attn_mask: torch.Tensor | None = None, + block_pattern: list[int] | None = None, + ) -> torch.Tensor: + """Forward pass. + + Args: + middle_point: Noised audio tensor [B, T, mel_dim] + condition: Masked audio condition [B, T, condition_dim] + text: Speech token tensor [B, T_text] + time_step: Timestep tensor [B] + padding_mask: Padding mask [B, T] + spkr_emb: Speaker embedding [B, spkr_dim] + is_causal: Use block-causal attention + attn_mask: Custom attention mask (overrides is_causal) + block_pattern: Token-level block sizes for block-causal mask + + Returns: + Predicted mel-spectrogram [B, T, mel_dim] + """ + # Cast floating inputs to model dtype (weights may be bfloat16) + model_dtype = self.proj_out.weight.dtype + middle_point = middle_point.to(model_dtype) + condition = condition.to(model_dtype) + time_step = time_step.to(model_dtype) + if spkr_emb is not None: + spkr_emb = spkr_emb.to(model_dtype) + + _, seq_len = middle_point.shape[0], middle_point.shape[1] + + # Time embedding + time_emb = self.time_embed(time_step) + + # Concatenate speaker embedding if using AdaLN + if self.spkr_emb_adaln and spkr_emb is not None: + time_emb = torch.cat([time_emb, spkr_emb], dim=-1) + + # Text embedding - ensure cast to model dtype + text_embed = self.text_emb_layer(text, seq_len, text_lens=text_lens).to(model_dtype) + + # Input projection + x = self.emb_concator(middle_point, condition, text_embed, drop_audio_cond=False) + + # Rotary embeddings + rope = self.rotary_embed.forward_from_seq_len(seq_len) + + # Build block-causal mask when is_causal and no explicit attn_mask + if attn_mask is None and is_causal and block_pattern is not None: + attn_mask = self._create_block_causal_mask( + block_pattern, + seq_len, + middle_point.device, + model_dtype, + ) + + # Long skip connection + if self.long_skip_connection is not None: + residual = x + + # Transformer blocks + for block in self.transformer_blocks: + x = block(x, time_emb, padding_mask=padding_mask.unsqueeze(1), rope=rope, attn_mask=attn_mask) + + # Long skip connection + if self.long_skip_connection is not None: + x = self.long_skip_connection(torch.cat((x, residual), dim=-1)) + + # Output projection + x = self.norm_out(x, time_emb) + output = self.proj_out(x) + + return output diff --git a/vllm_omni/model_executor/models/glm_tts/glm_tts_dit_wrapper.py b/vllm_omni/model_executor/models/glm_tts/glm_tts_dit_wrapper.py new file mode 100644 index 000000000..a7c26af30 --- /dev/null +++ b/vllm_omni/model_executor/models/glm_tts/glm_tts_dit_wrapper.py @@ -0,0 +1,1301 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""GLM-TTS DiT wrapper: LLM_GENERATION-compatible flow-matching + vocoder.""" + +from __future__ import annotations + +import dataclasses +import json +import math +import os +import threading +from collections.abc import Iterable +from typing import Any + +import numpy as np +import torch +import torch.nn as nn +from torch.cuda import CUDAGraph +from vllm.config import VllmConfig +from vllm.logger import init_logger +from vllm.platforms import current_platform + +from vllm_omni.model_executor.models.glm_tts.glm_tts import ( + resolve_glm_tts_model_dir, +) +from vllm_omni.model_executor.models.glm_tts.vocoder import ( + load_vocoder, + mel_to_audio, +) +from vllm_omni.model_executor.models.output_templates import OmniOutput + +logger = init_logger(__name__) + + +@dataclasses.dataclass +class _StreamRequestState: + """Per-request streaming state (replaces 5 separate dicts).""" + + diff_cache: dict[int | str, Any] = dataclasses.field(default_factory=dict) + prompt_payload: dict[str, torch.Tensor] = dataclasses.field(default_factory=dict) + audio_tail: torch.Tensor | None = None + wav_pointer: int = 0 + fade_tail: torch.Tensor | None = None + + +_GLM_TTS_OFFICIAL_FLOW_FALLBACK: dict[str, Any] = { + "spkr_emb_adaLN": True, + "speech_token_cfg": False, + "remove_spkr_concat_condition": True, + "mel_dim": 80, + "mel_framerate": 50, + "input_frame_rate": 25, +} + +_GLM_TTS_RUNTIME_FLOW_DEFAULTS: dict[str, Any] = { + "sample_rate": 24000, + "n_timesteps": 10, + "t_scheduler": "cosine", + "inference_cfg_rate": 0.7, + "spk_embed_dim": 80, + "trans_dim": 768, + "depth": 18, + "heads": 12, +} + + +def _load_glm_tts_flow_config(flow_dir: str) -> dict[str, Any]: + """Load GLM-TTS flow config from JSON/YAML, falling back to official defaults.""" + config: dict[str, Any] = dict(_GLM_TTS_OFFICIAL_FLOW_FALLBACK) + + # Try config.json first (standard HuggingFace layout) + json_path = os.path.join(flow_dir, "config.json") + if os.path.exists(json_path): + try: + with open(json_path) as f: + raw = json.load(f) + flow = raw.get("flow", raw) if isinstance(raw, dict) else {} + if isinstance(flow, dict): + config.update(flow) + except Exception as e: + logger.warning("Failed to parse GLM-TTS flow config.json: %s", e) + + # Try config.yaml (HyperPyYAML — strip tags, only extract kwargs) + yaml_path = os.path.join(flow_dir, "config.yaml") + if os.path.exists(yaml_path): + try: + import yaml + + class _TagStripper(yaml.SafeLoader): + pass + + _TagStripper.add_multi_constructor( + "!", + lambda loader, _tag, node: ( + loader.construct_mapping(node, deep=True) + if isinstance(node, yaml.MappingNode) + else loader.construct_sequence(node, deep=True) + if isinstance(node, yaml.SequenceNode) + else loader.construct_scalar(node) + ), + ) + with open(yaml_path) as f: + raw = yaml.load(f, Loader=_TagStripper) + flow = raw.get("flow", raw) if isinstance(raw, dict) else {} + if isinstance(flow, dict): + config.update(flow) + except Exception as e: + logger.warning("Failed to parse GLM-TTS flow config.yaml: %s", e) + + return config + + +def make_pad_mask(lengths: torch.Tensor, max_len: int | None = None) -> torch.Tensor: + if max_len is None: + max_len = int(lengths.max().item()) + seq_range = torch.arange(0, max_len, device=lengths.device) + return seq_range.unsqueeze(0).expand(lengths.shape[0], max_len) >= lengths.unsqueeze(1) + + +def _unwrap(value: object) -> object: + """Unwrap single-element lists (common runtime_info wrapping).""" + return value[0] if isinstance(value, list) and value else value + + +def nested_get(info: dict[str, Any], section: str, key: str) -> Any: + nested = info.get(section) + return nested[key] if isinstance(nested, dict) and key in nested else info.get(key) + + +def _glm_tts_private_info(info: dict[str, Any]) -> dict[str, Any]: + private = info.get("glm_tts") + if isinstance(private, dict): + return private + kv_metadata = info.get("kv_metadata") + if isinstance(kv_metadata, dict): + private = kv_metadata.get("glm_tts") + if isinstance(private, dict): + return private + return {} + + +def as_tensor(value: object) -> torch.Tensor | None: + value = _unwrap(value) + return value if isinstance(value, torch.Tensor) else None + + +def as_bool(value: object) -> bool: + value = _unwrap(value) + if isinstance(value, torch.Tensor) and value.numel() > 0: + return bool(value.reshape(-1)[0].item()) + return bool(value) if value is not None else False + + +def as_str(value: object) -> str | None: + value = _unwrap(value) + return str(value) if value is not None else None + + +def _as_scalar(value: object, typ: type, default: int | float = 0): + """Extract scalar int/float from runtime_info value.""" + value = _unwrap(value) + if isinstance(value, torch.Tensor) and value.numel() > 0: + return typ(value.reshape(-1)[0].item()) + if value is None: + return default + try: + return typ(value) + except (TypeError, ValueError): + return default + + +def as_int(value: object, default: int = 0) -> int: + return _as_scalar(value, int, default) + + +def as_float(value: object, default: float = 0.0) -> float: + return _as_scalar(value, float, default) + + +def as_int_list(value: object) -> list[int] | None: + if value is None: + return None + if isinstance(value, torch.Tensor): + return [int(x) for x in value.reshape(-1).tolist()] if value.numel() else [] + if isinstance(value, list): + return [int(x.item() if hasattr(x, "item") else x) for x in value] + return None + + +def valid_speech_tokens( + value: object, + *, + device: torch.device, + fallback: torch.Tensor | None = None, +) -> torch.Tensor: + """Return GLM-TTS speech tokens, treating an explicit empty list as empty.""" + if value is None: + token = fallback if fallback is not None else torch.empty(0, device=device, dtype=torch.long) + elif isinstance(value, torch.Tensor): + token = value.to(device=device, dtype=torch.long) + elif isinstance(value, np.ndarray): + token = torch.as_tensor(value, device=device, dtype=torch.long) + elif isinstance(value, list): + if len(value) == 1 and isinstance(value[0], torch.Tensor): + token = value[0].to(device=device, dtype=torch.long) + elif value and all(isinstance(item, torch.Tensor) for item in value): + token = torch.cat([item.reshape(-1).to(device=device, dtype=torch.long) for item in value]) + else: + token = torch.tensor(value, device=device, dtype=torch.long) + else: + token = fallback if fallback is not None else torch.empty(0, device=device, dtype=torch.long) + + token = token.reshape(-1).to(device=device, dtype=torch.long) + return token[(token >= 0) & (token < 32768)] + + +def split_request_ids(ids: torch.Tensor, seq_token_counts: list[int] | None = None) -> list[torch.Tensor]: + if seq_token_counts is not None: + boundaries, total = [0], ids.numel() + for count in seq_token_counts: + boundaries.append(boundaries[-1] + int(count)) + return [ids[boundaries[i] : min(boundaries[i + 1], total)] for i in range(len(seq_token_counts))] + try: + from vllm.forward_context import get_forward_context, is_forward_context_available + + if is_forward_context_available(): + slices = get_forward_context().ubatch_slices + if slices is not None and len(slices) > 1 and not any(hasattr(s, "token_slice") for s in slices): + boundaries = [0] + for s in slices: + boundaries.append(boundaries[-1] + int(s)) + return [ids[boundaries[i] : boundaries[i + 1]] for i in range(len(boundaries) - 1)] + except ImportError: + pass + return [ids] + + +class GLMTTSDiTForGeneration(nn.Module): + """GLM-TTS DiT flow-matching stage wrapped as LLM_GENERATION.""" + + have_multimodal_outputs = True + enable_update_additional_information = True + + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): + super().__init__() + config = vllm_config.model_config.hf_config + self.config = config + self.model_stage = "glm_tts_dit" + self.max_num_seqs = int(getattr(vllm_config.scheduler_config, "max_num_seqs", 1)) + self._use_dit_cuda_graphs = bool(getattr(config, "use_dit_cuda_graphs", False)) + self._dit_cudagraph: CUDAGraphGLMTTSDiTWrapper | None = None + self._codec_chunk_frames, self._codec_left_context_frames = self._connector_chunk_config(vllm_config) + + model_path = vllm_config.model_config.model + model_path = resolve_glm_tts_model_dir( + model_path, + tokenizer_path=getattr(vllm_config.model_config, "tokenizer", None), + required_files=("flow/flow.pt",), + optional_files=("hift/hift.pt", "vocos2d/generator_jit.ckpt"), + ) + self.model_dir = model_path + flow_dir = os.path.join(self.model_dir, "flow") + self.hf_flow_config = _load_glm_tts_flow_config(flow_dir) + logger.info("GLM-TTS flow config: %s", self.hf_flow_config) + + self.mel_dim = self.hf_flow_config.get("mel_dim", 80) + self.input_frame_rate = self.hf_flow_config.get("input_frame_rate", 25.0) + self.mel_framerate = self.hf_flow_config.get("mel_framerate", 50) + self.sample_rate = self.hf_flow_config.get("sample_rate", _GLM_TTS_RUNTIME_FLOW_DEFAULTS["sample_rate"]) + + self.n_timesteps = self.hf_flow_config.get("n_timesteps", _GLM_TTS_RUNTIME_FLOW_DEFAULTS["n_timesteps"]) + config_n_ts = getattr(config, "n_timesteps", None) + if config_n_ts is not None: + self.n_timesteps = int(config_n_ts) + self.t_scheduler = self.hf_flow_config.get("t_scheduler", _GLM_TTS_RUNTIME_FLOW_DEFAULTS["t_scheduler"]) + self.inference_cfg_rate = self.hf_flow_config.get( + "inference_cfg_rate", _GLM_TTS_RUNTIME_FLOW_DEFAULTS["inference_cfg_rate"] + ) + self.speech_token_cfg = self.hf_flow_config.get("speech_token_cfg", False) + + self.spk_embed_dim = self.hf_flow_config.get("spk_embed_dim", _GLM_TTS_RUNTIME_FLOW_DEFAULTS["spk_embed_dim"]) + self.spkr_emb_adaln = self.hf_flow_config.get("spkr_emb_adaLN", True) + self.remove_spkr_concat_condition = self.hf_flow_config.get("remove_spkr_concat_condition", True) + + from vllm_omni.model_executor.models.glm_tts.glm_tts_dit import GLMTTSDiT + + trans_dim = self.hf_flow_config.get("trans_dim", _GLM_TTS_RUNTIME_FLOW_DEFAULTS["trans_dim"]) + depth = self.hf_flow_config.get("depth", _GLM_TTS_RUNTIME_FLOW_DEFAULTS["depth"]) + heads = self.hf_flow_config.get("heads", _GLM_TTS_RUNTIME_FLOW_DEFAULTS["heads"]) + condition_dim = self.mel_dim if self.remove_spkr_concat_condition else self.mel_dim + self.spk_embed_dim + + self.dit = GLMTTSDiT( + trans_dim=trans_dim, + depth=depth, + heads=heads, + dim_head=64, + ff_mult=2, + dropout=0.0, + mel_dim=self.mel_dim, + text_vocab_size=100000, + text_emb_dim=512, + conv_layers=4, + condition_dim=condition_dim, + spkr_emb_adaln=self.spkr_emb_adaln, + spkr_dim=192, + use_wavlm_emb=False, + ) + self._model_dtype = self._resolve_model_dtype(vllm_config) + if self._model_dtype is not None: + self.dit.to(dtype=self._model_dtype) + + if not self.remove_spkr_concat_condition: + self.spk_embed_affine_layer = nn.Linear(192, self.spk_embed_dim) + if self._model_dtype is not None: + self.spk_embed_affine_layer.to(dtype=self._model_dtype) + + self._vocoder = None + self._vocoder_sample_rate: int | None = None + + self._stream_cache_lock = threading.Lock() + self._stream_state_by_req: dict[str, _StreamRequestState] = {} + # Track recently-seen req_ids so stale entries (aborted requests) + # can be evicted without modifying the shared model runner. + self._stream_last_seen: dict[str, int] = {} + self._stream_forward_count: int = 0 + + self._flow_weights_path = os.path.join(self.model_dir, "flow", "flow.pt") + self._hift_weights_path = os.path.join(self.model_dir, "hift", "hift.pt") + self._vocos_jit_path = os.path.join(self.model_dir, "vocos2d", "generator_jit.ckpt") + + @staticmethod + def _connector_chunk_config(vllm_config: VllmConfig) -> tuple[int, int]: + cc = getattr(vllm_config.model_config, "stage_connector_config", None) + extra = (cc or {}).get("extra") if isinstance(cc, dict) else getattr(cc, "extra", None) + if not isinstance(extra, dict): + return 25, 25 + cf = extra.get("codec_chunk_frames", 25) + return (int(cf[0]) if isinstance(cf, list) else int(cf)), int(extra.get("codec_left_context_frames", 25)) + + @staticmethod + def _resolve_model_dtype(vllm_config: VllmConfig) -> torch.dtype | None: + dtype = getattr(vllm_config.model_config, "dtype", None) + if isinstance(dtype, torch.dtype): + return dtype + if isinstance(dtype, str): + _aliases = {"bf16": "bfloat16", "fp16": "float16", "half": "float16", "fp32": "float32"} + return getattr(torch, _aliases.get(dtype.lower(), dtype.lower()), None) + return None + + def _ensure_vocoder_loaded(self) -> None: + if self._vocoder is not None: + return + + device = next(self.dit.parameters()).device + self._vocoder, self._vocoder_sample_rate = load_vocoder(self.model_dir, device, self.sample_rate) + if self._vocoder is None: + raise RuntimeError( + "GLM-TTS stage 1 could not load a vocoder. " + f"Expected either {self._hift_weights_path} or {self._vocos_jit_path}." + ) + + def embed_input_ids( + self, + input_ids: torch.Tensor, + **_: Any, + ) -> torch.Tensor: + """Return zero embeddings (DiT does not use token embeddings).""" + if input_ids.dim() != 1: + raise ValueError(f"GLM-TTS DiT input_ids must be 1D, got shape={tuple(input_ids.shape)}") + hidden = int(self.config.hidden_size) + return torch.zeros( + (input_ids.shape[0], hidden), + device=input_ids.device, + dtype=self._model_dtype or torch.float32, + ) + + def compute_logits(self, hidden_states: Any, sampling_metadata: Any = None) -> None: + """DiT does not produce logits for sampling.""" + return None + + def forward( + self, + input_ids: torch.Tensor, + positions: torch.Tensor, + **kwargs: Any, + ) -> OmniOutput: + """Run DiT flow-matching + vocoder on speech tokens.""" + # Evict stale streaming caches (aborted requests whose stream_finished + # was never sent). Entries not seen for 64 forward calls are dropped. + self._evict_stale_stream_caches() + + runtime_info = kwargs.get("model_intermediate_buffer") or [] + + seq_token_counts = kwargs.get("seq_token_counts") + flat_ids = input_ids.reshape(-1).to(dtype=torch.long) + if seq_token_counts is None and self.max_num_seqs > 1: + raise RuntimeError( + "GLM-TTS DiT stage requires seq_token_counts when max_num_seqs > 1; " + "otherwise concatenated speech tokens cannot be split per request." + ) + request_ids_list = split_request_ids(flat_ids, seq_token_counts) + + num_reqs = max(1, len(request_ids_list)) + sample_rate = torch.tensor(int(self.sample_rate), dtype=torch.int32) + empty_audio = torch.zeros((0,), dtype=torch.float32, device=input_ids.device) + audios: list[torch.Tensor] = [empty_audio] * num_reqs + srs: list[torch.Tensor] = [sample_rate] * num_reqs + + if not isinstance(runtime_info, list): + runtime_info = [] + + for idx, req_ids in enumerate(request_ids_list): + info = runtime_info[idx] if idx < len(runtime_info) and isinstance(runtime_info[idx], dict) else {} + req_id = as_str(nested_get(info, "meta", "req_id")) if info else None + if req_id is not None: + with self._stream_cache_lock: + self._stream_last_seen[req_id] = self._stream_forward_count + stream_finished_raw = nested_get(info, "meta", "stream_finished") if info else None + if stream_finished_raw is None and info: + stream_finished_raw = info.get("stream_finished") + stream_finished = as_bool(stream_finished_raw) if info else False + prompt_token = as_tensor(nested_get(info, "embed", "speech_token")) if info else None + if prompt_token is None and info: + prompt_token = as_tensor(info.get("prompt_token")) # backward compat + if prompt_token is None and info: + prompt_token = as_tensor(info.get("prompt_speech_token")) # backward compat + prompt_feat = as_tensor(nested_get(info, "embed", "speech_feat")) if info else None + if prompt_feat is None and info: + prompt_feat = as_tensor(info.get("prompt_feat")) # backward compat + embedding = as_tensor(nested_get(info, "embed", "embedding")) if info else None + if embedding is None and info: + embedding = as_tensor(info.get("embedding")) # backward compat + + speech_tokens_raw = info.get("speech_tokens") if info else None + has_meta = isinstance(info.get("meta"), dict) if info else False + uses_streaming = bool(info) and ( + has_meta + or "stream_finished" in info + or "token_offset" in info + or "left_context_size" in info + or "glm_tts" in info + or "kv_metadata" in info + ) + if uses_streaming and req_id is not None: + with self._stream_cache_lock: + state = self._stream_state_by_req.setdefault(req_id, _StreamRequestState()) + prompt_payload = state.prompt_payload + if prompt_token is not None: + prompt_payload["prompt_speech_token"] = prompt_token + elif "prompt_speech_token" in prompt_payload: + prompt_token = prompt_payload["prompt_speech_token"] + if prompt_feat is not None: + prompt_payload["prompt_feat"] = prompt_feat + elif "prompt_feat" in prompt_payload: + prompt_feat = prompt_payload["prompt_feat"] + if embedding is not None: + prompt_payload["embedding"] = embedding + elif "embedding" in prompt_payload: + embedding = prompt_payload["embedding"] + + fallback_token = req_ids.reshape(-1).to(device=input_ids.device, dtype=torch.long) + if uses_streaming: + token = valid_speech_tokens(None, device=input_ids.device, fallback=fallback_token) + else: + token = valid_speech_tokens( + speech_tokens_raw, + device=input_ids.device, + fallback=fallback_token, + ) + + if token.numel() == 0: + if stream_finished and req_id is not None: + with self._stream_cache_lock: + self._stream_state_by_req.pop(req_id, None) + self._stream_last_seen.pop(req_id, None) + audios[idx] = empty_audio + continue + + if uses_streaming: + private_info = _glm_tts_private_info(info) + token_offset_raw = nested_get(info, "meta", "left_context_size") + if token_offset_raw is None: + token_offset_raw = info.get("left_context_size") + if token_offset_raw is None: + token_offset_raw = nested_get(info, "meta", "token_offset") + if token_offset_raw is None: + token_offset_raw = info.get("token_offset") + token_offset = max( + 0, + as_int(token_offset_raw, 0), + ) + + chunk_sizes_raw = private_info.get("chunk_sizes_history") + if chunk_sizes_raw is None: + chunk_sizes_raw = nested_get(info, "meta", "chunk_sizes_history") + if chunk_sizes_raw is None: + chunk_sizes_raw = info.get("chunk_sizes_history") + block_pattern_raw = private_info.get("block_pattern") + if block_pattern_raw is None: + block_pattern_raw = nested_get(info, "meta", "block_pattern") + if block_pattern_raw is None: + block_pattern_raw = info.get("block_pattern") + crossfade_raw = private_info.get("crossfade_sec") + if crossfade_raw is None: + crossfade_raw = nested_get(info, "meta", "crossfade_sec") + if crossfade_raw is None: + crossfade_raw = info.get("crossfade_sec") + chunk_sizes_history = as_int_list(chunk_sizes_raw) + block_pattern = as_int_list(block_pattern_raw) + crossfade_sec = as_float(crossfade_raw, 0.1) + + audio = self._forward_full( + speech_tokens=token, + prompt_token=prompt_token, + prompt_feat=prompt_feat, + embedding=embedding, + token_offset=token_offset, + req_id=req_id, + stream_finished=stream_finished, + chunk_sizes_history=chunk_sizes_history, + block_pattern=block_pattern, + crossfade_sec=crossfade_sec, + ) + if stream_finished and req_id is not None: + with self._stream_cache_lock: + self._stream_state_by_req.pop(req_id, None) + self._stream_last_seen.pop(req_id, None) + else: + audio = self._forward_full( + speech_tokens=token, + prompt_token=prompt_token, + prompt_feat=prompt_feat, + embedding=embedding, + ) + + audios[idx] = audio.reshape(-1).to(dtype=torch.float32) + + return OmniOutput( + text_hidden_states=None, + multimodal_outputs={"audio": audios, "sr": srs}, + ) + + @staticmethod + def _to_2d(val: torch.Tensor | np.ndarray | None, device: torch.device, dtype: torch.dtype) -> torch.Tensor | None: + """Convert numpy/tensor to 2D+ tensor on device, or return None.""" + if val is None: + return None + if isinstance(val, np.ndarray): + val = torch.from_numpy(val) + val = val.to(device=device, dtype=dtype) + if val.ndim == 1: + val = val.unsqueeze(0) + return val + + @staticmethod + def _validate_voice_clone(prompt_token, prompt_feat, embedding) -> None: + """Raise if voice clone payload is incomplete.""" + fields = {"prompt_speech_token": prompt_token, "prompt_feat": prompt_feat, "embedding": embedding} + present = [k for k, v in fields.items() if v is not None] + missing = [k for k, v in fields.items() if v is None] + if present and missing: + raise RuntimeError(f"GLM-TTS voice clone payload incomplete: present={present}, missing={missing}.") + + @torch.inference_mode() + def _forward_full( + self, + speech_tokens: torch.Tensor, + prompt_token: torch.Tensor | None = None, + prompt_feat: torch.Tensor | None = None, + embedding: torch.Tensor | None = None, + token_offset: int = 0, + req_id: str | None = None, + stream_finished: bool = False, + chunk_sizes_history: list[int] | None = None, + block_pattern: list[int] | None = None, + crossfade_sec: float = 0.1, + ) -> torch.Tensor: + """DiT flow-matching + vocoder pipeline. Returns audio waveform.""" + device = speech_tokens.device + self._validate_voice_clone(prompt_token, prompt_feat, embedding) + + if speech_tokens.ndim == 1: + speech_tokens = speech_tokens.unsqueeze(0) + generated_token_len = int(speech_tokens.shape[1]) + + has_voice_clone = prompt_token is not None or embedding is not None + prompt_token = self._to_2d(prompt_token, device, torch.long) + if prompt_token is not None: + prompt_token_len = prompt_token.shape[1] + speech_tokens = torch.cat([prompt_token, speech_tokens], dim=1) + else: + prompt_token_len = 0 + has_prompt_audio_context = prompt_token_len > 0 + + token_len = speech_tokens.shape[1] + if token_len == 0: + return torch.zeros(0, device=device) + + text_lens = torch.tensor([token_len], device=device, dtype=torch.long) + feat_len = max(1, int(token_len / self.input_frame_rate * self.mel_framerate)) + + padding_mask = ~make_pad_mask(torch.tensor([feat_len], device=device), max_len=feat_len) + mel_cond = torch.zeros([1, feat_len, self.mel_dim], device=device) + + prompt_feat_len = 0 + prompt_feat = self._to_2d(prompt_feat, device, mel_cond.dtype) + if prompt_feat is not None: + if prompt_feat.ndim == 2: + prompt_feat = prompt_feat.unsqueeze(0) + copy_len = min(prompt_feat.shape[1], feat_len) + mel_cond[:, :copy_len, :] = prompt_feat[:, -copy_len:, :] + prompt_feat_len = copy_len + has_prompt_audio_context = has_prompt_audio_context and prompt_feat_len > 0 + + embedding = self._to_2d(embedding, device, mel_cond.dtype) + spkr_embedding = None + if not self.remove_spkr_concat_condition: + if has_voice_clone and embedding is not None: + spk_embedding_normed = torch.nn.functional.normalize(embedding, dim=1) + spk_embedding = self.spk_embed_affine_layer(spk_embedding_normed) + spk_embedding_expanded = spk_embedding.unsqueeze(1).expand(-1, feat_len, -1) + condition = torch.cat([mel_cond, spk_embedding_expanded], dim=-1) + spkr_embedding = spk_embedding_normed + else: + spk_zeros = torch.zeros([1, feat_len, self.spk_embed_dim], device=device) + condition = torch.cat([mel_cond, spk_zeros], dim=-1) + else: + condition = mel_cond + + if self.spkr_emb_adaln: + if has_voice_clone and embedding is not None: + if spkr_embedding is None: + spkr_embedding = torch.nn.functional.normalize(embedding, dim=1) + else: + spkr_embedding = torch.zeros([1, 192], device=device, dtype=mel_cond.dtype) + + with self._stream_cache_lock: + _rs = self._stream_state_by_req.get(req_id) if req_id is not None else None + last_step_cache = _rs.diff_cache if _rs is not None else None + + dit_block_pattern: list[int] | None = None + is_streaming = req_id is not None and token_offset >= 0 + if is_streaming: + pattern_source = block_pattern or chunk_sizes_history + if pattern_source is not None and len(pattern_source) > 0: + if prompt_token_len > 0: + dit_block_pattern = [prompt_token_len] + pattern_source + else: + dit_block_pattern = list(pattern_source) + + lookahead_mel_len = 0 + if is_streaming and chunk_sizes_history is not None and len(chunk_sizes_history) > 0: + last_chunk_tokens = chunk_sizes_history[-1] + lookahead_tokens = last_chunk_tokens // 2 + lookahead_mel_len = int(lookahead_tokens / self.input_frame_rate * self.mel_framerate) + + # Compute cache_len: only cache the stable region for streaming + stream_cache_len: int | None = None + if req_id is not None: + stable_mel_len = int(generated_token_len / self.input_frame_rate * self.mel_framerate) + override_mel = max(0, stable_mel_len - lookahead_mel_len) + stream_cache_len = min(feat_len, prompt_feat_len + override_mel + 1) + + mel, current_step_cache = self._do_sample( + speech_tokens=speech_tokens, + mel_cond=mel_cond, + condition=condition, + padding_mask=padding_mask, + spkr_embedding=spkr_embedding, + n_timesteps=self.n_timesteps, + last_step_cache=last_step_cache, + block_pattern=dit_block_pattern, + cache_len=stream_cache_len, + text_lens=text_lens, + ) + + if req_id is not None: + current_step_cache["override_len"] = int(stream_cache_len or 0) + with self._stream_cache_lock: + rs = self._stream_state_by_req.setdefault(req_id, _StreamRequestState()) + rs.diff_cache = current_step_cache + + if has_prompt_audio_context: + crop_len = min(prompt_feat_len, max(0, int(mel.shape[1]) - 1)) + if crop_len > 0: + mel = mel[:, crop_len:, :] + + self._ensure_vocoder_loaded() + + audio = mel_to_audio(self._vocoder, mel) + + effective_sr = self._vocoder_sample_rate or self.sample_rate + if effective_sr != self.sample_rate and audio is not None and audio.numel() > 0: + try: + import torchaudio + + audio = torchaudio.functional.resample(audio, effective_sr, self.sample_rate) + except Exception as e: + logger.warning( + "Failed to resample audio from %d to %d: %s", + effective_sr, + self.sample_rate, + e, + ) + + audio = audio.reshape(-1) + if is_streaming and req_id is not None: + audio = self._slice_streaming_audio( + audio, + req_id=req_id, + stream_finished=stream_finished, + lookahead_mel_len=lookahead_mel_len, + crossfade_sec=crossfade_sec, + ) + + return audio + + def _slice_streaming_audio( + self, + audio: torch.Tensor, + *, + req_id: str, + stream_finished: bool, + lookahead_mel_len: int, + crossfade_sec: float, + ) -> torch.Tensor: + """Slice cumulative streaming audio using the official wav pointer. + + Official GLM-TTS runs the flow/vocoder on the cumulative token prefix + every chunk, then drops the waveform samples that were already emitted. + Non-final chunks additionally keep a lookahead plus fade tail for the + next chunk; the final full-prefix pass flushes that tail. + """ + look_back_len = max(0, int(lookahead_mel_len * self.sample_rate / self.mel_framerate)) + fade_len = max(0, int(crossfade_sec * self.sample_rate)) + overlap_len = look_back_len + fade_len + + with self._stream_cache_lock: + _rs = self._stream_state_by_req.get(req_id) + wav_pointer = _rs.wav_pointer if _rs is not None else 0 + fade_tail = _rs.fade_tail if _rs is not None else None + + current = audio[min(wav_pointer, int(audio.numel())) :].clone() + if fade_tail is not None and fade_tail.numel() > 0 and current.numel() > 0: + blend_len = min(int(fade_tail.numel()), int(current.numel())) + fade_in = torch.linspace(0.0, 1.0, blend_len, dtype=current.dtype, device=current.device) + fade_out = 1.0 - fade_in + current[:blend_len] = ( + fade_tail[:blend_len].to(device=current.device, dtype=current.dtype) * fade_out + + current[:blend_len] * fade_in + ) + + if stream_finished: + with self._stream_cache_lock: + _rs = self._stream_state_by_req.get(req_id) + if _rs is not None: + _rs.wav_pointer = 0 + _rs.fade_tail = None + return current + + if overlap_len <= 0 or current.numel() <= overlap_len: + emit = torch.zeros(0, dtype=current.dtype, device=current.device) + next_tail = current.detach().clone() + else: + emit = current[:-overlap_len] + tail_end = -look_back_len if look_back_len > 0 else None + next_tail = current[-overlap_len:tail_end].detach().clone() + + with self._stream_cache_lock: + rs = self._stream_state_by_req.setdefault(req_id, _StreamRequestState()) + rs.wav_pointer = wav_pointer + int(emit.numel()) + rs.fade_tail = next_tail + return emit + + # Maximum number of forward calls before a cache entry is considered stale. + _STALE_THRESHOLD = 64 + + def _evict_stale_stream_caches(self) -> None: + """Evict streaming cache entries for requests not seen recently. + + This is a self-contained fallback: if ``stream_finished`` is never + sent (e.g. client disconnect), the entry would otherwise leak forever. + Called at the top of every ``forward`` so no changes to the shared + model runner are needed. + """ + self._stream_forward_count += 1 + # Only check every 16 forward calls to amortize the lock overhead. + if self._stream_forward_count % 16 != 0: + return + with self._stream_cache_lock: + if not self._stream_last_seen: + return + stale_ids = [ + rid + for rid, last_seen in self._stream_last_seen.items() + if self._stream_forward_count - last_seen > self._STALE_THRESHOLD + ] + for rid in stale_ids: + self._stream_state_by_req.pop(rid, None) + self._stream_last_seen.pop(rid, None) + + def _do_sample( + self, + speech_tokens: torch.Tensor, + mel_cond: torch.Tensor, + condition: torch.Tensor, + padding_mask: torch.Tensor, + spkr_embedding: torch.Tensor | None, + n_timesteps: int, + last_step_cache: dict[int | str, Any] | None = None, + block_pattern: list[int] | None = None, + cache_len: int | None = None, + text_lens: torch.Tensor | None = None, + ) -> tuple[torch.Tensor, dict[int | str, Any]]: + """Euler ODE sampling for flow matching.""" + # CUDA graph path: the captured graph does NOT pass is_causal / + # block_pattern to the DiT, so it always uses bidirectional attention. + # That is correct for non-streaming (full-sequence) inference but + # wrong for streaming where block-causal attention is required. + # Skip the graph when block_pattern is set so we fall through to the + # eager path that correctly forwards these arguments. + use_cudagraph = block_pattern is None + graph_result = ( + self._try_cudagraph_sample( + speech_tokens=speech_tokens, + mel_cond=mel_cond, + condition=condition, + padding_mask=padding_mask, + spkr_embedding=spkr_embedding, + n_timesteps=n_timesteps, + last_step_cache=last_step_cache, + cache_len=cache_len, + text_lens=text_lens, + ) + if use_cudagraph + else None + ) + if graph_result is not None: + return graph_result + + device = speech_tokens.device + # Always accumulate the Euler ODE state in float32 regardless of the + # DiT model dtype. Half-precision rounding errors across 10 steps (×2 + # CFG passes each) manifest as audible hiss on H20/H100. + x = torch.randn(mel_cond.shape, device=device, dtype=torch.float32) + # Cast conditioning tensors to float32 for the ODE loop. + condition_f32 = condition.to(torch.float32) + spkr_embedding_f32 = spkr_embedding.to(torch.float32) if spkr_embedding is not None else None + # Pre-cast invariant conditioning tensors to DiT dtype once (avoids + # repeated .to() calls inside the inner Euler loop). + condition_dit = condition_f32.to(mel_cond.dtype) + spkr_embedding_dit = spkr_embedding_f32.to(mel_cond.dtype) if spkr_embedding_f32 is not None else None + current_step_cache: dict[int | str, Any] = {} + effective_cache_len = min(int(cache_len or 0), int(mel_cond.shape[1])) + should_cache_steps = effective_cache_len > 0 + t_span = torch.linspace(0, 1, n_timesteps + 1, device=device, dtype=torch.float32) + if self.t_scheduler == "cosine": + t_span = 1 - torch.cos(t_span * 0.5 * torch.pi) + + for step in range(n_timesteps): + cache_step = step + 1 + if last_step_cache is not None and cache_step in last_step_cache: + x_cache = last_step_cache[cache_step].get("x") + if isinstance(x_cache, torch.Tensor): + override_len = int(last_step_cache.get("override_len", x_cache.shape[1])) + safe_len = min(int(x.shape[1]), int(x_cache.shape[1]), max(0, override_len)) + if safe_len > 0: + x[:, :safe_len, :] = x_cache[:, :safe_len, :].to(device=device, dtype=torch.float32) + if should_cache_steps: + current_step_cache[cache_step] = {"x": x[:, :effective_cache_len, :].detach().clone()} + + t_current = t_span[step] + dt = t_span[step + 1] - t_current + + use_causal = block_pattern is not None + x_dit = x.to(mel_cond.dtype) + + if self.inference_cfg_rate > 0: + # Batch conditional + unconditional into a single forward + middle_b = x_dit.expand(2, -1, -1) + condition_b = torch.cat([condition_dit, torch.zeros_like(condition_dit)], dim=0) + if self.speech_token_cfg: + text_b = torch.cat([speech_tokens, torch.zeros_like(speech_tokens)], dim=0) + else: + text_b = speech_tokens.expand(2, -1) + time_b = t_current.unsqueeze(0).expand(2) + padding_b = padding_mask.expand(2, -1) + if spkr_embedding_dit is not None and self.spkr_emb_adaln: + spkr_b = torch.cat([spkr_embedding_dit, torch.zeros_like(spkr_embedding_dit)], dim=0) + else: + spkr_b = None + text_lens_b = text_lens.expand(2) if text_lens is not None else None + pred_b = self.dit( + middle_point=middle_b, + condition=condition_b, + text=text_b, + time_step=time_b, + padding_mask=padding_b, + spkr_emb=spkr_b, + is_causal=use_causal, + block_pattern=block_pattern, + text_lens=text_lens_b, + ).to(torch.float32) + dphi_dt, cfg_dphi_dt = pred_b.chunk(2, dim=0) + dphi_dt = (1.0 + self.inference_cfg_rate) * dphi_dt - self.inference_cfg_rate * cfg_dphi_dt + else: + dphi_dt = self.dit( + middle_point=x_dit, + condition=condition_dit, + text=speech_tokens, + time_step=t_current.unsqueeze(0), + padding_mask=padding_mask, + spkr_emb=spkr_embedding_dit if spkr_embedding_dit is not None and self.spkr_emb_adaln else None, + is_causal=use_causal, + block_pattern=block_pattern, + text_lens=text_lens, + ).to(torch.float32) + + x = x + dt * dphi_dt + + return x.to(mel_cond.dtype), current_step_cache + + def _try_cudagraph_sample( + self, + *, + speech_tokens: torch.Tensor, + mel_cond: torch.Tensor, + condition: torch.Tensor, + padding_mask: torch.Tensor, + spkr_embedding: torch.Tensor | None, + n_timesteps: int, + last_step_cache: dict[int | str, Any] | None, + cache_len: int | None = None, + text_lens: torch.Tensor | None = None, + ) -> tuple[torch.Tensor, dict[int | str, Any]] | None: + if not self._use_dit_cuda_graphs or mel_cond.device.type != "cuda": + return None + if self._dit_cudagraph is None: + condition_dim = self.mel_dim if self.remove_spkr_concat_condition else self.mel_dim + self.spk_embed_dim + self._dit_cudagraph = CUDAGraphGLMTTSDiTWrapper( + self.dit, + mel_dim=self.mel_dim, + condition_dim=condition_dim, + input_frame_rate=self.input_frame_rate, + mel_framerate=self.mel_framerate, + inference_cfg_rate=self.inference_cfg_rate, + speech_token_cfg=self.speech_token_cfg, + spkr_emb_adaln=self.spkr_emb_adaln, + t_scheduler=self.t_scheduler, + enabled=True, + ) + try: + self._dit_cudagraph.warmup( + device=mel_cond.device, + dtype=mel_cond.dtype, + n_timesteps=n_timesteps, + codec_chunk_frames=self._codec_chunk_frames, + codec_left_context_frames=self._codec_left_context_frames, + ) + except Exception: + logger.warning("Disabling GLM-TTS DiT CUDA graphs after warmup failure", exc_info=True) + self._dit_cudagraph.enabled = False + return None + + try: + return self._dit_cudagraph.sample( + speech_tokens=speech_tokens, + mel_cond=mel_cond, + condition=condition, + padding_mask=padding_mask, + spkr_embedding=spkr_embedding, + n_timesteps=n_timesteps, + last_step_cache=last_step_cache, + cache_len=cache_len, + text_lens=text_lens, + ) + except Exception: + logger.warning("GLM-TTS DiT CUDA graph replay failed; falling back to eager", exc_info=True) + return None + + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: + """Load DiT + vocoder weights from {model_dir}/flow/flow.pt.""" + for _ in weights: + pass + + weight_dict: dict[str, torch.Tensor] = {} + if not os.path.exists(self._flow_weights_path): + raise FileNotFoundError(f"GLM-TTS flow weights not found at {self._flow_weights_path}") + + logger.info("Loading GLM-TTS flow weights from %s", self._flow_weights_path) + checkpoint = torch.load(self._flow_weights_path, map_location="cpu", weights_only=True) + if "model" in checkpoint: + weight_dict = checkpoint["model"] + else: + weight_dict = checkpoint + + dit_state = { + (name.removeprefix("estimator.") if name.startswith("estimator.") else name.removeprefix("dit.")): tensor + for name, tensor in weight_dict.items() + if (name.startswith("estimator.") or name.startswith("dit.")) + and "inv_freq" not in name + and "rotary_embed" not in name + } + if not dit_state: + raise RuntimeError( + "GLM-TTS flow checkpoint did not contain any DiT weights under 'estimator.*' or 'dit.*'." + ) + missing, unexpected = self.dit.load_state_dict(dit_state, strict=False) + missing = [k for k in missing if "inv_freq" not in k] + if missing: + logger.warning("Missing DiT weights: %s", missing[:10]) + if unexpected: + logger.warning("Unexpected DiT weights: %s", unexpected[:10]) + + loaded_spk: set[str] = set() + if hasattr(self, "spk_embed_affine_layer"): + for suffix in ("weight", "bias"): + key = f"spk_embed_affine_layer.{suffix}" + if key in weight_dict: + param = getattr(self.spk_embed_affine_layer, suffix) + param.data.copy_(weight_dict[key].to(device=param.device, dtype=param.dtype)) + loaded_spk.add(key) + self.spk_embed_affine_layer.eval() + self.dit.eval() + self._ensure_vocoder_loaded() + + # Return both prefixed and unprefixed param names for nested module compat + loaded_params: set[str] = set() + for k in dit_state: + loaded_params.update((f"dit.{k}", f"_dit_gen.dit.{k}")) + for k in loaded_spk: + loaded_params.update((k, f"_dit_gen.{k}")) + logger.info("Loaded GLMTTSDiTForGeneration: %d params", len(loaded_params)) + return loaded_params + + +class CUDAGraphGLMTTSDiTWrapper: + """Capture GLM-TTS DiT Euler sampling for fixed mel-frame buckets. + + The wrapper owns static buffers for a single-request DiT stage. Dynamic + requests are right-padded to the nearest captured mel-frame bucket and + trimmed back to the actual mel length after graph replay. + """ + + def __init__( + self, + dit: torch.nn.Module, + *, + mel_dim: int, + condition_dim: int, + input_frame_rate: float, + mel_framerate: float, + inference_cfg_rate: float, + speech_token_cfg: bool, + spkr_emb_adaln: bool, + t_scheduler: str, + enabled: bool = True, + capture_sizes: list[int] | None = None, + ) -> None: + self.dit = dit + self.mel_dim = int(mel_dim) + self.condition_dim = int(condition_dim) + self.input_frame_rate = float(input_frame_rate) + self.mel_framerate = float(mel_framerate) + self.inference_cfg_rate = float(inference_cfg_rate) + self.speech_token_cfg = bool(speech_token_cfg) + self.spkr_emb_adaln = bool(spkr_emb_adaln) + self.t_scheduler = str(t_scheduler) + self.enabled = bool(enabled) + self.capture_sizes = sorted(set(capture_sizes or [])) + + self.graphs: dict[int, CUDAGraph] = {} + self.static_x: dict[int, torch.Tensor] = {} + self.static_condition: dict[int, torch.Tensor] = {} + self.static_text: dict[int, torch.Tensor] = {} + self.static_text_lens: dict[int, torch.Tensor] = {} + self.static_padding_mask: dict[int, torch.Tensor] = {} + self.static_spkr_emb: dict[int, torch.Tensor] = {} + self.static_output: dict[int, torch.Tensor] = {} + self.static_step_cache: dict[int, list[torch.Tensor]] = {} + + self._warmed_up = False + self._n_timesteps: int | None = None + + @staticmethod + def compute_capture_sizes( + *, + codec_chunk_frames: int = 25, + codec_left_context_frames: int = 25, + input_frame_rate: float = 25.0, + mel_framerate: float = 50.0, + max_bucket: int = 1024, + ) -> list[int]: + sizes: set[int] = set() + + def token_to_mel(tokens: int) -> int: + return max(1, int(math.ceil(float(tokens) / input_frame_rate * mel_framerate))) + + if codec_chunk_frames > 0: + sizes.add(token_to_mel(codec_chunk_frames)) + if codec_left_context_frames > 0: + sizes.add(token_to_mel(codec_chunk_frames + codec_left_context_frames)) + + for size in (32, 64, 128, 256, 512, 1024): + if size <= max_bucket: + sizes.add(size) + return sorted(sizes) + + def _get_padded_size(self, actual_feat_len: int) -> int | None: + for size in self.capture_sizes: + if actual_feat_len <= size: + return size + return None + + def _token_bucket_len(self, bucket_feat_len: int) -> int: + return max(1, int(math.ceil(float(bucket_feat_len) * self.input_frame_rate / self.mel_framerate))) + + def warmup( + self, + *, + device: torch.device, + dtype: torch.dtype, + n_timesteps: int, + codec_chunk_frames: int = 25, + codec_left_context_frames: int = 25, + ) -> None: + if not self.enabled or self._warmed_up or device.type != "cuda" or torch.cuda.is_current_stream_capturing(): + return + self._n_timesteps = int(n_timesteps) + if not self.capture_sizes: + self.capture_sizes = self.compute_capture_sizes( + codec_chunk_frames=codec_chunk_frames, + codec_left_context_frames=codec_left_context_frames, + input_frame_rate=self.input_frame_rate, + mel_framerate=self.mel_framerate, + ) + + self.dit.eval() + logger.info("Starting GLM-TTS DiT CUDA graph warmup for sizes: %s", self.capture_sizes) + for size in self.capture_sizes: + try: + self._allocate_static_buffers(size, device, dtype) + self._capture(size, device) + logger.info("Captured GLM-TTS DiT CUDA graph for mel frames=%d", size) + except Exception: + logger.warning("Failed to capture GLM-TTS DiT CUDA graph for size=%d", size, exc_info=True) + self._warmed_up = True + + def _allocate_static_buffers(self, size: int, device: torch.device, dtype: torch.dtype) -> None: + token_len = self._token_bucket_len(size) + self.static_x[size] = torch.zeros(1, size, self.mel_dim, device=device, dtype=dtype) + self.static_condition[size] = torch.zeros(1, size, self.condition_dim, device=device, dtype=dtype) + self.static_text[size] = torch.zeros(1, token_len, device=device, dtype=torch.long) + self.static_text_lens[size] = torch.full((1,), token_len, device=device, dtype=torch.long) + self.static_padding_mask[size] = torch.zeros(1, size, device=device, dtype=torch.bool) + self.static_spkr_emb[size] = torch.zeros(1, 192, device=device, dtype=dtype) + self.static_output[size] = torch.zeros(1, size, self.mel_dim, device=device, dtype=dtype) + self.static_step_cache[size] = [ + torch.zeros(1, size, self.mel_dim, device=device, dtype=dtype) for _ in range(int(self._n_timesteps or 0)) + ] + + def _time_schedule(self, device: torch.device, dtype: torch.dtype) -> torch.Tensor: + n_timesteps = int(self._n_timesteps or 0) + t_span = torch.linspace(0, 1, n_timesteps + 1, device=device, dtype=dtype) + if self.t_scheduler == "cosine": + t_span = 1 - torch.cos(t_span * 0.5 * torch.pi) + return t_span + + def _run_static(self, size: int) -> torch.Tensor: + x = self.static_x[size] + condition = self.static_condition[size] + text = self.static_text[size] + text_lens = self.static_text_lens[size] + padding_mask = self.static_padding_mask[size] + spkr_emb = self.static_spkr_emb[size] if self.spkr_emb_adaln else None + t_span = self._time_schedule(x.device, x.dtype) + + for step in range(int(self._n_timesteps or 0)): + self.static_step_cache[size][step].copy_(x) + t_current = t_span[step] + dt = t_span[step + 1] - t_current + if self.inference_cfg_rate > 0: + middle_b = x.expand(2, -1, -1) + condition_b = torch.cat([condition, torch.zeros_like(condition)], dim=0) + if self.speech_token_cfg: + text_b = torch.cat([text, torch.zeros_like(text)], dim=0) + else: + text_b = text.expand(2, -1) + time_b = t_current.unsqueeze(0).expand(2) + padding_b = padding_mask.expand(2, -1) + text_lens_b = text_lens.expand(2) + if spkr_emb is not None: + spkr_b = torch.cat([spkr_emb, torch.zeros_like(spkr_emb)], dim=0) + else: + spkr_b = None + pred_b = self.dit( + middle_point=middle_b, + condition=condition_b, + text=text_b, + text_lens=text_lens_b, + time_step=time_b, + padding_mask=padding_b, + spkr_emb=spkr_b, + ) + dphi_dt, cfg_dphi_dt = pred_b.chunk(2, dim=0) + dphi_dt = (1.0 + self.inference_cfg_rate) * dphi_dt - self.inference_cfg_rate * cfg_dphi_dt + else: + dphi_dt = self.dit( + middle_point=x, + condition=condition, + text=text, + text_lens=text_lens, + time_step=t_current.unsqueeze(0), + padding_mask=padding_mask, + spkr_emb=spkr_emb, + ) + x = x + dt * dphi_dt + self.static_output[size].copy_(x) + return self.static_output[size] + + def _capture(self, size: int, device: torch.device) -> None: + with torch.no_grad(): + self.static_x[size].normal_() + _ = self._run_static(size) + torch.accelerator.synchronize(device) + + graph = CUDAGraph() + with torch.no_grad(): + with torch.cuda.graph(graph, pool=current_platform.get_global_graph_pool()): + self._run_static(size) + self.graphs[size] = graph + + def sample( + self, + *, + speech_tokens: torch.Tensor, + mel_cond: torch.Tensor, + condition: torch.Tensor, + padding_mask: torch.Tensor, + spkr_embedding: torch.Tensor | None, + n_timesteps: int, + last_step_cache: dict[int | str, Any] | None = None, + cache_len: int | None = None, + text_lens: torch.Tensor | None = None, + ) -> tuple[torch.Tensor, dict[int | str, Any]] | None: + if ( + not self.enabled + or not self._warmed_up + or int(n_timesteps) != int(self._n_timesteps or -1) + or speech_tokens.shape[0] != 1 + or mel_cond.shape[0] != 1 + or mel_cond.device.type != "cuda" + or torch.cuda.is_current_stream_capturing() + ): + return None + + actual_feat_len = int(mel_cond.shape[1]) + size = self._get_padded_size(actual_feat_len) + if size is None or size not in self.graphs: + return None + + token_bucket_len = int(self.static_text[size].shape[1]) + if int(speech_tokens.shape[1]) > token_bucket_len: + return None + + self.static_x[size].normal_() + self.static_condition[size].zero_() + self.static_text[size].zero_() + self.static_padding_mask[size].zero_() + self.static_spkr_emb[size].zero_() + + # Warm-start: when streaming, pre-fill the stable region of the + # initial noise from the step-1 cache of the previous chunk. + # The graph's Euler loop then evolves from this warm-started state. + # New (lookahead) region keeps random noise — no per-step overlay. + if last_step_cache is not None and 1 in last_step_cache: + cached_x = last_step_cache[1]["x"] + override_len = int(last_step_cache.get("override_len", cached_x.shape[1])) + safe = min(size, int(cached_x.shape[1]), max(0, override_len)) + if safe > 0: + self.static_x[size][:, :safe, :].copy_(cached_x[:, :safe, :].to(self.static_x[size])) + + self.static_condition[size][:, :actual_feat_len, : condition.shape[-1]] = condition + self.static_text[size][:, : speech_tokens.shape[1]] = speech_tokens + actual_text_len = int(speech_tokens.shape[1]) + if text_lens is not None: + self.static_text_lens[size].copy_(text_lens) + else: + self.static_text_lens[size].fill_(actual_text_len) + self.static_padding_mask[size][:, :actual_feat_len] = padding_mask + if spkr_embedding is not None: + self.static_spkr_emb[size].copy_(spkr_embedding.to(self.static_spkr_emb[size])) + + self.graphs[size].replay() + mel = self.static_output[size][:, :actual_feat_len, :].clone() + current_step_cache: dict[int | str, Any] = {} + cache_len = min(int(cache_len or 0), actual_feat_len) + if cache_len > 0: + for idx, cache in enumerate(self.static_step_cache[size], start=1): + current_step_cache[idx] = {"x": cache[:, :cache_len, :].detach().clone()} + return mel, current_step_cache diff --git a/vllm_omni/model_executor/models/glm_tts/pipeline.py b/vllm_omni/model_executor/models/glm_tts/pipeline.py new file mode 100644 index 000000000..f016a993e --- /dev/null +++ b/vllm_omni/model_executor/models/glm_tts/pipeline.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""GLM-TTS pipeline: Stage 0 (AR) → Stage 1 (DiT).""" + +from vllm_omni.config.stage_config import ( + PipelineConfig, + StageExecutionType, + StagePipelineConfig, +) + +_PROC = "vllm_omni.model_executor.stage_input_processors.glm_tts" + +GLM_TTS_PIPELINE = PipelineConfig( + model_type="glm_tts", + model_arch="GLMTTSForConditionalGeneration", + stages=( + StagePipelineConfig( + stage_id=0, + model_stage="glm_tts", + execution_type=StageExecutionType.LLM_AR, + input_sources=(), + owns_tokenizer=True, + engine_output_type="latent", + async_chunk_process_next_stage_input_func=(f"{_PROC}.ar_to_dit_async_chunk"), + sampling_constraints={ + # GLM-TTS uses 👂 (token string "👂", ID 59253) as + # end-of-audio marker. The ID is resolved dynamically by + # GLMTTSForConditionalGeneration.__init__ from the tokenizer and + # validated at runtime against this hardcoded value. If the + # upstream checkpoint changes the mapping, the model will log a + # warning so the constant here can be updated. + "stop_token_ids": [59253], + }, + ), + StagePipelineConfig( + stage_id=1, + model_stage="glm_tts_dit", + execution_type=StageExecutionType.LLM_GENERATION, + input_sources=(0,), + final_output=True, + final_output_type="audio", + engine_output_type="latent", + sync_process_input_func=f"{_PROC}.ar_to_dit", + ), + ), +) diff --git a/vllm_omni/model_executor/models/glm_tts/text_frontend.py b/vllm_omni/model_executor/models/glm_tts/text_frontend.py new file mode 100644 index 000000000..8cfc0e5c8 --- /dev/null +++ b/vllm_omni/model_executor/models/glm_tts/text_frontend.py @@ -0,0 +1,393 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Lightweight text frontend aligned with the official GLM-TTS preprocessing.""" + +from __future__ import annotations + +import json +import os +import re + +try: + import emoji +except ImportError: + emoji = None # type: ignore[assignment] + +try: + import inflect +except ImportError: + inflect = None # type: ignore[assignment] + +try: + from tn.chinese.normalizer import Normalizer as ZhNormalizer +except ImportError: + ZhNormalizer = None + +try: + from tn.english.normalizer import Normalizer as EnNormalizer +except ImportError: + EnNormalizer = None + + +CHINESE_CHAR_PATTERN = re.compile(r"[\u4e00-\u9fff]+") +PUNCTUATION_CHARS = r"。?!;:、.?!;:,," + + +def contains_chinese(text: str) -> bool: + return bool(CHINESE_CHAR_PATTERN.search(text)) + + +def markdown_norm(markdown_text: str) -> str: + markdown_text = re.sub(r"^(\d+)\. ", r"\1。", markdown_text) + return markdown_text.replace("\\n", "\n") + + +def multi_line_process(plain_text: str) -> str: + lines: list[str] = [] + for line in plain_text.split("\n"): + line = line.strip() + if not line: + continue + if line[-1] not in {".", "!", "?", ";", ":", ":", "。", "!", "?", ";", ","}: + line = f"{line}{'。' if contains_chinese(line) else '. '}" + lines.append(line) + return "".join(lines) if contains_chinese("".join(lines)) else " ".join(lines) + + +def remove_bracket(text: str, lang: str = "zh") -> str: + brackets_to_remove = [ + ("(", ")"), + ("(", ")"), + ("【", "】"), + ("「", "」"), + ("`", "`"), + ("《", "》"), + ("『", "』"), + ("{", "}"), + ("[", "]"), + ] + if lang != "en": + brackets_to_remove.append(("‘", "’")) + for left, right in brackets_to_remove: + text = text.replace(left, "").replace(right, "") + return text + + +def number_to_chinese(number: int | float | str) -> str: + units = ["", "十", "百", "千", "万", "十万", "百万", "千万", "亿", "十亿", "百亿", "千亿"] + nums = "零一二三四五六七八九" + if isinstance(number, (float, str)): + str_num = str(float(number)) + if "." in str_num: + int_part, decimal_part = str_num.split(".") + if decimal_part.strip("0") == "": + return number_to_chinese(int(int_part)) + chinese_int = number_to_chinese(int(int_part)) if int(int_part) != 0 else "零" + chinese_decimal = "".join(nums[int(d)] for d in decimal_part) + return f"{chinese_int}点{chinese_decimal}" + + number = int(number) + if number == 0: + return "零" + if number < 0: + return "负" + number_to_chinese(abs(number)) + + result: list[str] = [] + unit_position = 0 + zero_flag = False + last_unit = "" + while number > 0: + num = number % 10 + current_unit = units[unit_position] if unit_position < len(units) else "" + if num == 0: + if not zero_flag and result and last_unit not in ["万", "亿"]: + result.append(nums[num]) + zero_flag = True + else: + if current_unit: + result.append(current_unit) + result.append(nums[num]) + zero_flag = False + last_unit = current_unit + unit_position += 1 + number //= 10 + + result.reverse() + text = "".join(result) + text = re.sub(r"^一十", "十", text) + text = re.sub(r"零+", "零", text) + text = re.sub(r"零$", "", text) + return text + + +def tn_scientific_notation(text: str) -> str: + pattern = re.compile(r"(.*?)(-?\d+(?:\.\d+)?)[*x×](\d+(?:\.\d+)?)\^(-?\d+(?:\.\d+)?)(.*?)$") + match = pattern.match(text) + if not match: + return text + prefix, base, multiplier, exponent, suffix = match.groups() + description = f"{number_to_chinese(base)}乘{number_to_chinese(multiplier)}的{number_to_chinese(exponent)}次方" + return f"{prefix}{description}{suffix}" + + +def replace_asterisk_with_multiply(text: str, lang: str) -> str: + if lang == "en": + replace_name = "multiply" + replace_number = "0-9" + else: + replace_name = "乘" + replace_number = "一二三四五六七八九十百千万亿" + + rule0 = rf"(?<=[{replace_number}a-zA-Z])\s*\*\s*(?=[{replace_number}a-zA-Z])" + rule1 = rf"(?<=[{replace_number}a-zA-Z]\))\s*\*\s*(?=[{replace_number}a-zA-Z])" + rule2 = rf"(?<=[{replace_number}a-zA-Z])\s*\*\s*(?=\([{replace_number}a-zA-Z])" + rule3 = rf"(?<=[{replace_number}a-zA-Z]\))\s*\*\s*(?=\([{replace_number}a-zA-Z])" + return re.sub(f"{rule0}|{rule1}|{rule2}|{rule3}", replace_name, text).replace("*", "") + + +def special_replace(text: str) -> str: + text = text.replace("\\", "") + text = f" {text} " + text = re.sub(r"(?<=\W)额(?=\W)", "呃", text) + text = re.sub(r"[~~]+", "。", text) + text = replace_asterisk_with_multiply(text, "zh") + return text.strip() + + +def spell_out_number(text: str, inflect_parser: object | None) -> str: + if inflect_parser is None: + return text + new_text: list[str] = [] + start = None + for idx, char in enumerate(text): + if not char.isdigit(): + if start is not None: + new_text.append(inflect_parser.number_to_words(text[start:idx])) + start = None + new_text.append(char) + elif start is None: + start = idx + if start is not None and start < len(text): + new_text.append(inflect_parser.number_to_words(text[start:])) + return "".join(new_text) + + +def replace_space(text: str) -> str: + alphanumeric_pattern = r"[a-zA-Z0-9]" + punctuation_pattern = r"[.,!?;:]" + text = re.sub(r" +", " ", text) + result = "" + idx = 0 + while idx < len(text): + current_char = text[idx] + if current_char != " ": + result += current_char + idx += 1 + continue + prev_char = text[idx - 1] if idx > 0 else "" + next_char = text[idx + 1] if idx + 1 < len(text) else "" + if re.match(alphanumeric_pattern, prev_char) and re.match(alphanumeric_pattern, next_char): + result += " " + elif re.match(punctuation_pattern, prev_char) and re.match(alphanumeric_pattern, next_char): + result += " " + idx += 1 + return result + + +def normalize_punctuation(text: str, punctuation_chars: str) -> str: + text = replace_space(text) + text = re.sub(rf"([{punctuation_chars}])\1+", r"\1", text) + text = re.sub(rf"([{punctuation_chars}])(?=[{punctuation_chars}])", "", text) + text = text.replace("#", "") + text = text.replace("!", "。") + text = text.replace("!", ".") + return text + + +def ensure_proper_ending(text: str) -> str: + if not text: + return text + if text[-1] in "??": + return text + if text[-1] in PUNCTUATION_CHARS: + return f"{text[:-1]}{'。' if contains_chinese(text) else '.'}" + return f"{text}{'。' if contains_chinese(text) else '.'}" + + +class GLMTTSTextFrontend: + """Subset of the official GLM-TTS text frontend used by the adapter.""" + + def __init__(self) -> None: + self.inflect_parser = inflect.engine() if inflect is not None else None + self.zh_tn_model = ( + ZhNormalizer( + remove_erhua=False, + full_to_half=True, + remove_interjections=False, + overwrite_cache=True, + ) + if ZhNormalizer is not None + else None + ) + self.en_tn_model = EnNormalizer() if EnNormalizer is not None else None + self._custom_replacements = self._load_custom_replacements() + + @staticmethod + def _load_custom_replacements() -> list[tuple[str, str]]: + custom_replace_path = os.path.join(os.path.dirname(__file__), "configs", "custom_replace.jsonl") + if not os.path.exists(custom_replace_path): + return [] + replacements: list[tuple[str, str]] = [] + with open(custom_replace_path, encoding="utf-8") as f: + for line in f: + if not line.strip(): + continue + item = json.loads(line) + replacements.append((item["origin"], item["new"])) + return replacements + + def pre_replace(self, sentence: str) -> str: + sentence = tn_scientific_notation(sentence) + sentence = re.sub(r"(?<=\d)\s*-\s*(?=\d)", "减", sentence) + sentence = sentence.replace("-", "") + sentence = re.sub(r"咯([" + re.escape(PUNCTUATION_CHARS) + r"])", r"喽\1", sentence) + for origin, new in self._custom_replacements: + sentence = sentence.replace(origin, new) + return sentence + + def post_replace(self, sentence: str) -> str: + sentence = remove_bracket(sentence) + sentence = sentence.replace(" - ", ",") + sentence = sentence.replace("——", ",") + sentence = re.sub(r"[,::;;、]+", ",", sentence) + sentence = re.sub(r"[.…]+", "。", sentence) + sentence = re.sub(r"[_·]+", "", sentence) + sentence = re.sub(r"""['"‘’“”|]+""", "", sentence) + + symbol_map = { + "†": ",", + "²": "平方", + "³": "立方", + "/": "每", + "~": "到", + "~": "到", + "①": "一", + "②": "二", + "③": "三", + "④": "四", + "⑤": "五", + "⑥": "六", + "⑦": "七", + "⑧": "八", + "⑨": "九", + "⑩": "十", + "α": "阿尔法", + "β": "贝塔", + "γ": "伽玛", + "Γ": "伽玛", + "δ": "德尔塔", + "Δ": "德尔塔", + "△": "德尔塔", + "ε": "艾普西龙", + "ζ": "捷塔", + "η": "依塔", + "θ": "西塔", + "Θ": "西塔", + "ι": "艾欧塔", + "κ": "喀帕", + "λ": "拉姆达", + "Λ": "拉姆达", + "μ": "缪", + "ν": "拗", + "ξ": "克西", + "Ξ": "克西", + "ο": "欧米克伦", + "π": "派", + "Π": "派", + "ρ": "肉", + "ς": "西格玛", + "Σ": "西格玛", + "σ": "西格玛", + "τ": "套", + "υ": "宇普西龙", + "φ": "服艾", + "Φ": "服艾", + "χ": "器", + "ψ": "普赛", + "Ψ": "普赛", + "ω": "欧米伽", + "Ω": "欧米伽", + "□": "方框", + ">": "大于", + "<": "小于", + "∈": "属于", + "∉": "不属于", + "∪": "并", + "∩": "交", + "⊥": "垂直", + "∥": "平行", + "≠": "不等于", + "∵": "因为", + "∴": "所以", + "∅": "空集", + "⊂": "真包含于", + "⊃": "包含", + "⊆": "包含于", + "⊇": "真包含", + "⊄": "不属于", + "⊅": "非超集", + "⊈": "不属于", + "⊉": "非超集", + } + for old, new in symbol_map.items(): + sentence = sentence.replace(old, new) + return sentence + + def text_normalize(self, text: str | None) -> str | None: + if text is None: + return None + text = self._preprocess_text(text) + if contains_chinese(text): + text = self._normalize_chinese_text(text).lower() + else: + text = self._normalize_english_text(text) + text = normalize_punctuation(text, PUNCTUATION_CHARS) + return ensure_proper_ending(text) + + def _preprocess_text(self, text: str) -> str: + text = markdown_norm(text) + text = multi_line_process(text) + if emoji is not None: + text = emoji.replace_emoji(text, replace="") + return re.sub(r"(?<=[a-zA-Z])-(?=[a-zA-Z])", " ", text) + + def _normalize_chinese_text(self, text: str) -> str: + text = self.pre_replace(text) + if self.zh_tn_model is not None: + text = self.zh_tn_model.normalize(text) + text = special_replace(text) + return self.post_replace(text).strip() + + def _normalize_english_text(self, text: str) -> str: + text = text.replace("'", "’") + if self.en_tn_model is not None: + text = self.en_tn_model.normalize(text) + text = remove_bracket(text, "en") + text = replace_asterisk_with_multiply(text, "en") + text = text.replace("—", " ") + text = text.replace("’", "'") + text = spell_out_number(text, self.inflect_parser) + text = re.sub(r"\s+", " ", text) + keep_punctuation = r"\.,!\?'\:;" + pattern = rf"[^\w\s{keep_punctuation}]" + text = re.sub(pattern, "", text) + text = text.lower() + text = re.sub(r"\.+", ".", text) + text = re.sub(r"\,+", ",", text) + text = re.sub(r"!+", "!", text) + text = re.sub(r"\?+", "?", text) + text = re.sub(r"'+", "'", text) + text = re.sub(r":+", ":", text) + text = re.sub(r";+", ";", text) + text = re.sub(r"\s*([.,?!':;])\s*", r"\1 ", text) + return text.strip() diff --git a/vllm_omni/model_executor/models/glm_tts/vocoder.py b/vllm_omni/model_executor/models/glm_tts/vocoder.py new file mode 100644 index 000000000..77c61c0de --- /dev/null +++ b/vllm_omni/model_executor/models/glm_tts/vocoder.py @@ -0,0 +1,228 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Vocoder loading and mel-to-audio conversion for GLM-TTS. + +Supports: + - HiFT (24kHz) — reuses CosyVoice3's vendored HiFTGenerator + - Vocos2D JIT (32kHz) from TorchScript checkpoint + +Extracted from glm_tts_dit_wrapper.py to keep file sizes under 800 lines. +""" + +from __future__ import annotations + +import os +from typing import Any + +import torch +import torch.nn as nn +from vllm.logger import init_logger + +try: + from torch.nn.utils.parametrizations import weight_norm +except ImportError: + from torch.nn.utils import weight_norm + +logger = init_logger(__name__) + + +# --------------------------------------------------------------------------- +# HiFT vocoder (24kHz) — reuses CosyVoice3's vendored HiFTGenerator +# --------------------------------------------------------------------------- + + +class ConvRNNF0Predictor(nn.Module): + """Non-causal F0 predictor for GLM-TTS HiFT vocoder. + + GLM-TTS ships a non-causal HiFT checkpoint whose F0 predictor uses + standard ``nn.Conv1d`` layers (kernel_size=3, padding=1). CosyVoice3's + vendored hifigan only includes the *causal* variant + (``CausalConvRNNF0Predictor``), so we provide the non-causal version + here. The network structure and state_dict keys are identical to the + original ``cosyvoice.hifigan_cosy2.f0_predictor.ConvRNNF0Predictor``. + """ + + def __init__( + self, + num_class: int = 1, + in_channels: int = 80, + cond_channels: int = 512, + ): + super().__init__() + self.num_class = num_class + self.condnet = nn.Sequential( + weight_norm(nn.Conv1d(in_channels, cond_channels, kernel_size=3, padding=1)), + nn.ELU(), + weight_norm(nn.Conv1d(cond_channels, cond_channels, kernel_size=3, padding=1)), + nn.ELU(), + weight_norm(nn.Conv1d(cond_channels, cond_channels, kernel_size=3, padding=1)), + nn.ELU(), + weight_norm(nn.Conv1d(cond_channels, cond_channels, kernel_size=3, padding=1)), + nn.ELU(), + weight_norm(nn.Conv1d(cond_channels, cond_channels, kernel_size=3, padding=1)), + nn.ELU(), + ) + self.classifier = nn.Linear(in_features=cond_channels, out_features=self.num_class) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.condnet(x) + x = x.transpose(1, 2) + return torch.abs(self.classifier(x).squeeze(-1)) + + +class HiFTWrapper: + """Thin wrapper around CosyVoice3's vendored HiFTGenerator.""" + + def __init__(self, state_dict: dict[str, Any], device: torch.device): + self.device = device + self.sample_rate = 24000 + from vllm_omni.model_executor.models.cosyvoice3.code2wav_core.hifigan import ( + HiFTGenerator, + ) + + f0_predictor = ConvRNNF0Predictor(num_class=1, in_channels=80, cond_channels=512) + self.model = HiFTGenerator( + in_channels=80, + base_channels=512, + nb_harmonics=8, + sampling_rate=24000, + nsf_alpha=0.1, + nsf_sigma=0.003, + nsf_voiced_threshold=10, + upsample_rates=[8, 5, 3], + upsample_kernel_sizes=[16, 11, 7], + istft_params={"n_fft": 16, "hop_len": 4}, + resblock_kernel_sizes=[3, 7, 11], + resblock_dilation_sizes=[[1, 3, 5], [1, 3, 5], [1, 3, 5]], + source_resblock_kernel_sizes=[7, 7, 11], + source_resblock_dilation_sizes=[ + [1, 3, 5], + [1, 3, 5], + [1, 3, 5], + ], + lrelu_slope=0.1, + audio_limit=0.99, + f0_predictor=f0_predictor, + ).to(device) + self.model.load_state_dict(state_dict) + self.model.eval() + self.model.float() + + def __call__(self, mel: torch.Tensor) -> torch.Tensor: + mel = mel.to(self.device) + with torch.no_grad(): + audio, _ = self.model.inference(mel) + return audio + + +def load_hift(ckpt_path: str, device: torch.device) -> HiFTWrapper: + """Load HiFT vocoder from checkpoint.""" + state_dict = torch.load(ckpt_path, map_location=device, weights_only=True) + if isinstance(state_dict, dict): + for key in ("state_dict", "model", "generator"): + nested = state_dict.get(key) + if isinstance(nested, dict): + state_dict = nested + break + return HiFTWrapper(state_dict, device) + + +# --------------------------------------------------------------------------- +# Vocos2D JIT vocoder (32kHz) +# --------------------------------------------------------------------------- + +_MEL_LOGDIFF = -7.847762537473608 + + +class Vocos2DWrapper: + """Thin wrapper around Vocos2D TorchScript model.""" + + def __init__(self, ckpt_path: str, device: torch.device): + self.device = device + self.gen_model = torch.jit.load(ckpt_path, map_location=device) + self.gen_model.eval() + self._mel_logdiff = _MEL_LOGDIFF + + def __call__(self, mel: torch.Tensor) -> torch.Tensor: + if mel.size(-1) == 1: + mel = torch.cat([mel, mel], dim=-1) + xs_mel = mel.transpose(-1, -2) + self._mel_logdiff + xs = self.gen_model(xs_mel.to(self.device)) + if xs.ndim == 2: + xs = xs.unsqueeze(1) + return xs + + +def load_vocos2d_jit(ckpt_path: str, device: torch.device) -> Vocos2DWrapper: + """Load Vocos2D JIT vocoder from TorchScript checkpoint.""" + return Vocos2DWrapper(ckpt_path, device) + + +# --------------------------------------------------------------------------- +# Unified loader +# --------------------------------------------------------------------------- + + +def load_vocoder( + model_root: str, + device: torch.device, + sample_rate: int = 24000, +) -> tuple[Any | None, int]: + """Try to load the best available vocoder. + + Returns: + (vocoder, actual_sample_rate) or (None, sample_rate) on failure. + """ + # Try HiFT first for 24kHz + if sample_rate == 24000: + hift_path = os.path.join(model_root, "hift", "hift.pt") + if os.path.isfile(hift_path): + try: + vocoder = load_hift(hift_path, device) + logger.info("Loaded HiFT vocoder from %s", hift_path) + return vocoder, 24000 + except Exception as e: + logger.warning("Failed to load HiFT: %s", e) + + # Try Vocos2D JIT + vocos_jit_path = os.path.join(model_root, "vocos2d", "generator_jit.ckpt") + if os.path.isfile(vocos_jit_path): + try: + vocoder = load_vocos2d_jit(vocos_jit_path, device) + logger.info("Loaded Vocos2D JIT vocoder (32kHz)") + return vocoder, 32000 + except Exception as e: + logger.warning("Failed to load Vocos2D JIT: %s", e) + + logger.warning("No vocoder available") + return None, sample_rate + + +def mel_to_audio( + vocoder: Any | None, + mel: torch.Tensor, +) -> torch.Tensor: + """Convert mel-spectrogram to audio waveform. + + Args: + vocoder: Vocoder instance (HiFTWrapper, Vocos2DWrapper, or None). + mel: Mel-spectrogram [B, T, mel_dim]. + + Returns: + Audio waveform [B, 1, samples]. + """ + # Transpose to [B, mel_dim, T] for vocoder. + # HiFT / Vocos2D are precision-sensitive; always feed float32. + mel = mel.to(torch.float32).transpose(1, 2) + + if vocoder is not None: + if callable(vocoder) and not hasattr(vocoder, "decode"): + audio = vocoder(mel) + else: + audio = vocoder.decode(mel) + if audio.ndim == 2: + audio = audio.unsqueeze(1) + else: + raise RuntimeError("GLM-TTS vocoder is not loaded.") + + return audio diff --git a/vllm_omni/model_executor/models/glm_tts/voice_clone.py b/vllm_omni/model_executor/models/glm_tts/voice_clone.py new file mode 100644 index 000000000..d4d32f4f6 --- /dev/null +++ b/vllm_omni/model_executor/models/glm_tts/voice_clone.py @@ -0,0 +1,299 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""GLM-TTS voice cloning frontend: speech tokenizer, speaker embedding, mel features. + +Lazy-loads external models (WhisperVQ, CampPlus ONNX) and provides extraction +helpers consumed by the GLM-TTS multimodal processor. +""" + +from __future__ import annotations + +import os + +import torch +import torchaudio.compliance.kaldi as kaldi +import torchaudio.transforms as T +from vllm.logger import init_logger + +logger = init_logger(__name__) + +_MEL_BASIS_CACHE: dict[tuple[int, int, int, int, int, str], torch.Tensor] = {} +_HANN_WINDOW_CACHE: dict[tuple[int, str], torch.Tensor] = {} + + +def _spectral_normalize_torch(magnitudes: torch.Tensor) -> torch.Tensor: + return torch.log(torch.clamp(magnitudes, min=1e-5)) + + +def _get_glm_tts_mel_basis( + *, + device: torch.device, + sample_rate: int, + n_fft: int, + n_mels: int, + f_min: int, + f_max: int, +) -> torch.Tensor: + """Compute or retrieve cached GLM-TTS mel filter bank. + + Uses torchaudio melscale_fbanks with Slaney parameters (norm="slaney", + mel_scale="slaney") to match the official GLM-TTS ``utils/audio.py`` + librosa.filters.mel output. The previous HTK / norm=None config + produced a numerically different filter bank that shifted the + prompt_feat distribution and degraded voice-clone conditioning. + """ + cache_key = (sample_rate, n_fft, n_mels, f_min, f_max, str(device)) + mel_basis = _MEL_BASIS_CACHE.get(cache_key) + if mel_basis is not None: + return mel_basis + + import torchaudio.functional as AF + + mel_basis = ( + AF.melscale_fbanks( + n_freqs=(n_fft // 2) + 1, + f_min=float(f_min), + f_max=float(f_max), + n_mels=n_mels, + sample_rate=sample_rate, + norm="slaney", + mel_scale="slaney", + ) + .transpose(0, 1) + .contiguous() + .float() + .to(device) + ) + + _MEL_BASIS_CACHE[cache_key] = mel_basis + return mel_basis + + +def _extract_glm_tts_mel_feature( + audio: torch.Tensor, + *, + sample_rate: int, + n_fft: int, + win_length: int, + hop_length: int, + n_mels: int, + f_min: int, + f_max: int, +) -> torch.Tensor: + """Extract time-major log-mel spectrogram features from audio waveform.""" + device = audio.device + mel_basis = _get_glm_tts_mel_basis( + device=device, + sample_rate=sample_rate, + n_fft=n_fft, + n_mels=n_mels, + f_min=f_min, + f_max=f_max, + ) + + hann_key = (win_length, str(device)) + window = _HANN_WINDOW_CACHE.get(hann_key) + if window is None: + window = torch.hann_window(win_length, device=device) + _HANN_WINDOW_CACHE[hann_key] = window + + if audio.ndim == 1: + audio = audio.unsqueeze(0) + + pad = int((n_fft - hop_length) / 2) + audio = torch.nn.functional.pad( + audio.unsqueeze(1), + (pad, pad), + mode="reflect", + ).squeeze(1) + + spec = torch.stft( + audio, + n_fft, + hop_length=hop_length, + win_length=win_length, + window=window, + center=False, + pad_mode="reflect", + normalized=False, + onesided=True, + return_complex=True, + ) + spec = torch.sqrt(spec.real.pow(2) + spec.imag.pow(2) + 1e-9) + spec = torch.matmul(mel_basis, spec) + spec = _spectral_normalize_torch(spec) + return spec.squeeze(0).transpose(0, 1).contiguous() + + +def load_voice_clone_frontend( + model_root: str, + model_device: torch.device, + *, + speech_tokenizer_cache: tuple | None, + campplus_cache: object | None, + campplus_path: str | None = None, +) -> tuple[tuple | None, object | None]: + """Lazy-load voice cloning frontend models. + + Args: + campplus_path: Pre-resolved path to ``campplus.onnx``. When provided + this takes priority over the default ``model_root/frontend/`` + lookup. The caller (``GLMTTSForConditionalGeneration``) resolves + the path via ``transformers.utils.hub.cached_file`` during + ``__init__`` so the file is already downloaded. + + Returns: + ``(speech_tokenizer, campplus_session)`` — guaranteed non-None + on success. Raises ``RuntimeError`` when a required component + cannot be loaded. + """ + speech_tokenizer = speech_tokenizer_cache + campplus_session = campplus_cache + + if speech_tokenizer is None: + speech_tokenizer_paths = ( + os.path.join(model_root, "speech_tokenizer"), + os.path.join(model_root, "ckpt", "speech_tokenizer"), + ) + speech_tokenizer_path = next((path for path in speech_tokenizer_paths if os.path.isdir(path)), None) + if speech_tokenizer_path is None: + raise RuntimeError( + f"GLM-TTS speech tokenizer directory not found at {speech_tokenizer_paths[0]} " + f"or {speech_tokenizer_paths[1]}. Voice cloning requires the WhisperVQ " + f"speech tokenizer from the model snapshot." + ) + try: + from safetensors.torch import load_file + from transformers import AutoFeatureExtractor, WhisperConfig + + from vllm_omni.model_executor.models.common.whisper_vq import ( + WhisperVQEncoder, + ) + + _config = WhisperConfig.from_pretrained(speech_tokenizer_path) + _config.quantize_encoder_only = True + _model = WhisperVQEncoder(_config) + _sd = load_file(os.path.join(speech_tokenizer_path, "model.safetensors")) + _prefix, _cleaned = "model.encoder.", {} + for k, v in _sd.items(): + _cleaned[k[len(_prefix) :] if k.startswith(_prefix) else k] = v + _model.load_state_dict(_cleaned, strict=False) + _model = _model.to(model_device).eval() + _fe = AutoFeatureExtractor.from_pretrained(speech_tokenizer_path) + speech_tokenizer = (_model, _fe) + logger.info("Loaded GLM-TTS speech tokenizer from %s", speech_tokenizer_path) + except Exception as exc: + raise RuntimeError(f"Failed to load GLM-TTS speech tokenizer from {speech_tokenizer_path}") from exc + + if campplus_session is None: + cp = campplus_path or os.path.join(model_root, "frontend", "campplus.onnx") + if not os.path.isfile(cp): + raise RuntimeError( + f"GLM-TTS campplus.onnx not found at {cp}. " + f"Voice cloning requires the CampPlus speaker embedding model." + ) + try: + import onnxruntime + + cuda_avail = "CUDAExecutionProvider" in onnxruntime.get_available_providers() + cuda_ok = model_device.type == "cuda" and cuda_avail + providers = (["CUDAExecutionProvider"] if cuda_ok else []) + ["CPUExecutionProvider"] + campplus_session = onnxruntime.InferenceSession(cp, providers=providers) + logger.info("Loaded GLM-TTS campplus ONNX from %s", cp) + except Exception as exc: + raise RuntimeError(f"Failed to load GLM-TTS campplus ONNX from {cp}") from exc + + return speech_tokenizer, campplus_session + + +def extract_prompt_speech_token( + ref_audio_wav: torch.Tensor, + ref_audio_sr: int, + speech_tokenizer: tuple, +) -> list[int] | None: + """Extract prompt speech tokens from reference audio using WhisperVQ.""" + + model, feature_extractor = speech_tokenizer + device = model.device + + audio = ref_audio_wav.float().to(device) + if ref_audio_sr != 16000: + resampler = T.Resample(orig_freq=ref_audio_sr, new_freq=16000).to(device) + audio = resampler(audio) + + if audio.ndim > 1: + audio = audio[0] + + audio_np = audio.cpu().numpy() + pooling_kernel_size = getattr(model.config, "pooling_kernel_size", 1) + stride = model.conv1.stride[0] * model.conv2.stride[0] * pooling_kernel_size * feature_extractor.hop_length + + all_tokens: list[int] = [] + time_step = 0 + while time_step * 16000 < audio_np.shape[0]: + segment = audio_np[time_step * 16000 : (time_step + 30) * 16000] + features = feature_extractor( + [segment], + sampling_rate=16000, + return_attention_mask=True, + return_tensors="pt", + padding="longest", + pad_to_multiple_of=stride, + ).to(device) + with torch.no_grad(): + outputs = model(**features) + tokens = outputs.quantized_token_ids + attn = features.attention_mask[:, :: model.conv1.stride[0] * model.conv2.stride[0]][:, ::pooling_kernel_size] + all_tokens.extend(tokens[0][attn[0].bool()].tolist()) + time_step += 30 + + return all_tokens if all_tokens else None + + +def extract_spk_embedding( + ref_audio_wav: torch.Tensor, + ref_audio_sr: int, + campplus_session: object, +) -> list[float] | None: + """Extract speaker embedding from reference audio using CampPlus ONNX.""" + + audio = ref_audio_wav.float().cpu() + if ref_audio_sr != 16000: + resampler = T.Resample(orig_freq=ref_audio_sr, new_freq=16000) + audio = resampler(audio) + if audio.ndim > 1: + audio = audio[0] + + feat = kaldi.fbank(audio.unsqueeze(0), num_mel_bins=80, dither=0, sample_frequency=16000) + feat = feat - feat.mean(dim=0, keepdim=True) + + input_name = campplus_session.get_inputs()[0].name + embedding = campplus_session.run(None, {input_name: feat.unsqueeze(dim=0).cpu().numpy()})[0].flatten().tolist() + return embedding + + +def extract_prompt_feat( + ref_audio_wav: torch.Tensor, + ref_audio_sr: int, + model_device: torch.device, +) -> torch.Tensor | None: + """Extract mel features from reference audio for DiT conditioning.""" + audio = ref_audio_wav.float().to(model_device) + if ref_audio_sr != 24000: + resampler = T.Resample(orig_freq=ref_audio_sr, new_freq=24000).to(model_device) + audio = resampler(audio) + if audio.ndim > 1: + audio = audio[0] + + with torch.no_grad(): + feat = _extract_glm_tts_mel_feature( + audio, + sample_rate=24000, + n_fft=1920, + win_length=1920, + hop_length=480, + n_mels=80, + f_min=0, + f_max=8000, + ) + return feat # [T_mel, mel_dim] diff --git a/vllm_omni/model_executor/models/registry.py b/vllm_omni/model_executor/models/registry.py index 4f29c0ae3..4c49afc36 100644 --- a/vllm_omni/model_executor/models/registry.py +++ b/vllm_omni/model_executor/models/registry.py @@ -129,6 +129,12 @@ _OMNI_MODELS = { "glm_image_ar", "GlmImageForConditionalGeneration", ), + ## glm_tts + "GLMTTSForConditionalGeneration": ( + "glm_tts", + "glm_tts", + "GLMTTSForConditionalGeneration", + ), "OmniBagelForConditionalGeneration": ( "bagel", "bagel", diff --git a/vllm_omni/model_executor/stage_input_processors/glm_tts.py b/vllm_omni/model_executor/stage_input_processors/glm_tts.py new file mode 100644 index 000000000..caafbf86c --- /dev/null +++ b/vllm_omni/model_executor/stage_input_processors/glm_tts.py @@ -0,0 +1,362 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Stage input processor for GLM-TTS: AR → DiT Pipeline. + +Supports both sync (non-streaming) and async_chunk (streaming) modes. +Adapted for LLM_GENERATION execution type on stage 1 (DiT). +""" + +from typing import Any + +import torch +from vllm.logger import init_logger + +from vllm_omni.data_entry_keys import ( + CodesStruct, + EmbeddingsStruct, + MetaStruct, + OmniPayloadStruct, +) +from vllm_omni.engine.serialization import deserialize_additional_information + +logger = init_logger(__name__) + +_GLM_TTS_PRIVATE_KEY = "glm_tts" + + +def _copy_voice_clone_payload( + src: dict[str, Any], + dst: dict[str, Any], + *, + to_cpu: bool = False, + skip_existing: bool = False, +) -> None: + if not skip_existing or "prompt_speech_token" not in dst: + prompt = src.get("prompt_speech_token") + if prompt is None: + prompt = src.get("prompt_token") + if prompt is not None: + if to_cpu: + prompt = _to_cpu_tensor(prompt) + if prompt is not None: + dst["prompt_speech_token"] = prompt + + for key in ("prompt_feat", "embedding"): + if skip_existing and key in dst: + continue + val = src.get(key) + if val is not None: + if to_cpu: + val = _to_cpu_tensor(val) + if val is not None: + dst[key] = val + + +def _build_voice_clone_embed_struct(payload: dict[str, Any]) -> EmbeddingsStruct | None: + prompt_token = payload.get("prompt_speech_token") + if prompt_token is None: + prompt_token = payload.get("prompt_token") + prompt_feat = payload.get("prompt_feat") + embedding = payload.get("embedding") + if prompt_token is None and prompt_feat is None and embedding is None: + return None + return EmbeddingsStruct( + speech_token=prompt_token, + speech_feat=prompt_feat, + embedding=embedding, + ) + + +# --------------------------------------------------------------------------- +# Sync processor: collect all speech tokens from AR, pass to DiT +# --------------------------------------------------------------------------- + + +def ar_to_dit( + source_outputs: list[Any], + prompt: Any = None, + _requires_multimodal_data: bool = False, +) -> list[Any]: + """Non-streaming: collect speech tokens from AR, pass to DiT. + + Propagates voice cloning data (prompt_token, prompt_feat, embedding) + from the AR model's multimodal_output to the DiT stage using the same + nested ``embed`` dict format as the async_chunk path. This ensures + ``nested_get(info, "embed", ...)`` in the DiT wrapper reads all three + fields without relying on backward-compat flat-key fallbacks. + """ + from vllm_omni.inputs.data import OmniTokensPrompt + + dit_inputs: list[OmniTokensPrompt] = [] + + for output in source_outputs: + mm = output.outputs[0].multimodal_output + speech_tokens = mm.get("speech_tokens") + + # Extract valid token list (filter -1 placeholder markers from prefill) + if isinstance(speech_tokens, torch.Tensor): + valid = speech_tokens.to(torch.long).reshape(-1) + token_list = valid[valid >= 0].cpu().tolist() + elif speech_tokens is not None: + token_list = [t for t in speech_tokens if t >= 0] + else: + token_list = [] + + if not token_list: + logger.warning("No valid speech tokens for request %s", output.request_id) + + additional_info: dict[str, Any] = {"speech_tokens": token_list} + + # Build nested embed dict (unified with async_chunk path). + # _build_voice_clone_embed_struct reads prompt_speech_token/prompt_feat/ + # embedding from *mm* and returns an EmbeddingsStruct; we convert to a + # plain dict so flatten_payload/unflatten_payload round-trips correctly + # (key "embed" is in _NESTED_KEYS). + embed = _build_voice_clone_embed_struct(mm) + if embed is not None: + additional_info["embed"] = { + "speech_token": embed.speech_token, + "speech_feat": embed.speech_feat, + "embedding": embed.embedding, + } + + # Populate meta dict so DiT wrapper uses block-causal attention. + # Without meta, forward() sets uses_streaming=False → bidirectional + # attention → garbage audio. The full token sequence is treated as + # a single "chunk" with stream_finished=True. + n_tokens = len(token_list) if token_list else 1 + additional_info["meta"] = { + "req_id": [output.request_id], + "left_context_size": 0, + "stream_finished": True, + } + additional_info["kv_metadata"] = { + _GLM_TTS_PRIVATE_KEY: { + "chunk_sizes_history": [n_tokens], + "block_pattern": [n_tokens], + } + } + + dit_inputs.append( + OmniTokensPrompt( + # LLM_GENERATION scheduler needs at least 1 token + prompt_token_ids=token_list or [0], + additional_information=additional_info, + ) + ) + + return dit_inputs + + +# --------------------------------------------------------------------------- +# Helper: extract last speech token from AR model pooling output +# --------------------------------------------------------------------------- + + +def _extract_last_speech_token(pooling_output: dict[str, Any]) -> int | None: + """Extract the last valid speech token from AR model output. + + GLM-TTS AR produces one speech token per decode step. + Returns the token ID (relative to ATS, i.e. 0-based), or None. + """ + speech_tokens = pooling_output.get("speech_tokens") + if not isinstance(speech_tokens, torch.Tensor) or speech_tokens.numel() == 0: + return None + token_val = int(speech_tokens.reshape(-1).to(torch.long)[-1].item()) + # -1 = invalid/EOA marker + if token_val < 0: + return None + return token_val + + +def _to_cpu_tensor(value: Any) -> torch.Tensor | None: + """Convert value to CPU tensor if possible.""" + if isinstance(value, torch.Tensor): + return value.detach().cpu() + if isinstance(value, list): + if not value: + return None + if isinstance(value[0], torch.Tensor): + return value[0].detach().cpu() + return None + + +# --------------------------------------------------------------------------- +# Async streaming processor: emit speech token chunks as AR produces them +# --------------------------------------------------------------------------- + + +def ar_to_dit_async_chunk( + transfer_manager: Any, + pooling_output: dict[str, Any] | None, + request: Any, + is_finished: bool = False, +) -> OmniPayloadStruct | None: + """Async streaming processor: emit speech token chunks as AR produces them. + + Accumulates per-step speech tokens and emits fixed-size cumulative prefixes + for GLM-TTS flow-cache streaming. + + Follows the CosyVoice3 talker2code2wav_async_chunk transfer pattern: + - Per-request state tracking (seen tokens, prompt sent flag) + - First chunk carries voice clone conditioning payload + - Each chunk includes left_context_size, stream_finished, req_id + - code_predictor_codes for chunk_transfer_adapter consumption + + Unlike codec left-context decoders, official GLM-TTS sends the cumulative + AR token prefix to the flow stage on every chunk and reuses diffusion + latents internally. Here left_context_size means the stable token prefix + that has already been emitted, so DiT can crop regenerated audio after + sampling. + + GLM-TTS produces single-token-per-step (no multi-codebook), so each entry + in code_prompt_token_ids is a plain int, not a list of codebook values. + """ + request_id = getattr(request, "external_req_id", None) or getattr(request, "request_id", None) + if request_id is None: + raise ValueError("GLM-TTS async chunk request is missing request id") + finished = bool(is_finished or request.is_finished()) + + # Read connector chunk config (supports progressive list or single int) + connector = getattr(transfer_manager, "connector", None) + raw_cfg = getattr(connector, "config", {}) or {} + cfg = raw_cfg.get("extra", raw_cfg) if isinstance(raw_cfg, dict) else {} + chunk_frames_cfg = cfg.get("codec_chunk_frames", 25) + if isinstance(chunk_frames_cfg, list): + progressive_chunk_sizes = [int(c) for c in chunk_frames_cfg] + else: + progressive_chunk_sizes = [int(chunk_frames_cfg)] + left_context_size_config = int(cfg.get("codec_left_context_frames", 25)) + crossfade_sec = float(cfg.get("crossfade_sec", 0.1)) + + if not progressive_chunk_sizes or any(c <= 0 for c in progressive_chunk_sizes) or left_context_size_config < 0: + raise ValueError( + f"Invalid codec chunk config: codec_chunk_frames={chunk_frames_cfg}, " + f"codec_left_context_frames={left_context_size_config}" + ) + + # Initialize per-request state (like CosyVoice3) + request_payload = getattr(transfer_manager, "request_payload", None) + if request_payload is None: + request_payload = {} + transfer_manager.request_payload = request_payload + code_prompt_token_ids = getattr(transfer_manager, "code_prompt_token_ids", None) + if code_prompt_token_ids is None: + code_prompt_token_ids = {} + transfer_manager.code_prompt_token_ids = code_prompt_token_ids + code_prompt_token_ids.setdefault(request_id, []) + request_state = request_payload.get(request_id) + if not isinstance(request_state, dict) or "_glm_tts_async_state" not in request_state: + # Extract voice clone conditioning from request additional_information + info = deserialize_additional_information(getattr(request, "additional_information", None)) + prompt_payload: dict[str, Any] = {} + _copy_voice_clone_payload(info, prompt_payload, to_cpu=True) + + # Also try to extract from pooling_output (first call) + if isinstance(pooling_output, dict): + _copy_voice_clone_payload(pooling_output, prompt_payload, to_cpu=True, skip_existing=True) + + request_state = { + "_glm_tts_async_state": { + "seen_len": 0, + "sent_prompt": False, + "emitted_chunks": 0, + "emitted_token_len": 0, + "terminal_sent": False, + "prompt_payload": prompt_payload, + "chunk_sizes_history": [], + "block_pattern": progressive_chunk_sizes, + } + } + request_payload[request_id] = request_state + + state = request_state["_glm_tts_async_state"] + if state.get("terminal_sent", False): + return None + + # Helper: get voice clone embed (once per request) + def _pop_embed() -> EmbeddingsStruct | None: + if state.get("sent_prompt", False): + return None + state["sent_prompt"] = True + return _build_voice_clone_embed_struct(state.get("prompt_payload", {})) + + # Helper: build terminal or chunk payload + def _make_payload( + codes: list[int], + *, + tok_offset: int, + is_terminal: bool, + ) -> OmniPayloadStruct: + return OmniPayloadStruct( + codes=CodesStruct( + audio=(torch.tensor(codes, dtype=torch.long) if codes else torch.empty(0, dtype=torch.long)) + ), + meta=MetaStruct( + finished=torch.tensor(is_terminal, dtype=torch.bool), + stream_finished=torch.tensor(is_terminal, dtype=torch.bool), + req_id=[request_id], + left_context_size=tok_offset, + ), + kv_metadata={ + _GLM_TTS_PRIVATE_KEY: { + "chunk_sizes_history": list(state.get("chunk_sizes_history", [])), + "block_pattern": list(state.get("block_pattern", progressive_chunk_sizes)), + "crossfade_sec": crossfade_sec, + } + }, + embed=_pop_embed(), + ) + + # Accumulate new speech token from this step + if isinstance(pooling_output, dict): + token = _extract_last_speech_token(pooling_output) + if token is not None: + code_prompt_token_ids[request_id].append(token) + elif not finished: + return None + + token_frames = code_prompt_token_ids[request_id] + length = len(token_frames) + emitted_token_len = int(state.get("emitted_token_len", 0)) + + # Terminal: no tokens, or finished on/before chunk boundary + if finished and length <= emitted_token_len: + state["terminal_sent"] = True + # Boundary finish is metadata-only: the cumulative prefix has already + # been emitted, so resending it would make the DiT/vocoder redo work + # and can duplicate audio. + return _make_payload([], tok_offset=emitted_token_len, is_terminal=True) + + if length <= 0: + return None + + # Progressive chunk size: 25 → 50 → 200 (official GLM-TTS pattern) + chunk_count = int(state.get("emitted_chunks", 0)) + idx = min(chunk_count, len(progressive_chunk_sizes) - 1) + current_chunk_size = progressive_chunk_sizes[idx] + + # Check if enough tokens accumulated for next chunk + if not finished and (length - emitted_token_len) < current_chunk_size: + return None + + # Determine end index for cumulative prefix + if emitted_token_len == 0: + end_index = min(length, current_chunk_size) + token_offset = 0 + else: + end_index = length if finished else min(length, emitted_token_len + current_chunk_size) + token_offset = emitted_token_len + + # Track chunk sizes for cache slicing + chunk_sizes_history: list[int] = list(state.get("chunk_sizes_history", [])) + chunk_sizes_history.append(end_index - emitted_token_len) + state["chunk_sizes_history"] = chunk_sizes_history + state["emitted_chunks"] = chunk_count + 1 + + if finished: + state["terminal_sent"] = True + else: + state["emitted_token_len"] = max(emitted_token_len, end_index) + + return _make_payload(list(token_frames[:end_index]), tok_offset=token_offset, is_terminal=finished) diff --git a/vllm_omni/transformers_utils/configs/__init__.py b/vllm_omni/transformers_utils/configs/__init__.py index 708d9c2b6..1a806cc46 100644 --- a/vllm_omni/transformers_utils/configs/__init__.py +++ b/vllm_omni/transformers_utils/configs/__init__.py @@ -17,6 +17,7 @@ _CLASS_TO_MODULE: dict[str, str] = { "FishSpeechConfig": "vllm_omni.transformers_utils.configs.fish_speech", "FishSpeechSlowARConfig": "vllm_omni.transformers_utils.configs.fish_speech", "FishSpeechFastARConfig": "vllm_omni.transformers_utils.configs.fish_speech", + "GLMTTSConfig": "vllm_omni.transformers_utils.configs.glm_tts", "VoxCPM2Config": "vllm_omni.transformers_utils.configs.voxcpm2", "VoxtralTTSConfig": "vllm_omni.transformers_utils.configs.voxtral_tts", "CosyVoice3Config": "vllm_omni.transformers_utils.configs.cosyvoice3", @@ -36,6 +37,7 @@ __all__ = [ "FishSpeechConfig", "FishSpeechSlowARConfig", "FishSpeechFastARConfig", + "GLMTTSConfig", "VoxCPM2Config", "VoxtralTTSConfig", "CosyVoice3Config", @@ -65,6 +67,7 @@ def __dir__(): # run as soon as `vllm_omni.transformers_utils.configs` is imported. from vllm_omni.transformers_utils.configs import cosyvoice3 as _cosyvoice3 # noqa: F401, E402 from vllm_omni.transformers_utils.configs import fish_speech as _fish_speech # noqa: F401, E402 +from vllm_omni.transformers_utils.configs import glm_tts as _glm_tts # noqa: F401, E402 from vllm_omni.transformers_utils.configs import mammoth_moda2 as _mammoth_moda2 # noqa: F401, E402 from vllm_omni.transformers_utils.configs import ming_flash_omni as _ming_flash_omni # noqa: F401, E402 from vllm_omni.transformers_utils.configs import omnivoice as _omnivoice # noqa: F401, E402 diff --git a/vllm_omni/transformers_utils/configs/glm_tts.py b/vllm_omni/transformers_utils/configs/glm_tts.py new file mode 100644 index 000000000..9a7f8dfbf --- /dev/null +++ b/vllm_omni/transformers_utils/configs/glm_tts.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""GLM-TTS config registration with transformers AutoConfig. + +Registers GLMTTSConfig (model_type="glm_tts") so that +``AutoConfig.from_pretrained("path/to/glm-tts")`` returns the correct config class. + +Note: GLM-TTS uses a Llama backbone, but we register a custom config +to handle the special token IDs and flow model parameters. +""" + +from __future__ import annotations + +from typing import Any + +from transformers import AutoConfig, PretrainedConfig + + +class GLMTTSConfig(PretrainedConfig): + """Llama-based AR model for text-to-speech token generation. + + Special token IDs are loaded dynamically from the tokenizer at init time. + """ + + model_type: str = "glm_tts" + + def __init__( + self, + vocab_size: int = 98304, + hidden_size: int = 2048, + intermediate_size: int = 6144, + num_hidden_layers: int = 28, + num_attention_heads: int = 16, + num_key_value_heads: int = 4, + hidden_act: str = "silu", + max_position_embeddings: int = 8192, + rms_norm_eps: float = 1e-5, + rope_theta: float = 10000.0, + audio_token_start: int = -1, + audio_token_end: int = -1, + boa_token_id: int = -1, + eoa_token_id: int = -1, + pad_token_id: int = -1, + bos_token_id: int = -1, + speech_token_vocab_size: int = 32768, + speech_token_dim: int = 512, + spk_embed_dim: int = 192, + mel_dim: int = 80, + input_frame_rate: float = 25.0, + mel_framerate: int = 50, + max_token_text_ratio: float = 20.0, + min_token_text_ratio: float = 2.0, + sample_method: str = "ras", + ras_top_p: float = 0.8, + ras_top_k: int = 25, + ras_win_size: int = 10, + ras_tau_r: float = 0.1, + **kwargs: Any, + ): + self.vocab_size = vocab_size + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_hidden_layers = num_hidden_layers + self.num_attention_heads = num_attention_heads + self.num_key_value_heads = num_key_value_heads + self.hidden_act = hidden_act + self.max_position_embeddings = max_position_embeddings + self.rms_norm_eps = rms_norm_eps + self.rope_theta = rope_theta + self.audio_token_start = audio_token_start + self.audio_token_end = audio_token_end + self.boa_token_id = boa_token_id + self.eoa_token_id = eoa_token_id + self.speech_token_vocab_size = speech_token_vocab_size + self.speech_token_dim = speech_token_dim + self.spk_embed_dim = spk_embed_dim + self.mel_dim = mel_dim + self.input_frame_rate = input_frame_rate + self.mel_framerate = mel_framerate + self.max_token_text_ratio = max_token_text_ratio + self.min_token_text_ratio = min_token_text_ratio + self.sample_method = sample_method + self.ras_top_p = ras_top_p + self.ras_top_k = ras_top_k + self.ras_win_size = ras_win_size + self.ras_tau_r = ras_tau_r + super().__init__(pad_token_id=pad_token_id, bos_token_id=bos_token_id, **kwargs) + + +AutoConfig.register("glm_tts", GLMTTSConfig) + +__all__ = [ + "GLMTTSConfig", +] From cc0e15ba574716c6d578852cd7d75e529ce96bff Mon Sep 17 00:00:00 2001 From: Alex Brooks Date: Sun, 24 May 2026 07:55:34 -0600 Subject: [PATCH 018/112] [Bugfix] Fix LTX2 CacheDiT Integration (#3621) Signed-off-by: Alex Brooks --- tests/diffusion/cache/test_cache_dit.py | 87 +++++++++++++++++++ tests/helpers/fixtures/env.py | 13 +++ .../diffusion/models/ltx2/ltx2_transformer.py | 26 ++++-- 3 files changed, 119 insertions(+), 7 deletions(-) diff --git a/tests/diffusion/cache/test_cache_dit.py b/tests/diffusion/cache/test_cache_dit.py index 40a564485..0fd07393d 100644 --- a/tests/diffusion/cache/test_cache_dit.py +++ b/tests/diffusion/cache/test_cache_dit.py @@ -8,9 +8,14 @@ Model specific tests for CacheDiT enablement. from unittest.mock import Mock, patch import pytest +import torch +from cache_dit.caching.cache_blocks.pattern_0_1_2 import CachedBlocks_Pattern_0_1_2 import vllm_omni.diffusion.cache.cache_dit_backend as cd_backend +from vllm_omni.diffusion.cache.cache_dit_backend import CacheDiTBackend from vllm_omni.diffusion.data import DiffusionCacheConfig +from vllm_omni.diffusion.models.ltx2.ltx2_transformer import LTX2VideoTransformer3DModel +from vllm_omni.platforms import current_omni_platform pytestmark = [pytest.mark.core_model, pytest.mark.cpu] @@ -40,3 +45,85 @@ def test_separate_cfg(mock_cache_dit, mock_block_adapter, enabler): mock_cache_dit.enable_cache.assert_called_once() adapter_kwargs = mock_block_adapter.call_args.kwargs assert adapter_kwargs["has_separate_cfg"] is True + + +# This test is skipped on ROCm since rocm_unquantized_gemm doesn't support CPU backend +@pytest.mark.skipif( + current_omni_platform.is_rocm(), + reason="vLLM ROCm custom ops lack CPU fallback", +) +def test_ltx2_cache_dit_receives_audio_as_encoder(init_fake_tp_group): + """CacheDiT Pattern_0 treats the second positional arg as encoder_hidden_states, + which is a collision for one of the kwargs in LTX2 since we treat the audio + hidden states as encoder_hidden_states. + + This test ensures that a tiny LTX2 transformer can be initialized and run + through the cache DiT backend without hitting a collision on the kwargs. + """ + seq_len = 4 + video_in = torch.full((1, seq_len, 16), 1.0) + audio_in = torch.full((1, seq_len, 16), 2.0) + text_in = torch.full((1, seq_len, 16), 3.0) + audio_text_in = torch.full((1, seq_len, 16), 4.0) + + model = LTX2VideoTransformer3DModel( + in_channels=16, + out_channels=16, + patch_size=1, + patch_size_t=1, + num_attention_heads=2, + attention_head_dim=8, + cross_attention_dim=16, + audio_in_channels=16, + audio_out_channels=16, + audio_num_attention_heads=2, + audio_attention_head_dim=8, + audio_cross_attention_dim=16, + num_layers=2, + caption_channels=16, + ) + + # NOTE: This is currently using the LTX2 custom enabler, but the custom + # enablers will be consolidated after + # https://github.com/vllm-project/vllm-omni/pull/2527 lands. + LTX2Pipeline = type("LTX2Pipeline", (), {}) + pipeline = LTX2Pipeline() + pipeline.transformer = model + backend = CacheDiTBackend(DiffusionCacheConfig()) + backend.enable(pipeline) + backend.refresh(pipeline, num_inference_steps=5) + + # Wrap call_Fn_blocks in CacheDiT so that we can verify the + # hidden/encoder states are what we expect them to be + captured = {} + original = CachedBlocks_Pattern_0_1_2.call_Fn_blocks + + def call_Fn_blocks_and_capture(self, hidden_states, encoder_hidden_states, *a, **kw): + captured["hidden_states"] = hidden_states + captured["encoder_hidden_states"] = encoder_hidden_states + return original(self, hidden_states, encoder_hidden_states, *a, **kw) + + # Also, map projections to identity so that we can just check + # the captured tensors directly instead of having to reproject + identity = torch.nn.Identity() + with ( + patch.object(model, "proj_in", identity), + patch.object(model, "audio_proj_in", identity), + patch.object(CachedBlocks_Pattern_0_1_2, "call_Fn_blocks", call_Fn_blocks_and_capture), + torch.no_grad(), + ): + model( + hidden_states=video_in, + audio_hidden_states=audio_in, + encoder_hidden_states=text_in, + audio_encoder_hidden_states=audio_text_in, + timestep=torch.tensor([[1000.0] * seq_len]), + num_frames=1, + height=2, + width=2, + audio_num_frames=seq_len, + ) + + # Pattern_0 maps (hidden_states, encoder_hidden_states) to (video, audio) + assert torch.equal(captured["hidden_states"], video_in) + assert torch.equal(captured["encoder_hidden_states"], audio_in) diff --git a/tests/helpers/fixtures/env.py b/tests/helpers/fixtures/env.py index 452ae2d2d..2d38e4907 100644 --- a/tests/helpers/fixtures/env.py +++ b/tests/helpers/fixtures/env.py @@ -2,6 +2,7 @@ import os import pytest import torch +from vllm.distributed import parallel_state @pytest.fixture(scope="session", autouse=True) @@ -59,3 +60,15 @@ def default_vllm_config(): with set_current_vllm_config(VllmConfig(device_config=device_config)): yield + + +@pytest.fixture() +def init_fake_tp_group(mocker): + """Provide a fake TP group so vllm linear layers can be instantiated.""" + mock_tp = mocker.MagicMock() + mock_tp.world_size = 1 + mock_tp.rank_in_group = 0 + old = parallel_state._TP + parallel_state._TP = mock_tp + yield + parallel_state._TP = old diff --git a/vllm_omni/diffusion/models/ltx2/ltx2_transformer.py b/vllm_omni/diffusion/models/ltx2/ltx2_transformer.py index 2a65ad6b0..64bb67e22 100644 --- a/vllm_omni/diffusion/models/ltx2/ltx2_transformer.py +++ b/vllm_omni/diffusion/models/ltx2/ltx2_transformer.py @@ -1939,10 +1939,6 @@ class LTX2VideoTransformer3DModel(nn.Module): # 5. Run transformer blocks for block in self.transformer_blocks: block_kwargs = { - "hidden_states": hidden_states, - "audio_hidden_states": audio_hidden_states, - "encoder_hidden_states": encoder_hidden_states, - "audio_encoder_hidden_states": audio_encoder_hidden_states, "temb": temb, "temb_audio": temb_audio, "temb_ca_scale_shift": video_cross_attn_scale_shift, @@ -1958,11 +1954,27 @@ class LTX2VideoTransformer3DModel(nn.Module): "encoder_attention_mask": encoder_attention_mask, "audio_encoder_attention_mask": audio_encoder_attention_mask, } + # NOTE: We intentionally pass the first 4 args here positionally, + # because CacheDiT expects the first 2 args to be hidden_states + # and encoder_hidden_states, so passing them as kwargs will cause + # positional / keywords arg collisions. if torch.is_grad_enabled() and self.gradient_checkpointing: - hidden_states, audio_hidden_states = self._gradient_checkpointing_func(block, **block_kwargs) + hidden_states, audio_hidden_states = self._gradient_checkpointing_func( + block, + hidden_states, + audio_hidden_states, + encoder_hidden_states, + audio_encoder_hidden_states, + **block_kwargs, + ) else: - hidden_states, audio_hidden_states = block(**block_kwargs) - + hidden_states, audio_hidden_states = block( + hidden_states, + audio_hidden_states, + encoder_hidden_states, + audio_encoder_hidden_states, + **block_kwargs, + ) # 6. Output layers (including unpatchification) scale_shift_values = self.scale_shift_table[None, None] + embedded_timestep[:, :, None] shift, scale = scale_shift_values[:, :, 0], scale_shift_values[:, :, 1] From 6c1bb27b6916fb6450b1ec1ce9fef81474e15881 Mon Sep 17 00:00:00 2001 From: akshatvishu <33392262+akshatvishu@users.noreply.github.com> Date: Sun, 24 May 2026 22:21:39 +0530 Subject: [PATCH 019/112] docs: fix CUDA pre-built image command (#3836) Signed-off-by: akshatvishu --- docs/getting_started/installation/gpu/cuda.inc.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting_started/installation/gpu/cuda.inc.md b/docs/getting_started/installation/gpu/cuda.inc.md index 69fc05351..9f3c86843 100644 --- a/docs/getting_started/installation/gpu/cuda.inc.md +++ b/docs/getting_started/installation/gpu/cuda.inc.md @@ -103,10 +103,10 @@ docker run --runtime nvidia --gpus 2 \ -p 8091:8091 \ --ipc=host \ vllm/vllm-omni:v0.21.0 \ - --model Qwen/Qwen3-Omni-30B-A3B-Instruct --port 8091 + vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 ``` !!! tip - You can use this docker image to serve models the same way you would with in vLLM! To do so, make sure you overwrite the default entrypoint (`vllm serve --omni`) which works only for models supported in the vLLM-Omni project. + The CUDA image does not define a default entrypoint, so include `vllm serve ... --omni` after the image name. # --8<-- [end:pre-built-images] From c890b3a99c04e91fc050fc2bfc39706d40fbea4a Mon Sep 17 00:00:00 2001 From: Canlin Guo Date: Mon, 25 May 2026 09:08:18 +0800 Subject: [PATCH 020/112] [BugFix][NPU] Honor prefer_model_sampler in NPU AR runner (#3517) Signed-off-by: gcanlin Co-authored-by: Hongsheng Liu --- .../npu/worker/npu_ar_model_runner.py | 33 +++++++++++++ vllm_omni/worker/gpu_ar_model_runner.py | 47 ------------------ vllm_omni/worker/gpu_model_runner.py | 49 +++++++++++++++++++ 3 files changed, 82 insertions(+), 47 deletions(-) diff --git a/vllm_omni/platforms/npu/worker/npu_ar_model_runner.py b/vllm_omni/platforms/npu/worker/npu_ar_model_runner.py index 82d628ae9..8d058335e 100644 --- a/vllm_omni/platforms/npu/worker/npu_ar_model_runner.py +++ b/vllm_omni/platforms/npu/worker/npu_ar_model_runner.py @@ -730,6 +730,39 @@ class NPUARModelRunner(OmniNPUModelRunner): return None + def _sample( + self, + logits: torch.Tensor | None, + spec_decode_metadata: Any, + ): + sampling_metadata = self.input_batch.sampling_metadata + if spec_decode_metadata is None: + model_sample = getattr(self.model, "sample", None) + self.input_batch.update_async_output_token_ids() + if logits is not None and callable(model_sample) and getattr(self.model, "prefer_model_sampler", False): + # Apply logit bias (min_tokens, allowed_token_ids) before + # the custom model sampler — the standard GPU sampler does + # this internally, but prefer_model_sampler bypasses it. + if hasattr(self.sampler, "logit_bias_state"): + self.sampler.logit_bias_state.apply_logit_bias( + logits, + self.input_batch.expanded_idx_mapping, + self.input_batch.idx_mapping_np, + self.input_batch.positions[self.input_batch.logits_indices], + ) + sampler_output = model_sample( + logits, + self._sampling_metadata_for_model_sampler(sampling_metadata), + ) + if sampler_output is not None: + return sampler_output + return self.sampler( + logits=logits, + sampling_metadata=sampling_metadata, + ) + + return super()._sample(logits, spec_decode_metadata) + @torch.inference_mode() def sample_tokens( self, grammar_output: GrammarOutput | None diff --git a/vllm_omni/worker/gpu_ar_model_runner.py b/vllm_omni/worker/gpu_ar_model_runner.py index 2bcd4ba01..34d571c11 100644 --- a/vllm_omni/worker/gpu_ar_model_runner.py +++ b/vllm_omni/worker/gpu_ar_model_runner.py @@ -109,53 +109,6 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin): with maybe_disable_pin_memory_for_ray(self, total_bytes): return super()._make_buffer(*size, dtype=dtype, numpy=numpy) - def _build_model_sampler_output_token_ids(self) -> list[list[int]]: - """Build decoded-token history for custom model samplers. - - vLLM only populates sampling_metadata.output_token_ids when penalties or - logits processors require it. CosyVoice3's custom RAS sampler also - depends on this history, so we reconstruct it directly from the input - batch for prefer_model_sampler models. - """ - req_output_token_ids = getattr(self.input_batch, "req_output_token_ids", []) - req_ids = list(getattr(self.input_batch, "req_ids", [])) - output_token_ids = [list(req_output_token_ids[idx] or []) for idx in range(len(req_ids))] - - sampled_token_ids_cpu = getattr(self.input_batch, "sampled_token_ids_cpu", None) - async_copy_ready_event = getattr(self.input_batch, "async_copy_ready_event", None) - prev_req_id_to_index = getattr(self.input_batch, "prev_req_id_to_index", None) - if sampled_token_ids_cpu is None or not output_token_ids or prev_req_id_to_index is None: - return output_token_ids - - sampled_token_ids: list[list[int]] | None = None - for index, req_id in enumerate(req_ids): - prev_index = prev_req_id_to_index.get(req_id) - if prev_index is None: - continue - req_history = output_token_ids[index] - if not req_history or req_history[-1] != -1: - continue - if sampled_token_ids is None: - assert async_copy_ready_event is not None - async_copy_ready_event.synchronize() - sampled_token_ids = sampled_token_ids_cpu.tolist() - new_ids = list(sampled_token_ids[prev_index]) - if not new_ids: - continue - num_sampled_ids = len(new_ids) if new_ids[-1] != -1 else new_ids.index(-1) - first_placeholder = req_history.index(-1) - num_placeholders = len(req_history) - first_placeholder - num_to_replace = min(num_sampled_ids, num_placeholders) - req_history[first_placeholder : first_placeholder + num_to_replace] = new_ids[:num_to_replace] - - return output_token_ids - - def _sampling_metadata_for_model_sampler(self, sampling_metadata): - output_token_ids = self._build_model_sampler_output_token_ids() - if output_token_ids == sampling_metadata.output_token_ids: - return sampling_metadata - return replace(sampling_metadata, output_token_ids=output_token_ids) - def _request_final_stage_id(self, req_id: str) -> int | None: info = self.model_intermediate_buffer.get(req_id) if not isinstance(info, dict): diff --git a/vllm_omni/worker/gpu_model_runner.py b/vllm_omni/worker/gpu_model_runner.py index eae7c0a11..5f76f7be1 100644 --- a/vllm_omni/worker/gpu_model_runner.py +++ b/vllm_omni/worker/gpu_model_runner.py @@ -1,5 +1,6 @@ import contextlib from collections.abc import Callable +from dataclasses import replace from typing import TYPE_CHECKING, Any, cast import numpy as np @@ -121,6 +122,54 @@ class OmniGPUModelRunner(GPUModelRunner): self.last_talker_hidden = self._make_buffer(max_batch_size, hidden_size, dtype=self.dtype, numpy=False) self.text_step = self._make_buffer(max_batch_size, hidden_size, dtype=self.dtype, numpy=False) + def _build_model_sampler_output_token_ids(self) -> list[list[int]]: + """Build decoded-token history for ``prefer_model_sampler`` models. + + vLLM only populates ``sampling_metadata.output_token_ids`` when penalties + or logits processors require it. Models that opt into a custom sampler + (e.g. CosyVoice3 RAS sampler, HunyuanImage3 stage-transition sampler) + also depend on this history, so we reconstruct it directly from the + input batch. Shared by GPU and NPU AR runners. + """ + req_output_token_ids = getattr(self.input_batch, "req_output_token_ids", []) + req_ids = list(getattr(self.input_batch, "req_ids", [])) + output_token_ids = [list(req_output_token_ids[idx] or []) for idx in range(len(req_ids))] + + sampled_token_ids_cpu = getattr(self.input_batch, "sampled_token_ids_cpu", None) + async_copy_ready_event = getattr(self.input_batch, "async_copy_ready_event", None) + prev_req_id_to_index = getattr(self.input_batch, "prev_req_id_to_index", None) + if sampled_token_ids_cpu is None or not output_token_ids or prev_req_id_to_index is None: + return output_token_ids + + sampled_token_ids: list[list[int]] | None = None + for index, req_id in enumerate(req_ids): + prev_index = prev_req_id_to_index.get(req_id) + if prev_index is None: + continue + req_history = output_token_ids[index] + if not req_history or req_history[-1] != -1: + continue + if sampled_token_ids is None: + assert async_copy_ready_event is not None + async_copy_ready_event.synchronize() + sampled_token_ids = sampled_token_ids_cpu.tolist() + new_ids = list(sampled_token_ids[prev_index]) + if not new_ids: + continue + num_sampled_ids = len(new_ids) if new_ids[-1] != -1 else new_ids.index(-1) + first_placeholder = req_history.index(-1) + num_placeholders = len(req_history) - first_placeholder + num_to_replace = min(num_sampled_ids, num_placeholders) + req_history[first_placeholder : first_placeholder + num_to_replace] = new_ids[:num_to_replace] + + return output_token_ids + + def _sampling_metadata_for_model_sampler(self, sampling_metadata): + output_token_ids = self._build_model_sampler_output_token_ids() + if output_token_ids == sampling_metadata.output_token_ids: + return sampling_metadata + return replace(sampling_metadata, output_token_ids=output_token_ids) + def _init_mrope_positions(self, req_state: CachedRequestState): """Initialize M-RoPE positions for multimodal inputs. From b52a5ca40164d72af2495ef5172ed3c5b6a55d7d Mon Sep 17 00:00:00 2001 From: hurukawa <61525444+nagisa-kunhah@users.noreply.github.com> Date: Mon, 25 May 2026 09:38:24 +0800 Subject: [PATCH 021/112] [Bugfix][Example][OmniVoice] Drop hardcoded "voice": "default" from speech_client.py (#3829) Signed-off-by: nagisa-kun <1434936049@qq.com> Co-authored-by: Hongsheng Liu --- .../text_to_speech/omnivoice/speech_client.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/examples/online_serving/text_to_speech/omnivoice/speech_client.py b/examples/online_serving/text_to_speech/omnivoice/speech_client.py index b8e6f3889..4c3ec334b 100644 --- a/examples/online_serving/text_to_speech/omnivoice/speech_client.py +++ b/examples/online_serving/text_to_speech/omnivoice/speech_client.py @@ -6,6 +6,9 @@ Examples: # Specify language python speech_client.py --text "Bonjour, comment allez-vous?" --language French + + # Use a specific uploaded/supported voice + python speech_client.py --text "Hello" --voice my_uploaded_voice """ import argparse @@ -21,15 +24,18 @@ def run_tts(args) -> None: payload = { "model": args.model, "input": args.text, - "voice": "default", "response_format": args.response_format, } + if args.voice: + payload["voice"] = args.voice if args.language: payload["language"] = args.language print(f"Model: {args.model}") print(f"Text: {args.text}") + if args.voice: + print(f"Voice: {args.voice}") if args.language: print(f"Language: {args.language}") print("Generating audio...") @@ -68,6 +74,11 @@ def main(): parser.add_argument("--api-key", default=DEFAULT_API_KEY, help="API key") parser.add_argument("--model", "-m", default="k2-fsa/OmniVoice", help="Model name") parser.add_argument("--text", required=True, help="Text to synthesize") + parser.add_argument( + "--voice", + default=None, + help="Voice name (omit for auto voice; must match a supported or uploaded speaker if set)", + ) parser.add_argument("--language", default=None, help="Language hint (e.g., English, Chinese, French)") parser.add_argument( "--response-format", From 9a37ca83d405e169300f887531158bbc54aa5deb Mon Sep 17 00:00:00 2001 From: zzh <67720593+BLANKETusers@users.noreply.github.com> Date: Mon, 25 May 2026 11:03:39 +0800 Subject: [PATCH 022/112] Add hunyuan online accuracy test (#3795) Signed-off-by: zzh <943967662@qq.com> --- tests/e2e/accuracy/test_hunyuan_image3.py | 104 +++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/tests/e2e/accuracy/test_hunyuan_image3.py b/tests/e2e/accuracy/test_hunyuan_image3.py index bc3fd7f4d..81394297d 100644 --- a/tests/e2e/accuracy/test_hunyuan_image3.py +++ b/tests/e2e/accuracy/test_hunyuan_image3.py @@ -14,6 +14,7 @@ from dataclasses import dataclass from pathlib import Path import pytest +import requests import torch import yaml from PIL import Image @@ -27,6 +28,7 @@ from tests.e2e.accuracy.helpers import ( model_output_dir, ) from tests.helpers.mark import hardware_test +from tests.helpers.runtime import OmniRunner, OmniServer from vllm_omni.diffusion.models.hunyuan_image3.prompt_utils import build_prompt_tokens, resolve_stop_token_ids os.environ["DIFFUSION_ATTENTION_BACKEND"] = "TORCH_SDPA" @@ -240,7 +242,6 @@ def _make_quant_dit_config(path: Path) -> None: def _run_offline(deploy_config_path: str, output_path: Path) -> tuple[Image.Image, str, float]: from transformers import AutoTokenizer - from tests.helpers.runtime import OmniRunner from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniPromptType from vllm_omni.platforms import current_omni_platform @@ -313,6 +314,107 @@ def _run_offline(deploy_config_path: str, output_path: Path) -> tuple[Image.Imag return image, cot_text, elapsed +def _run_online(stage_configs_path: str, output_path: Path) -> tuple[Image.Image, str, float]: + from benchmarks.accuracy.common import decode_base64_image, pil_to_png_bytes + + server_args = [ + "--stage-configs-path", + stage_configs_path, + "--stage-init-timeout", + "300", + "--init-timeout", + "900", + ] + try: + with OmniServer(MODEL_PATH, server_args, use_omni=True) as omni_server: + images = download_images(TEST_IMAGE_URLS) + t0 = time.perf_counter() + response = requests.post( + f"http://{omni_server.host}:{omni_server.port}/v1/images/edits", + data={ + "model": omni_server.model, + "prompt": PROMPT, + "n": 1, + "response_format": "b64_json", + "num_inference_steps": NUM_INFERENCE_STEPS, + "guidance_scale": GUIDANCE_SCALE, + "seed": SEED, + "sys_type": "en_unified", + "bot_task": "think_recaption", + "size": "1280x720", + }, + files=[ + ("image", (f"image_{i}.png", pil_to_png_bytes(img), "image/png")) for i, img in enumerate(images) + ], + timeout=600, + ) + elapsed = time.perf_counter() - t0 + if not response.ok: + print(f"[ONLINE] HTTP {response.status_code} response body: {response.text}") + response.raise_for_status() + payload = response.json() + assert len(payload["data"]) == 1 + image = decode_base64_image(payload["data"][0]["b64_json"]) + image.load() + image.save(output_path / "image_online.png") + cot_text = payload.get("cot_output") or "" + (output_path / "cot_online.txt").write_text(cot_text, encoding="utf-8") + return image, cot_text, elapsed + finally: + gc.collect() + if torch.accelerator.is_available(): + torch.accelerator.empty_cache() + + +@pytest.mark.skipif( + torch.accelerator.device_count() < AR_TP_SIZE + DIT_TP_SIZE, + reason=f"Needs {AR_TP_SIZE + DIT_TP_SIZE}+ GPUs ({AR_TP_SIZE} AR + {DIT_TP_SIZE} DiT)", +) +def test_image_to_image_alignment_online(accuracy_artifact_root: Path, accuracy_assets_root: Path) -> None: + """Online API test: same pipeline, same seed as offline → PSNR >= 10 dB.""" + if importlib.util.find_spec("FlagEmbedding") is None: + raise ImportError("Missing dependency: FlagEmbedding\nInstall with: pip install FlagEmbedding") + from tabulate import tabulate + + output_dir = model_output_dir(accuracy_artifact_root, MODEL_NAME + "-online") + + with tempfile.TemporaryDirectory() as tmpdir: + tmp = Path(tmpdir) + _make_config(True, tmp / "online.yaml") + online_image, online_cot, _ = _run_online(str(tmp / "online.yaml"), output_dir) + + online_cot = online_cot.lstrip("\n") + scorer = SemanticSimilarityScorer() + clip_scorer = CLIPScorer() + cot_results = scorer.text_similarity(online_cot, COT_REF) + image_ref = Image.open(str(accuracy_assets_root / "hunyuan_image_ref.png")).convert("RGB") + image_clip_score = clip_scorer.image_image_score(online_image, image_ref) + ssim_value, psnr_value = compute_image_ssim_psnr(prediction=online_image, reference=image_ref, compare_mode="RGB") + + table = [ + ["COT similarity to reference", f"{cot_results['cot_semantic_sim']:.4f}", 0.9644], + ["COT prefix match", f"{cot_results['text_prefix_match_count']:.4f}", 29], + ["Image-Image similarity", f"{image_clip_score:.4f}", 94.5538], + ["SSIM", f"{ssim_value:.4f}", 0.242], + ["PSNR (dB)", f"{psnr_value:.2f}", 14.1], + ] + print("[ONLINE] " + tabulate(table, headers=["Metric", "Value", "L20x Reference"], tablefmt="grid")) + + assert cot_results["cot_semantic_sim"] >= THRESHOLDS["cot_semantic_sim"], ( + f"[ONLINE] COT semantic similarity {cot_results['cot_semantic_sim']:.4f} below threshold {THRESHOLDS['cot_semantic_sim']}" + ) + assert cot_results["text_prefix_match_count"] >= THRESHOLDS["text_prefix_match"], ( + f"[ONLINE] COT prefix match {cot_results['text_prefix_match_count']} below threshold {THRESHOLDS['text_prefix_match']}" + ) + assert image_clip_score >= THRESHOLDS["clip_score"], ( + f"[ONLINE] Image-Image similarity {image_clip_score:.4f} below threshold {THRESHOLDS['clip_score']}" + ) + assert ssim_value >= THRESHOLDS["ssim"], f"[ONLINE] SSIM {ssim_value:.4f} below threshold {THRESHOLDS['ssim']}" + assert psnr_value >= THRESHOLDS["psnr"], ( + f"[ONLINE] PSNR {psnr_value:.2f} dB below threshold {THRESHOLDS['psnr']} dB" + ) + + def _extract_image(outputs) -> Image.Image: assert outputs, "Pipeline produced no outputs" for output in outputs: From 5f9aee193eb0a1119fa6f5ba66ee7244daf6bc3c Mon Sep 17 00:00:00 2001 From: zhumingjue138 Date: Mon, 25 May 2026 11:38:17 +0800 Subject: [PATCH 023/112] [CI] Increase timeout for Quantization Test in nightly build to 60 minutes (#3845) Signed-off-by: zhumingjue --- .buildkite/test-nightly.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.buildkite/test-nightly.yml b/.buildkite/test-nightly.yml index 82e872581..1e25cf2e0 100644 --- a/.buildkite/test-nightly.yml +++ b/.buildkite/test-nightly.yml @@ -1086,9 +1086,8 @@ steps: if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test" || build.pull_request.labels includes "diffusion-x2iat-test" steps: - label: ":full_moon: Quantization Test with H100" - timeout_in_minutes: 90 commands: - - timeout 30m pytest -sv tests/diffusion/quantization -m 'full_model and cuda and H100' --run-level "full_model" + - timeout 60m pytest -sv tests/diffusion/quantization -m 'full_model and cuda and H100' --run-level "full_model" agents: queue: "mithril-h100-pool" plugins: From e996723bc9357409a287df6a495d72ff5e677504 Mon Sep 17 00:00:00 2001 From: Yueqian Lin <70319226+linyueqian@users.noreply.github.com> Date: Mon, 25 May 2026 11:09:38 -0400 Subject: [PATCH 024/112] [Bugfix] Fix Qwen3-TTS Stage 0 prefix-caching correctness (#3665) Signed-off-by: Yueqian Lin Signed-off-by: Rein Yang Co-authored-by: Rein Yang the ci failure case is irrelavent to this PR and occurs rarely --- tests/core/test_prefix_cache.py | 120 ++++++++++- .../test_qwen3_tts_talker_preprocess.py | 3 + vllm_omni/core/prefix_cache.py | 197 +++++++++++++++++- vllm_omni/deploy/qwen3_tts.yaml | 7 +- vllm_omni/engine/orchestrator.py | 2 +- vllm_omni/entrypoints/omni_base.py | 4 + .../entrypoints/openai/serving_speech.py | 130 ++++++++++++ .../models/qwen3_tts/qwen3_tts_talker.py | 138 +++++++++++- .../npu/worker/npu_ar_model_runner.py | 21 +- vllm_omni/utils/mm_outputs.py | 11 +- vllm_omni/worker/gpu_ar_model_runner.py | 63 +++++- vllm_omni/worker/gpu_model_runner.py | 2 + 12 files changed, 655 insertions(+), 43 deletions(-) diff --git a/tests/core/test_prefix_cache.py b/tests/core/test_prefix_cache.py index b5d0e96d3..326eb6b34 100644 --- a/tests/core/test_prefix_cache.py +++ b/tests/core/test_prefix_cache.py @@ -13,7 +13,7 @@ DEFAULT_SHAPE = torch.Size([NUM_BLOCKS, BLOCK_SIZE, HIDDEN_SIZE]) class MockInputBatch: - def __init__(self, num_computed_tokens_cpu): + def __init__(self, num_computed_tokens_cpu, block_table=None): self.req_ids = ["req1", "req2"] self.req_id_to_index = {req_id: i for i, req_id in enumerate(self.req_ids)} self.num_computed_tokens_cpu = num_computed_tokens_cpu @@ -21,11 +21,33 @@ class MockInputBatch: # Block table is only mocked for validation of length; # we don't actually need to add valid values here since # we patch the table when testing. - class _DummyBlockTable: - pass + if block_table is None: - self.block_table = _DummyBlockTable() - self.block_table.block_tables = [None] + class _DummyBlockTable: + pass + + self.block_table = _DummyBlockTable() + self.block_table.block_tables = [None] + else: + + class _TensorWrapper: + def __init__(self, tensor): + self.cpu = tensor + + class _BlockGroup: + def __init__(self, tensor): + self.block_table = _TensorWrapper(tensor) + + class _BlockTable: + def __init__(self, tensor): + self._group = _BlockGroup(tensor) + self.block_tables = [self._group.block_table] + + def __getitem__(self, idx): + assert idx == 0 + return self._group + + self.block_table = _BlockTable(block_table) def get_omni_pcache_with_mm_tensors(feat_dims, seq_len) -> OmniTensorPrefixCache: @@ -171,6 +193,94 @@ def test_update_with_multimodal_outputs(feat_dims): assert torch.all(slot_states == new_output) +def test_deferred_multimodal_cache_write_commits_on_completion(): + cache = get_omni_pcache() + mm_key = "codes.audio" + block_table = torch.tensor( + [ + [2, 3, 4, 5], + [6, 7, 8, 9], + ], + dtype=torch.long, + ) + input_batch = MockInputBatch( + num_computed_tokens_cpu=torch.tensor([4, 0], dtype=torch.long), + block_table=block_table, + ) + + first_chunk = torch.arange(8, dtype=torch.long).reshape(4, 2) + cache.update_omni_tensor_prefix_cache( + hidden_states=None, + multimodal_outputs={mm_key: first_chunk}, + num_tokens_unpadded=4, + slot_mapping=torch.arange(8, 12), + skip_mm_cache_keys={mm_key}, + ) + + rows = cache.mm_outputs_cache[mm_key].view(-1, 2) + assert torch.all(rows[8:12] == 0) + + cache.stage_deferred_mm_outputs( + query_start_loc=torch.tensor([0, 4], dtype=torch.long), + input_batch=input_batch, + multimodal_outputs={mm_key: first_chunk}, + num_scheduled_tokens={"req1": 4, "req2": 0}, + deferred_mm_cache_keys={mm_key}, + ) + + second_chunk = torch.arange(8, 16, dtype=torch.long).reshape(4, 2) + input_batch.num_computed_tokens_cpu = torch.tensor([8, 0], dtype=torch.long) + cache.stage_deferred_mm_outputs( + query_start_loc=torch.tensor([0, 4], dtype=torch.long), + input_batch=input_batch, + multimodal_outputs={mm_key: second_chunk}, + num_scheduled_tokens={"req1": 4, "req2": 0}, + deferred_mm_cache_keys={mm_key}, + ) + + cache.commit_deferred_mm_outputs({"req1"}, input_batch) + + expected = torch.cat([first_chunk, second_chunk], dim=0) + rows = cache.mm_outputs_cache[mm_key].view(-1, 2) + assert torch.equal(rows[8:16], expected) + + +def test_deferred_multimodal_cache_can_be_merged_on_full_block_hit(): + cache = get_omni_pcache() + mm_key = "codes.audio" + block_table = torch.tensor( + [ + [2, 3, 4, 5], + [6, 7, 8, 9], + ], + dtype=torch.long, + ) + input_batch = MockInputBatch( + num_computed_tokens_cpu=torch.tensor([8, 0], dtype=torch.long), + block_table=block_table, + ) + cached_codes = torch.arange(16, dtype=torch.long).reshape(8, 2) + cache.stage_deferred_mm_outputs( + query_start_loc=torch.tensor([0, 8], dtype=torch.long), + input_batch=input_batch, + multimodal_outputs={mm_key: cached_codes}, + num_scheduled_tokens={"req1": 8, "req2": 0}, + deferred_mm_cache_keys={mm_key}, + ) + cache.commit_deferred_mm_outputs({"req1"}, input_batch) + + new_codes = torch.tensor([[99, 100]], dtype=torch.long) + cache.add_prefix_cached_new_req_id("req1") + merged = cache.get_merged_multimodal_states( + query_start_loc=torch.tensor([0, 1], dtype=torch.long), + input_batch=input_batch, + multimodal_outputs={mm_key: new_codes}, + num_scheduled_tokens={"req1": 1, "req2": 0}, + ) + + assert torch.equal(merged[mm_key]["req1"], torch.cat([cached_codes, new_codes], dim=0)) + + ### Tests for Merging def fake_get_cached_block_ids(self, req_idx, *args, **kwargs): """Fake block table lookup. diff --git a/tests/model_executor/models/qwen3_tts/test_qwen3_tts_talker_preprocess.py b/tests/model_executor/models/qwen3_tts/test_qwen3_tts_talker_preprocess.py index fd3fecaa9..d4b8e4a32 100644 --- a/tests/model_executor/models/qwen3_tts/test_qwen3_tts_talker_preprocess.py +++ b/tests/model_executor/models/qwen3_tts/test_qwen3_tts_talker_preprocess.py @@ -1,3 +1,4 @@ +from collections import OrderedDict from types import SimpleNamespace import numpy as np @@ -15,6 +16,8 @@ from vllm_omni.model_executor.models.qwen3_tts.qwen3_tts_talker import ( def _make_minimal_talker(): model = Qwen3TTSTalkerForConditionalGeneration.__new__(Qwen3TTSTalkerForConditionalGeneration) model.talker_config = SimpleNamespace(codec_pad_id=7, num_code_groups=16) + model._ref_audio_artifact_cache_max_entries = 256 + model._ref_audio_artifact_cache = OrderedDict() return model diff --git a/vllm_omni/core/prefix_cache.py b/vllm_omni/core/prefix_cache.py index 69e7346c4..f80074173 100644 --- a/vllm_omni/core/prefix_cache.py +++ b/vllm_omni/core/prefix_cache.py @@ -47,6 +47,7 @@ class OmniTensorPrefixCache: self.mm_outputs_cache = {} self.mm_cache_keys = set() self._new_req_cache_hit_ids: set[str] = set() + self._deferred_mm_outputs: dict[str, dict[str, list[tuple[int, torch.Tensor]]]] = {} def maybe_init_missing_mm_cache_keys(self, multimodal_outputs: dict, seq_len: int): """Given multimodal outputs from executing the model, dynamically @@ -62,7 +63,17 @@ class OmniTensorPrefixCache: determined by the warmup. """ for key, val in multimodal_outputs.items(): - if isinstance(val, torch.Tensor) and val.shape[0] == seq_len and key not in self.mm_cache_keys: + # Only cache per-token feature tensors: 2D+ with first dim == seq_len. + # A 1D tensor of shape (seq_len,) is a broadcast scalar (per-request + # metadata such as ref_code_len / codec_streaming), not per-token data; + # caching it by slot causes a shape mismatch when a later request has a + # different scheduled seq length. + if ( + isinstance(val, torch.Tensor) + and val.ndim >= 2 + and val.shape[0] == seq_len + and key not in self.mm_cache_keys + ): feat_dim = val.shape[-1] self.mm_outputs_cache[key] = self._get_cache_tensor( dtype=val.dtype, @@ -89,6 +100,10 @@ class OmniTensorPrefixCache: """Clears the cache hit IDs to prepare for a new engine step.""" self._new_req_cache_hit_ids.clear() + def has_prefix_cached_new_req_ids(self) -> bool: + """Return True when this step contains a newly scheduled prefix hit.""" + return bool(self._new_req_cache_hit_ids) + @staticmethod def _coerce_to_cpu_tensor(maybe_gpu_tensor: torch.Tensor) -> torch.Tensor: """Convert GPU tensors -> contiguous CPU tensors if needed.""" @@ -101,6 +116,7 @@ class OmniTensorPrefixCache: num_tokens_unpadded: int, slot_mapping: torch.Tensor, num_tokens_padded: int | None = None, + skip_mm_cache_keys: set[str] | None = None, ): """Updates the hidden cache state for the provided hidden states and multimodal outputs. @@ -110,10 +126,12 @@ class OmniTensorPrefixCache: num_tokens_unpadded: Number of tokens without padding slot_mapping: Slot mapping for the input sequence num_tokens_padded: Total number of tokens including padding + skip_mm_cache_keys: Multimodal keys whose CPU cache writes are deferred """ unpadded_slot_mapping = slot_mapping[:num_tokens_unpadded] if num_tokens_padded is None: num_tokens_padded = num_tokens_unpadded + skip_mm_cache_keys = skip_mm_cache_keys or set() if hidden_states is not None: # Slice to unpadded portion before caching @@ -136,6 +154,8 @@ class OmniTensorPrefixCache: for mm_out_key, mm_cache in self.mm_outputs_cache.items(): if mm_out_key in multimodal_outputs: + if mm_out_key in skip_mm_cache_keys: + continue # Slice to unpadded portion before caching mm_state = multimodal_outputs[mm_out_key][:num_tokens_unpadded] mm_state = OmniTensorPrefixCache._coerce_to_cpu_tensor(mm_state) @@ -143,6 +163,165 @@ class OmniTensorPrefixCache: flat_cache[unpadded_slot_mapping] = mm_state logger.debug("Writing to mm output cache for %s tokens", num_tokens_unpadded) + def stage_deferred_mm_outputs( + self, + query_start_loc: torch.Tensor, + input_batch: InputBatch, + multimodal_outputs: dict[str, torch.Tensor] | None, + num_scheduled_tokens: dict[str, int], + deferred_mm_cache_keys: set[str], + ) -> None: + """Keep GPU multimodal chunks until a request finishes. + + The normal prefix-cache write path copies every cached multimodal tensor + to CPU on every step. For model outputs that are only needed by future + full-block prefix hits, we can keep detached GPU chunks and materialize + the CPU cache once when the request completes. + """ + if not multimodal_outputs or not deferred_mm_cache_keys: + return + + for mm_key in deferred_mm_cache_keys: + mm_state = multimodal_outputs.get(mm_key) + if not isinstance(mm_state, torch.Tensor) or mm_state.ndim < 2: + continue + for req_id in input_batch.req_ids: + req_idx = input_batch.req_id_to_index[req_id] + sched = int(num_scheduled_tokens.get(req_id, 0)) + if sched <= 0: + continue + start = int(query_start_loc[req_idx]) + end = start + sched + if start >= int(mm_state.shape[0]): + continue + end = min(end, int(mm_state.shape[0])) + if end <= start: + continue + + computed_end = int(input_batch.num_computed_tokens_cpu[req_idx]) + token_start = max(0, computed_end - sched) + chunk = mm_state[start:end].detach() + self._deferred_mm_outputs.setdefault(req_id, {}).setdefault(mm_key, []).append((token_start, chunk)) + + def commit_deferred_mm_outputs( + self, + finished_req_ids: set[str] | list[str], + input_batch: InputBatch, + ) -> None: + """Write deferred multimodal chunks into the CPU prefix cache. + + This must run before finished requests are removed from ``input_batch``, + because the block table is needed to map logical token positions to KV + cache slots. + """ + if not finished_req_ids or not self._deferred_mm_outputs: + return + + for req_id in tuple(finished_req_ids): + per_key = self._deferred_mm_outputs.pop(req_id, None) + if not per_key: + continue + req_idx = input_batch.req_id_to_index.get(req_id) + if req_idx is None: + continue + for mm_key, chunks in per_key.items(): + self._commit_deferred_mm_key(req_idx, input_batch, mm_key, chunks) + + def discard_deferred_mm_outputs(self, req_id: str) -> None: + """Drop deferred chunks for requests that leave without a cache commit.""" + self._deferred_mm_outputs.pop(req_id, None) + + def _commit_deferred_mm_key( + self, + req_idx: int, + input_batch: InputBatch, + mm_key: str, + chunks: list[tuple[int, torch.Tensor]], + ) -> None: + if not chunks: + return + + chunks = sorted(chunks, key=lambda item: item[0]) + first_start = chunks[0][0] + expected_start = first_start + tensors: list[torch.Tensor] = [] + for token_start, tensor in chunks: + if tensor.numel() == 0: + continue + if token_start != expected_start: + if tensors: + self._write_deferred_mm_tensor(req_idx, input_batch, mm_key, tensors, first_start) + first_start = token_start + expected_start = token_start + tensors = [] + tensors.append(tensor) + expected_start += int(tensor.shape[0]) + + if tensors: + self._write_deferred_mm_tensor(req_idx, input_batch, mm_key, tensors, first_start) + + def _write_deferred_mm_tensor( + self, + req_idx: int, + input_batch: InputBatch, + mm_key: str, + tensors: list[torch.Tensor], + token_start: int, + ) -> None: + mm_state = tensors[0] if len(tensors) == 1 else torch.cat(tensors, dim=0) + if mm_state.ndim < 2 or mm_state.shape[0] == 0: + return + + if mm_key not in self.mm_cache_keys: + self.mm_outputs_cache[mm_key] = self._get_cache_tensor( + dtype=mm_state.dtype, + hidden_size=int(mm_state.shape[-1]), + ) + self.mm_cache_keys.add(mm_key) + logger.info( + "Initializing deferred multimodal output cache of size %s for key: %s", + list(self.mm_outputs_cache[mm_key].shape), + mm_key, + ) + + slots = self._get_slot_ids_for_token_range( + req_idx=req_idx, + input_batch=input_batch, + token_start=token_start, + num_tokens=int(mm_state.shape[0]), + ) + if slots.numel() == 0: + return + + mm_state_cpu = OmniTensorPrefixCache._coerce_to_cpu_tensor(mm_state[: slots.numel()]) + mm_cache = self.mm_outputs_cache[mm_key] + flat_cache = mm_cache.view(-1, mm_cache.shape[-1]) + flat_cache[slots] = mm_state_cpu + + def _get_slot_ids_for_token_range( + self, + req_idx: int, + input_batch: InputBatch, + token_start: int, + num_tokens: int, + ) -> torch.Tensor: + if num_tokens <= 0: + return torch.empty((0,), dtype=torch.long) + + block_table = input_batch.block_table[0].block_table.cpu + token_positions = torch.arange(token_start, token_start + num_tokens, dtype=torch.long) + block_offsets = token_positions // self.block_size + max_blocks = int(block_table.shape[1]) + valid = block_offsets < max_blocks + if not bool(valid.all()): + token_positions = token_positions[valid] + block_offsets = block_offsets[valid] + if token_positions.numel() == 0: + return torch.empty((0,), dtype=torch.long) + + block_ids = block_table[req_idx, block_offsets].to(torch.long) + return block_ids * self.block_size + (token_positions % self.block_size) + def _coerce_to_payload_dict( self, element: object, @@ -151,17 +330,25 @@ class OmniTensorPrefixCache: num_scheduled_tokens: dict[str, int], ) -> dict[str, object]: """Build the multimodal passthrough data per request for - the object under consideration. This is identical to the case - for no prefix cache when we tensor does have a first dimension - matching the seq len. + the object under consideration. This mirrors the no-prefix-cache + path: a tensor whose first dimension matches the total scheduled + token count is sliced per request, so 1D (seq_len,) metadata that + is intentionally not cached (e.g. ref_code_len, codec_streaming) + is still split per request instead of leaking the whole batch. """ + total_scheduled_tokens = sum(int(num_scheduled_tokens[r]) for r in input_batch.req_ids) elem_dict = {} for req_id in input_batch.req_ids: req_idx = input_batch.req_id_to_index[req_id] start = query_start_loc[req_idx] end = start + num_scheduled_tokens[req_id] elem_dict[req_id] = to_payload_element( - element, req_idx, start=start, end=end, pass_lists_through=True, seq_len=None + element, + req_idx, + start=start, + end=end, + pass_lists_through=True, + seq_len=total_scheduled_tokens, ) return elem_dict diff --git a/vllm_omni/deploy/qwen3_tts.yaml b/vllm_omni/deploy/qwen3_tts.yaml index 0c976d090..f54cca6ef 100644 --- a/vllm_omni/deploy/qwen3_tts.yaml +++ b/vllm_omni/deploy/qwen3_tts.yaml @@ -34,7 +34,12 @@ stages: max_num_seqs: 10 gpu_memory_utilization: 0.3 trust_remote_code: true - enable_prefix_caching: false + # Prefix caching on by default. The HS opt-out (talker only consumes the + # last token's hidden state) and deferred mm-output writes added in this + # PR make per-step bookkeeping negligible, and any text repetition across + # requests (same voice or shared ref_audio) is a net throughput win. + # Disable explicitly if memory headroom is tight on a single-GPU deploy. + enable_prefix_caching: true async_scheduling: true max_num_batched_tokens: 512 max_model_len: 4096 diff --git a/vllm_omni/engine/orchestrator.py b/vllm_omni/engine/orchestrator.py index 27bb44ad7..ae0c82765 100644 --- a/vllm_omni/engine/orchestrator.py +++ b/vllm_omni/engine/orchestrator.py @@ -107,7 +107,7 @@ def build_engine_core_request_from_tokens( pooling_params=pooling_params, arrival_time=arrival_time, lora_request=getattr(params, "lora_request", None), - cache_salt=None, + cache_salt=prompt.get("cache_salt"), data_parallel_rank=None, prompt_embeds=prompt_embeds, resumable=resumable, diff --git a/vllm_omni/entrypoints/omni_base.py b/vllm_omni/entrypoints/omni_base.py index 7bd27be81..de4ef327c 100644 --- a/vllm_omni/entrypoints/omni_base.py +++ b/vllm_omni/entrypoints/omni_base.py @@ -277,6 +277,10 @@ class OmniBase(PDDisaggregationMixin): try: if req_state is None or req_state.metrics is None: return + if self.log_stats: + # Emit per-request orchestrator timing (including e2e_total_ms) + # before dropping request state. + req_state.metrics.build_and_log_summary() except Exception: logger.exception( "[%s] Failed to build/log summary for req=%s", diff --git a/vllm_omni/entrypoints/openai/serving_speech.py b/vllm_omni/entrypoints/openai/serving_speech.py index 10036978e..7643ed625 100644 --- a/vllm_omni/entrypoints/openai/serving_speech.py +++ b/vllm_omni/entrypoints/openai/serving_speech.py @@ -1,5 +1,6 @@ import asyncio import base64 +import hashlib import io import json import math @@ -186,6 +187,55 @@ def _validate_path_within_directory(file_path: Path, directory: Path) -> bool: return False +def _conditioning_cache_salt(request, tts_params: dict | None = None) -> str: + """Stable hash of the real Stage 0 conditioning for the prefix cache. + + The talker's vLLM prompt is placeholder token ids; the real inputs are + rebuilt from text / ref_audio / ref_text into inputs_embeds. vLLM hashes + token ids (folded with cache_salt) for prefix caching, so without a salt + every request collides and a hit could reuse KV from a semantically + different input. Tying the salt to the conditioning keeps a hit safe: + identical conditioning may share the prefix, any difference never does. + + Raw request fields alone are not enough for uploaded voices: the request + only carries the voice *name* (ref_audio/ref_text/task_type are resolved + from stored voice data into ``tts_params``). Delete + re-upload under the + same name leaves every raw field identical, so the resolved conditioning + must also be folded in. ``voice_created_at`` bumps on every (re-)upload, + which uniquely identifies the resolved reference artifact together with + the voice name; the decoded ref_audio array itself need not be hashed. + """ + h = hashlib.sha256() + for part in ( + request.input, + request.task_type, + request.language, + request.voice, + request.ref_text, + request.ref_audio, + request.instructions, + request.x_vector_only_mode, + request.speaker_embedding, + ): + h.update(b"\x00") + if part is not None: + h.update(repr(part).encode("utf-8")) + # Fold resolved conditioning that is auto-derived for uploaded voices and + # absent from the raw request. + for key in ( + "voice_created_at", + "task_type", + "speaker", + "ref_text", + "x_vector_only_mode", + ): + h.update(b"\x00") + value = tts_params.get(key) if tts_params is not None else None + if value is not None: + h.update(repr(value).encode("utf-8")) + return h.hexdigest()[:32] + + class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): _diffusion_mode: bool = False _tts_executor: ThreadPoolExecutor | None = None @@ -1500,6 +1550,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): request_id: str, response_format: str = "pcm", raw_request: Request | None = None, + request_start_s: float | None = None, ): """Generate audio chunks for streaming response. @@ -1520,6 +1571,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): prev_count = 0 sample_rate_val = 24000 first_chunk = True + first_audio_chunk_s: float | None = None + stream_start_s = request_start_s if request_start_s is not None else time.perf_counter() try: async for res in generator: @@ -1571,11 +1624,40 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): stream_format="audio", base64_encode=False, ) + if first_audio_chunk_s is None: + first_audio_chunk_s = time.perf_counter() yield self.create_audio(audio_obj).audio_data + total_ms = (time.perf_counter() - stream_start_s) * 1000.0 + if first_audio_chunk_s is not None: + first_chunk_ms = (first_audio_chunk_s - stream_start_s) * 1000.0 + logger.info( + "[SpeechE2E] request_id=%s stream=true status=ok total_ms=%.2f first_chunk_ms=%.2f", + request_id, + total_ms, + first_chunk_ms, + ) + else: + logger.info( + "[SpeechE2E] request_id=%s stream=true status=ok total_ms=%.2f first_chunk_ms=NA", + request_id, + total_ms, + ) except asyncio.CancelledError: + total_ms = (time.perf_counter() - stream_start_s) * 1000.0 + logger.info( + "[SpeechE2E] request_id=%s stream=true status=cancelled total_ms=%.2f", + request_id, + total_ms, + ) logger.info("Streaming request %s cancelled by client", request_id) raise except EngineDeadError as e: + total_ms = (time.perf_counter() - stream_start_s) * 1000.0 + logger.error( + "[SpeechE2E] request_id=%s stream=true status=engine_dead total_ms=%.2f", + request_id, + total_ms, + ) logger.error( "EngineDeadError during streaming speech for %s: %s", request_id, @@ -1589,6 +1671,13 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): ) raise except Exception as e: + total_ms = (time.perf_counter() - stream_start_s) * 1000.0 + logger.exception( + "[SpeechE2E] request_id=%s stream=true status=error total_ms=%.2f error=%s", + request_id, + total_ms, + e, + ) logger.exception("Streaming speech generation failed for %s: %s", request_id, e) raise @@ -2195,6 +2284,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): tts_params["seed"] = [sampling_params_list[0].seed] prompt = tokens_input(prompt_token_ids=[1]) prompt["additional_information"] = tts_params + prompt["cache_salt"] = _conditioning_cache_salt(request, tts_params) else: tts_params = self._build_tts_params(request) # Resolve ref_audio (explicit or auto-set for uploaded voices) @@ -2210,6 +2300,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): ph_len = await self._estimate_prompt_len_async(tts_params) prompt = tokens_input(prompt_token_ids=[1] * ph_len) prompt["additional_information"] = tts_params + prompt["cache_salt"] = _conditioning_cache_salt(request, tts_params) else: # Qwen omni models (Qwen3-Omni, Qwen2.5-Omni) use a "talker" # stage whose preprocess requires chat-templated tokens. The @@ -2651,6 +2742,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): return error_check_ret request_id = f"speech-{random_uuid()}" + request_start_s = time.perf_counter() if raw_request: raw_request.state.request_metadata = RequestResponseMetadata( request_id=request_id, @@ -2683,20 +2775,58 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): request_id, response_format, raw_request=raw_request, + request_start_s=request_start_s, ), media_type=media_type, ) audio_bytes, media_type = await self._generate_audio_bytes(request, request_id=request_id) + total_ms = (time.perf_counter() - request_start_s) * 1000.0 + logger.info( + "[SpeechE2E] request_id=%s stream=false status=ok total_ms=%.2f response_bytes=%d", + request_id, + total_ms, + len(audio_bytes) if isinstance(audio_bytes, (bytes, bytearray)) else len(str(audio_bytes)), + ) return Response(content=audio_bytes, media_type=media_type) except asyncio.CancelledError: + total_ms = (time.perf_counter() - request_start_s) * 1000.0 + logger.info( + "[SpeechE2E] request_id=%s stream=%s status=cancelled total_ms=%.2f", + request_id, + bool(request.stream), + total_ms, + ) return self.create_error_response("Client disconnected") except (EngineGenerateError, EngineDeadError): + total_ms = (time.perf_counter() - request_start_s) * 1000.0 + logger.error( + "[SpeechE2E] request_id=%s stream=%s status=engine_error total_ms=%.2f", + request_id, + bool(request.stream), + total_ms, + ) raise # Propagate to the global Omni exception handler except ValueError as e: + total_ms = (time.perf_counter() - request_start_s) * 1000.0 + logger.warning( + "[SpeechE2E] request_id=%s stream=%s status=bad_request total_ms=%.2f error=%s", + request_id, + bool(request.stream), + total_ms, + e, + ) return self.create_error_response(e) except Exception as e: + total_ms = (time.perf_counter() - request_start_s) * 1000.0 + logger.exception( + "[SpeechE2E] request_id=%s stream=%s status=error total_ms=%.2f error=%s", + request_id, + bool(request.stream), + total_ms, + e, + ) logger.exception("Speech generation failed: %s", e) return self.create_error_response(f"Speech generation failed: {e}") diff --git a/vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_talker.py b/vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_talker.py index a705a1cd4..470ad34d5 100644 --- a/vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_talker.py +++ b/vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_talker.py @@ -2,8 +2,10 @@ from __future__ import annotations import base64 import copy +import hashlib import io import os +from collections import OrderedDict from collections.abc import Callable, Iterable, Mapping from typing import Any from urllib.parse import urlparse @@ -40,6 +42,7 @@ logger = init_logger(__name__) _TRAILING_TEXT_COMPACT_MIN_FRAMES = 64 _PRECOMPUTED_REF_CODE_KEY = "precomputed_ref" _NORMALIZED_REF_AUDIO_KEY = "_qwen3_tts_normalized_ref_audio" +_REF_AUDIO_CACHE_KEY = "_qwen3_tts_ref_audio_cache_key" _PRECOMPUTED_TEXT_IDS_KEY = "_qwen3_tts_text_ids" _PRECOMPUTED_REF_IDS_KEY = "_qwen3_tts_ref_ids" @@ -354,6 +357,17 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module): self.have_multimodal_outputs = True self.has_preprocess = True self.has_postprocess = True + # Qwen3-TTS postprocess() only reads hidden_states[-1, :]. On a prefix- + # cache hit, the last hidden state is in the newly computed tail, so + # reconstructing the full cached_prefix + new_tail span is wasted work. + # Opt out of the per-step GPU->CPU hidden-state cache write and merged- + # tensor read; postprocess receives the tail-only slice instead, which + # avoids ~18 ms merge + ~6 ms write per step (Sy0307 profile, #3665). + self.requires_full_prefix_cached_hidden_states = False + # ``codes.audio`` is only needed for future prefix-hit reconstruction + # after a request has produced codec rows. Keep per-step rows on GPU and + # materialize the CPU OmniTensorPrefixCache entry once at completion. + self.deferred_prefix_cache_mm_keys = {"codes.audio"} # Used by OmniGPUModelRunner for the GPU-side MTP fast-path. self.mtp_hidden_size = int(self.talker_config.hidden_size) # OmniGPUModelRunner will store talker_mtp output under this key in @@ -443,6 +457,8 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module): self._speech_tokenizer: Qwen3TTSTokenizer | None = None self._speaker_cache = get_speaker_cache() + self._ref_audio_artifact_cache_max_entries = 256 + self._ref_audio_artifact_cache: OrderedDict[str, dict[str, torch.Tensor | bool]] = OrderedDict() raw_subtalker_sampling = getattr(vllm_config.model_config, "subtalker_sampling_params", None) self._subtalker_sampling_params: dict[str, Any] = ( dict(raw_subtalker_sampling) if isinstance(raw_subtalker_sampling, Mapping) else {} @@ -634,9 +650,11 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module): info_update["meta"]["ref_code_len"] = int(ref_code_len) # Always return a span_len slice; if the scheduled placeholder is longer, pad with tts_pad_embed. # This preserves placeholder/embedding alignment. - offset = 0 - s = 0 - e = span_len + offset = int(info_dict.get("_omni_num_computed_tokens", 0) or 0) + if offset < 0: + offset = 0 + s = max(0, min(offset, int(prompt_embeds_cpu.shape[0]))) + e = max(0, min(offset + span_len, int(prompt_embeds_cpu.shape[0]))) take = prompt_embeds_cpu[s:e] if int(take.shape[0]) < span_len: pad_n = int(span_len - int(take.shape[0])) @@ -1393,9 +1411,50 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module): # 12Hz: likely [T, Q] or [B, T, Q] if ref_code.ndim == 3: ref_code = ref_code[0] - return ref_code.to(device=next(self.parameters()).device, dtype=torch.long) + out = ref_code.to(device=next(self.parameters()).device, dtype=torch.long) + return out raise ValueError("SpeechTokenizer.encode did not return audio_codes tensor") + @staticmethod + def _make_ref_audio_cache_key(wav: np.ndarray, sr: int) -> str: + wav_f32 = wav.astype(np.float32, copy=False).reshape(-1) + h = hashlib.sha1() + h.update(int(sr).to_bytes(4, byteorder="little", signed=False)) + h.update(int(wav_f32.size).to_bytes(8, byteorder="little", signed=False)) + h.update(wav_f32.tobytes(order="C")) + return h.hexdigest() + + def _get_ref_audio_artifacts(self, cache_key: str) -> dict[str, torch.Tensor | bool] | None: + entry = self._ref_audio_artifact_cache.get(cache_key) + if entry is None: + return None + # Touch for LRU. + self._ref_audio_artifact_cache.move_to_end(cache_key, last=True) + return entry + + def _put_ref_audio_artifacts( + self, + cache_key: str, + *, + ref_code: torch.Tensor | None = None, + ref_spk_embedding: torch.Tensor | None = None, + ) -> None: + if not cache_key: + return + entry = self._ref_audio_artifact_cache.get(cache_key) + if entry is None: + entry = {} + if isinstance(ref_code, torch.Tensor): + entry["ref_code"] = ref_code.detach().to("cpu", dtype=torch.long).contiguous() + if isinstance(ref_spk_embedding, torch.Tensor): + entry["ref_spk_embedding"] = ref_spk_embedding.detach().to("cpu", dtype=torch.bfloat16).reshape(-1) + if not entry: + return + self._ref_audio_artifact_cache[cache_key] = entry + self._ref_audio_artifact_cache.move_to_end(cache_key, last=True) + while len(self._ref_audio_artifact_cache) > self._ref_audio_artifact_cache_max_entries: + self._ref_audio_artifact_cache.popitem(last=False) + @staticmethod def _first_value(value: object, default: object = None) -> object: if isinstance(value, list): @@ -1544,6 +1603,14 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module): # invalid ref_audio errors with its existing messages. continue info_dict[_NORMALIZED_REF_AUDIO_KEY] = (wav, sr) + cache_key = self._make_ref_audio_cache_key(wav, sr) + info_dict[_REF_AUDIO_CACHE_KEY] = cache_key + cached = self._get_ref_audio_artifacts(cache_key) + if cached is not None: + cached_ref_code = self._coerce_ref_code_tensor(cached.get("ref_code"), device=device) + if isinstance(cached_ref_code, torch.Tensor): + info_dict.setdefault("codes", {})[_PRECOMPUTED_REF_CODE_KEY] = cached_ref_code + continue groups.setdefault(int(sr), []).append((info_dict, wav, int(sr))) if pending_text or pending_ref_text: @@ -1587,6 +1654,9 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module): continue info_dict.setdefault("codes", {})[_PRECOMPUTED_REF_CODE_KEY] = ref_code_t info_dict[_NORMALIZED_REF_AUDIO_KEY] = (wav, item_sr) + cache_key = info_dict.get(_REF_AUDIO_CACHE_KEY) + if isinstance(cache_key, str) and cache_key: + self._put_ref_audio_artifacts(cache_key, ref_code=ref_code_t) def _generate_icl_prompt( self, @@ -1773,6 +1843,7 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module): voice_clone_prompt = _normalize_voice_clone_prompt(info_dict.get("voice_clone_prompt")) ref_audio_wav: np.ndarray | None = None ref_audio_sr: int | None = None + ref_audio_cache_key: str | None = None def _get_ref_audio() -> tuple[np.ndarray, int]: nonlocal ref_audio_wav, ref_audio_sr @@ -1792,6 +1863,32 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module): ref_audio_wav, ref_audio_sr = self._normalize_ref_audio(ref_audio_list[0]) return ref_audio_wav, ref_audio_sr + def _get_ref_audio_cache_key() -> str: + nonlocal ref_audio_cache_key + if isinstance(ref_audio_cache_key, str) and ref_audio_cache_key: + return ref_audio_cache_key + cached_key = info_dict.get(_REF_AUDIO_CACHE_KEY) + if isinstance(cached_key, str) and cached_key: + ref_audio_cache_key = cached_key + return ref_audio_cache_key + wav_np, sr = _get_ref_audio() + ref_audio_cache_key = self._make_ref_audio_cache_key(wav_np, sr) + info_dict[_REF_AUDIO_CACHE_KEY] = ref_audio_cache_key + return ref_audio_cache_key + + def _try_get_ref_audio_cache_key() -> str | None: + nonlocal ref_audio_cache_key + if isinstance(ref_audio_cache_key, str) and ref_audio_cache_key: + return ref_audio_cache_key + cached_key = info_dict.get(_REF_AUDIO_CACHE_KEY) + if isinstance(cached_key, str) and cached_key: + ref_audio_cache_key = cached_key + return ref_audio_cache_key + ref_audio_list = info_dict.get("ref_audio") + if isinstance(ref_audio_list, list) and ref_audio_list: + return _get_ref_audio_cache_key() + return None + # Speaker cache: only for uploaded (named) speakers _speaker_cache_key = None if voice_clone_prompt is None: @@ -1849,12 +1946,24 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module): if isinstance(ref_code_t, torch.Tensor): ref_code_len = int(ref_code_t.shape[0]) if ref_code_t is None and in_context_mode: - # Compute ref_code from ref_audio if not provided. - wav_np, sr = _get_ref_audio() - ref_code_t = self._encode_ref_audio_to_code(wav_np, sr).to(device=input_ids.device) - ref_code_len = int(ref_code_t.shape[0]) + cache_key = _get_ref_audio_cache_key() + cached = self._get_ref_audio_artifacts(cache_key) + cached_ref_code = None + if isinstance(cached, dict): + cached_ref_code = cached.get("ref_code") + ref_code_t = self._coerce_ref_code_tensor(cached_ref_code, device=input_ids.device) + if isinstance(ref_code_t, torch.Tensor): + ref_code_len = int(ref_code_t.shape[0]) + else: + wav_np, sr = _get_ref_audio() + ref_code_t = self._encode_ref_audio_to_code(wav_np, sr).to(device=input_ids.device) + ref_code_len = int(ref_code_t.shape[0]) + self._put_ref_audio_artifacts(cache_key, ref_code=ref_code_t) if isinstance(ref_code_t, torch.Tensor): ref_code_prompt = ref_code_t + cache_key = _try_get_ref_audio_cache_key() + if isinstance(cache_key, str) and cache_key: + self._put_ref_audio_artifacts(cache_key, ref_code=ref_code_prompt) # Speaker embedding: use prompt embed if provided; otherwise extract from audio. # NOTE: Do NOT use _as_singleton here — the embedding may be a plain @@ -1869,8 +1978,17 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module): # Plain list/array from API (survived msgspec IPC serialization). speaker_embed = torch.tensor(spk, dtype=torch.bfloat16, device=input_ids.device).view(1, 1, -1) else: - wav_np, sr = _get_ref_audio() - speaker_embed = self._extract_speaker_embedding(wav_np, sr).view(1, 1, -1) + cache_key = _get_ref_audio_cache_key() + cached = self._get_ref_audio_artifacts(cache_key) + cached_spk = None + if isinstance(cached, dict): + cached_spk = cached.get("ref_spk_embedding") + if isinstance(cached_spk, torch.Tensor): + speaker_embed = cached_spk.to(device=input_ids.device, dtype=torch.bfloat16).view(1, 1, -1) + else: + wav_np, sr = _get_ref_audio() + speaker_embed = self._extract_speaker_embedding(wav_np, sr).view(1, 1, -1) + self._put_ref_audio_artifacts(cache_key, ref_spk_embedding=speaker_embed) # Cache miss: store extraction result if _speaker_cache_key is not None and speaker_embed is not None: diff --git a/vllm_omni/platforms/npu/worker/npu_ar_model_runner.py b/vllm_omni/platforms/npu/worker/npu_ar_model_runner.py index 8d058335e..3a8556273 100644 --- a/vllm_omni/platforms/npu/worker/npu_ar_model_runner.py +++ b/vllm_omni/platforms/npu/worker/npu_ar_model_runner.py @@ -155,6 +155,11 @@ class NPUARModelRunner(OmniNPUModelRunner): finally: set_cudagraph_capturing_enabled(False) + def _model_needs_full_prefix_hidden_states(self) -> bool: + """See gpu_ar_model_runner._model_needs_full_prefix_hidden_states.""" + model = getattr(self, "model", None) + return bool(getattr(model, "requires_full_prefix_cached_hidden_states", True)) + def _maybe_update_prefix_cache( self, hidden_states: torch.Tensor, @@ -169,8 +174,9 @@ class NPUARModelRunner(OmniNPUModelRunner): type(multimodal_outputs), ) + hs_for_cache = hidden_states if self._model_needs_full_prefix_hidden_states() else None self.omni_prefix_cache.update_omni_tensor_prefix_cache( - hidden_states=hidden_states, + hidden_states=hs_for_cache, multimodal_outputs=flatten_payload(multimodal_outputs) if multimodal_outputs else multimodal_outputs, num_tokens_unpadded=num_tokens_unpadded, slot_mapping=self.input_batch.block_table[0].slot_mapping.cpu, @@ -185,12 +191,13 @@ class NPUARModelRunner(OmniNPUModelRunner): ) -> tuple[dict[str, torch.Tensor] | None, dict | None]: combined_hidden_states, combined_multimodal_outputs = None, None if self.omni_prefix_cache is not None: - combined_hidden_states = self.omni_prefix_cache.get_merged_hidden_states( - query_start_loc=self.query_start_loc.cpu, - input_batch=self.input_batch, - hidden_states=hidden_states, - num_scheduled_tokens=num_scheduled_tokens, - ) + if self._model_needs_full_prefix_hidden_states(): + combined_hidden_states = self.omni_prefix_cache.get_merged_hidden_states( + query_start_loc=self.query_start_loc.cpu, + input_batch=self.input_batch, + hidden_states=hidden_states, + num_scheduled_tokens=num_scheduled_tokens, + ) combined_multimodal_outputs = self.omni_prefix_cache.get_merged_multimodal_states( query_start_loc=self.query_start_loc.cpu, input_batch=self.input_batch, diff --git a/vllm_omni/utils/mm_outputs.py b/vllm_omni/utils/mm_outputs.py index cf2837a7c..43e8fa110 100644 --- a/vllm_omni/utils/mm_outputs.py +++ b/vllm_omni/utils/mm_outputs.py @@ -68,12 +68,13 @@ def to_payload_element( if we need to avoid splitting nonempty lists prior to calling postprocess, which is the case for prefix cache. seq_len: Optional sequence length (i.e., dim 0 of hidden states). - This should be set to None in the prefix caching case, because - the condition that would be executed here is the same as the - criteria for being added to the multimodal outputs cache. + When set, a tensor whose first dimension equals seq_len is + sliced per request. The prefix cache passthrough also passes + the total scheduled token count here so 1D (seq_len,) metadata + that is intentionally not cached is still split per request. """ - # Prefix cache won't hit this case because this is the condition - # for being a mm_cache_key in the multimodal outputs tensor. + # Cached per-token tensors are merged elsewhere; here a first dim + # equal to seq_len means a per-request slice is required. if seq_len is not None and isinstance(element, torch.Tensor) and element.shape[0] == seq_len: return element[start:end].contiguous() # Every other case is shared between prefix cache (passthrough data) diff --git a/vllm_omni/worker/gpu_ar_model_runner.py b/vllm_omni/worker/gpu_ar_model_runner.py index 34d571c11..b3fa99fc5 100644 --- a/vllm_omni/worker/gpu_ar_model_runner.py +++ b/vllm_omni/worker/gpu_ar_model_runner.py @@ -203,6 +203,24 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin): finally: set_cudagraph_capturing_enabled(False) + def _model_needs_full_prefix_hidden_states(self) -> bool: + """Opt-out hook for models whose postprocess only consumes the tail. + + When False, we skip both the per-step GPU->CPU hidden-state write into + OmniTensorPrefixCache and the merged-tensor reconstruction on hits; + postprocess receives the normal scheduled-token slice instead. Models + that need the full cached_prefix + new_tail span (default) are not + affected. + """ + model = getattr(self, "model", None) + return bool(getattr(model, "requires_full_prefix_cached_hidden_states", True)) + + def _deferred_prefix_cache_mm_keys(self) -> set[str]: + """Model-declared multimodal keys whose prefix-cache writes are deferred.""" + model = getattr(self, "model", None) + keys = getattr(model, "deferred_prefix_cache_mm_keys", ()) + return set(keys or ()) + def _maybe_update_prefix_cache( self, hidden_states: torch.Tensor, @@ -225,12 +243,14 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin): type(multimodal_outputs), ) + hs_for_cache = hidden_states if self._model_needs_full_prefix_hidden_states() else None self.omni_prefix_cache.update_omni_tensor_prefix_cache( - hidden_states=hidden_states, + hidden_states=hs_for_cache, multimodal_outputs=flatten_payload(multimodal_outputs) if multimodal_outputs else multimodal_outputs, num_tokens_unpadded=num_tokens_unpadded, slot_mapping=self.input_batch.block_table[0].slot_mapping.cpu, num_tokens_padded=num_tokens_padded, + skip_mm_cache_keys=self._deferred_prefix_cache_mm_keys(), ) def _maybe_get_combined_prefix_cache_tensors( @@ -246,12 +266,18 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin): # the prefix cached hidden states and multimodal states. combined_hidden_states, combined_multimodal_outputs = None, None if self.omni_prefix_cache is not None: - combined_hidden_states = self.omni_prefix_cache.get_merged_hidden_states( - query_start_loc=self.query_start_loc.cpu, - input_batch=self.input_batch, - hidden_states=hidden_states, - num_scheduled_tokens=num_scheduled_tokens, - ) + if ( + not self._model_needs_full_prefix_hidden_states() + and not self.omni_prefix_cache.has_prefix_cached_new_req_ids() + ): + return None, None + if self._model_needs_full_prefix_hidden_states(): + combined_hidden_states = self.omni_prefix_cache.get_merged_hidden_states( + query_start_loc=self.query_start_loc.cpu, + input_batch=self.input_batch, + hidden_states=hidden_states, + num_scheduled_tokens=num_scheduled_tokens, + ) combined_multimodal_outputs = self.omni_prefix_cache.get_merged_multimodal_states( query_start_loc=self.query_start_loc.cpu, input_batch=self.input_batch, @@ -313,6 +339,12 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin): if flush_ids: self.flush_full_payload_outputs(flush_ids) + if self.omni_prefix_cache is not None and scheduler_output.finished_req_ids: + self.omni_prefix_cache.commit_deferred_mm_outputs( + set(scheduler_output.finished_req_ids), + self.input_batch, + ) + if self.routed_experts_initialized: capturer = get_global_experts_capturer() if capturer is not None and hasattr(capturer, "finalize_pending_copy"): @@ -884,9 +916,22 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin): dtype=np.int32, ) query_start_loc_cpu = self.query_start_loc.cpu + if self.omni_prefix_cache is not None: + deferred_mm_cache_keys = self._deferred_prefix_cache_mm_keys() + if deferred_mm_cache_keys: + self.omni_prefix_cache.stage_deferred_mm_outputs( + query_start_loc=query_start_loc_cpu, + input_batch=self.input_batch, + multimodal_outputs=( + flatten_payload(multimodal_outputs) if multimodal_outputs else multimodal_outputs + ), + num_scheduled_tokens=scheduler_output.num_scheduled_tokens, + deferred_mm_cache_keys=deferred_mm_cache_keys, + ) pooler_output: list[dict[str, object]] | None = None if needs_pooler_payload: + mm_cpu = None # Prior to applying the post-processing func, extract # the prefix cached hidden states and multimodal states. if self.omni_prefix_cache is not None: @@ -899,8 +944,8 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin): scheduler_output.num_scheduled_tokens, ) # Otherwise we don't have the mm CPU data yet, so we still need to build it - if self.omni_prefix_cache is None: - mm_cpu = build_mm_cpu(flatten_payload(multimodal_outputs)) + if self.omni_prefix_cache is None or combined_multimodal_outputs is None: + mm_cpu = build_mm_cpu(flatten_payload(multimodal_outputs) if multimodal_outputs else multimodal_outputs) self._process_additional_information_updates( hidden_states, diff --git a/vllm_omni/worker/gpu_model_runner.py b/vllm_omni/worker/gpu_model_runner.py index 5f76f7be1..9ba0b5f7b 100644 --- a/vllm_omni/worker/gpu_model_runner.py +++ b/vllm_omni/worker/gpu_model_runner.py @@ -332,6 +332,8 @@ class OmniGPUModelRunner(GPUModelRunner): self.requests.pop(req_id, None) self.model_intermediate_buffer.pop(req_id, None) self.num_prompt_logprobs.pop(req_id, None) + if self.omni_prefix_cache is not None: + self.omni_prefix_cache.discard_deferred_mm_outputs(req_id) if hasattr(self, "_downstream_payload_cache"): self._downstream_payload_cache.pop(req_id, None) if hasattr(self, "_talker_mtp_generators"): From 80dea88007fdf3370d3e7266c31b703b3491decd Mon Sep 17 00:00:00 2001 From: "rongfu.leng" Date: Mon, 25 May 2026 23:13:38 +0800 Subject: [PATCH 025/112] [Bugfix] fix when diffusion model not set sleeping_stages (#3023) Signed-off-by: rongfu.leng thanks --- .../openai_api/test_omni_sleep_wakeup.py | 289 ++++++++++++++++++ vllm_omni/entrypoints/openai/api_server.py | 2 +- 2 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 tests/entrypoints/openai_api/test_omni_sleep_wakeup.py diff --git a/tests/entrypoints/openai_api/test_omni_sleep_wakeup.py b/tests/entrypoints/openai_api/test_omni_sleep_wakeup.py new file mode 100644 index 000000000..cf7d6e234 --- /dev/null +++ b/tests/entrypoints/openai_api/test_omni_sleep_wakeup.py @@ -0,0 +1,289 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +import dataclasses + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + + +@dataclasses.dataclass +class FakeAck: + stage_id: int + result: str = "ok" + + +def _make_app(engine_client): + from vllm_omni.entrypoints.openai.api_server import router + + app = FastAPI() + app.include_router(router) + app.state.engine_client = engine_client + app.state.sleeping_stages = set() + return app + + +@pytest.fixture +def sleep_capable_engine(mocker): + engine = mocker.MagicMock() + engine.sleep = mocker.AsyncMock(return_value=[FakeAck(stage_id=0), FakeAck(stage_id=1)]) + engine.wake_up = mocker.AsyncMock(return_value=[FakeAck(stage_id=0), FakeAck(stage_id=1)]) + return engine + + +@pytest.fixture +def sleep_incapable_engine(mocker): + return mocker.MagicMock(spec=[]) # no 'sleep' or 'wake_up' attributes + + +# --------------------------------------------------------------------------- +# /v1/omni/sleep +# --------------------------------------------------------------------------- + + +def test_sleep_success(sleep_capable_engine): + app = _make_app(sleep_capable_engine) + client = TestClient(app) + + response = client.post("/v1/omni/sleep", json={"stage_ids": [0, 1], "level": 2}) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "SUCCESS" + assert [ack["stage_id"] for ack in data["acks"]] == [0, 1] + assert app.state.sleeping_stages == {0, 1} + sleep_capable_engine.sleep.assert_awaited_once_with(stage_ids=[0, 1], level=2) + + +def test_sleep_default_level(sleep_capable_engine): + app = _make_app(sleep_capable_engine) + client = TestClient(app) + + response = client.post("/v1/omni/sleep", json={"stage_ids": [0]}) + + assert response.status_code == 200 + sleep_capable_engine.sleep.assert_awaited_once_with(stage_ids=[0], level=2) + + +def test_sleep_empty_stage_ids(sleep_capable_engine, mocker): + """Empty stage_ids: engine is still called and returns SUCCESS with no acks.""" + sleep_capable_engine.sleep = mocker.AsyncMock(return_value=[]) + app = _make_app(sleep_capable_engine) + client = TestClient(app) + + response = client.post("/v1/omni/sleep", json={"stage_ids": [], "level": 2}) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "SUCCESS" + assert data["acks"] == [] + assert app.state.sleeping_stages == set() + sleep_capable_engine.sleep.assert_awaited_once_with(stage_ids=[], level=2) + + +def test_sleep_updates_sleeping_set(sleep_capable_engine): + app = _make_app(sleep_capable_engine) + app.state.sleeping_stages = {5} # pre-existing entry + client = TestClient(app) + + client.post("/v1/omni/sleep", json={"stage_ids": [0, 1], "level": 2}) + + assert app.state.sleeping_stages == {5, 0, 1} + + +def test_sleep_engine_not_support(sleep_incapable_engine): + app = _make_app(sleep_incapable_engine) + client = TestClient(app) + + response = client.post("/v1/omni/sleep", json={"stage_ids": [0], "level": 1}) + + assert response.status_code == 501 + assert "sleep" in response.json()["detail"] + + +# --------------------------------------------------------------------------- +# /v1/omni/wakeup +# --------------------------------------------------------------------------- + + +def test_wakeup_success(sleep_capable_engine): + app = _make_app(sleep_capable_engine) + app.state.sleeping_stages = {0, 1} + client = TestClient(app) + + response = client.post("/v1/omni/wakeup", json={"stage_ids": [0, 1]}) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "SUCCESS" + assert [ack["stage_id"] for ack in data["acks"]] == [0, 1] + assert app.state.sleeping_stages == set() + sleep_capable_engine.wake_up.assert_awaited_once_with(stage_ids=[0, 1]) + + +def test_wakeup_skipped_when_not_sleeping(sleep_capable_engine): + app = _make_app(sleep_capable_engine) + app.state.sleeping_stages = set() # nothing sleeping + client = TestClient(app) + + response = client.post("/v1/omni/wakeup", json={"stage_ids": [0, 1]}) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "SKIPPED" + sleep_capable_engine.wake_up.assert_not_awaited() + + +def test_wakeup_partial_sleeping(sleep_capable_engine): + """Only stage 0 is sleeping; stage 1 is not. Should still proceed (partial match).""" + app = _make_app(sleep_capable_engine) + app.state.sleeping_stages = {0} + client = TestClient(app) + + response = client.post("/v1/omni/wakeup", json={"stage_ids": [0, 1]}) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "SUCCESS" + assert 0 not in app.state.sleeping_stages + + +def test_wakeup_empty_stage_ids(sleep_capable_engine): + """Empty stage_ids: any() on empty iterable is False → SKIPPED, engine not called.""" + app = _make_app(sleep_capable_engine) + app.state.sleeping_stages = {0, 1} + client = TestClient(app) + + response = client.post("/v1/omni/wakeup", json={"stage_ids": []}) + + assert response.status_code == 200 + assert response.json()["status"] == "SKIPPED" + sleep_capable_engine.wake_up.assert_not_awaited() + + +def test_wakeup_engine_not_support(sleep_incapable_engine): + app = _make_app(sleep_incapable_engine) + app.state.sleeping_stages = {0} + client = TestClient(app) + + response = client.post("/v1/omni/wakeup", json={"stage_ids": [0]}) + + assert response.status_code == 501 + assert "wake_up" in response.json()["detail"] + + +def test_wakeup_removes_only_requested_stages(sleep_capable_engine): + app = _make_app(sleep_capable_engine) + app.state.sleeping_stages = {0, 1, 2} + client = TestClient(app) + + client.post("/v1/omni/wakeup", json={"stage_ids": [0]}) + + assert app.state.sleeping_stages == {1, 2} + + +def test_sleep_then_wakeup_roundtrip(sleep_capable_engine): + app = _make_app(sleep_capable_engine) + client = TestClient(app) + + sleep_resp = client.post("/v1/omni/sleep", json={"stage_ids": [0, 1], "level": 2}) + assert sleep_resp.status_code == 200 + assert app.state.sleeping_stages == {0, 1} + + wakeup_resp = client.post("/v1/omni/wakeup", json={"stage_ids": [0, 1]}) + assert wakeup_resp.status_code == 200 + assert app.state.sleeping_stages == set() + + +# --------------------------------------------------------------------------- +# pure-diffusion branch: omni_init_app_state initialises sleeping_stages +# --------------------------------------------------------------------------- + + +@pytest.fixture +def pure_diffusion_engine(mocker): + engine = mocker.MagicMock() + engine.stage_configs = [{"stage_type": "diffusion"}] + engine.sleep = mocker.AsyncMock(return_value=[FakeAck(stage_id=0)]) + engine.wake_up = mocker.AsyncMock(return_value=[FakeAck(stage_id=0)]) + # Remove attributes that would make _get_vllm_config return a config + del engine.get_vllm_config + del engine.vllm_config + return engine + + +@pytest.fixture +def pure_diffusion_app(pure_diffusion_engine, mocker): + """App whose state was initialised via omni_init_app_state (pure diffusion path).""" + import asyncio + from argparse import Namespace + + from vllm_omni.entrypoints.openai.api_server import omni_init_app_state, router + + args = Namespace( + served_model_name=None, + model="fake-diffusion-model", + enable_log_requests=False, + disable_log_stats=True, + max_log_len=0, + enable_server_load_tracking=False, + ) + + app = FastAPI() + app.include_router(router) + + mocker.patch( + "vllm_omni.entrypoints.openai.api_server.OmniOpenAIServingChat" + ).for_diffusion.return_value = mocker.MagicMock() + mocker.patch( + "vllm_omni.entrypoints.openai.api_server.OmniOpenAIServingVideo" + ).for_diffusion.return_value = mocker.MagicMock() + mocker.patch( + "vllm_omni.entrypoints.openai.api_server.OmniOpenAIServingSpeech" + ).for_diffusion.return_value = mocker.MagicMock() + mocker.patch("vllm_omni.entrypoints.openai.api_server._DiffusionServingModels") + + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(omni_init_app_state(pure_diffusion_engine, app.state, args)) + finally: + loop.close() + + return app + + +def test_pure_diffusion_sleep_success(pure_diffusion_app, pure_diffusion_engine): + client = TestClient(pure_diffusion_app) + + response = client.post("/v1/omni/sleep", json={"stage_ids": [0], "level": 2}) + + assert response.status_code == 200 + assert response.json()["status"] == "SUCCESS" + assert pure_diffusion_app.state.sleeping_stages == {0} + pure_diffusion_engine.sleep.assert_awaited_once_with(stage_ids=[0], level=2) + + +def test_pure_diffusion_wakeup_skipped(pure_diffusion_app, pure_diffusion_engine): + # sleeping_stages is empty after init — nothing is sleeping + client = TestClient(pure_diffusion_app) + + response = client.post("/v1/omni/wakeup", json={"stage_ids": [0]}) + + assert response.status_code == 200 + assert response.json()["status"] == "SKIPPED" + pure_diffusion_engine.wake_up.assert_not_awaited() + + +def test_pure_diffusion_sleep_then_wakeup(pure_diffusion_app, pure_diffusion_engine): + client = TestClient(pure_diffusion_app) + + sleep_resp = client.post("/v1/omni/sleep", json={"stage_ids": [0], "level": 2}) + assert sleep_resp.status_code == 200 + assert pure_diffusion_app.state.sleeping_stages == {0} + + wakeup_resp = client.post("/v1/omni/wakeup", json={"stage_ids": [0]}) + assert wakeup_resp.status_code == 200 + assert pure_diffusion_app.state.sleeping_stages == set() diff --git a/vllm_omni/entrypoints/openai/api_server.py b/vllm_omni/entrypoints/openai/api_server.py index 29788d958..69a6dd603 100644 --- a/vllm_omni/entrypoints/openai/api_server.py +++ b/vllm_omni/entrypoints/openai/api_server.py @@ -586,6 +586,7 @@ async def omni_init_app_state( state.engine_client = engine_client state.log_stats = not args.disable_log_stats state.args = args + state.sleeping_stages = set() # For omni models state.stage_configs = engine_client.stage_configs if hasattr(engine_client, "stage_configs") else None @@ -949,7 +950,6 @@ async def omni_init_app_state( state.enable_server_load_tracking = args.enable_server_load_tracking state.server_load_metrics = 0 - state.sleeping_stages = set() def Omnivideo(request: Request) -> OmniOpenAIServingVideo | None: From 8f45e68bcd4b5beb44c45ddbb576e59ea9c7a741 Mon Sep 17 00:00:00 2001 From: Yuekai Zhang Date: Mon, 25 May 2026 23:16:07 +0800 Subject: [PATCH 026/112] [Higgs-Audio] bosonai/higgs-audio-v2-generation-3B-base TTS model support (#3762) Signed-off-by: Yuekai Zhang Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com> --- .buildkite/test-merge.yml | 25 + .readthedocs.yml | 5 + .../offline_inference/text_to_speech.md | 44 + .../examples/online_serving/text_to_speech.md | 47 + .../text_to_speech/higgs_audio_v2/README.md | 38 + .../text_to_speech/higgs_audio_v2/end2end.py | 205 ++ .../text_to_speech/higgs_audio_v2/README.md | 63 + .../higgs_audio_v2/batch_speech_client.py | 122 ++ .../higgs_audio_v2/run_server.sh | 37 + setup.py | 6 + .../offline_inference/test_higgs_audio_v2.py | 231 +++ .../e2e/online_serving/test_higgs_audio_v2.py | 299 +++ vllm_omni/config/pipeline_registry.py | 4 + vllm_omni/deploy/higgs_audio_v2.yaml | 83 + .../entrypoints/openai/protocol/audio.py | 44 + .../entrypoints/openai/serving_speech.py | 189 +- .../models/higgs_audio_v2/__init__.py | 9 + .../configuration_higgs_audio_v2.py | 169 ++ .../higgs_audio_v2/higgs_audio_decoder.py | 471 +++++ .../higgs_audio_v2/higgs_audio_v2_code2wav.py | 413 ++++ .../higgs_audio_v2/higgs_audio_v2_talker.py | 1684 +++++++++++++++++ .../higgs_audio_v2_tokenizer.py | 332 ++++ .../models/higgs_audio_v2/pipeline.py | 49 + vllm_omni/model_executor/models/registry.py | 16 + .../stage_input_processors/higgs_audio_v2.py | 252 +++ 25 files changed, 4835 insertions(+), 2 deletions(-) create mode 100644 examples/offline_inference/text_to_speech/higgs_audio_v2/README.md create mode 100644 examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py create mode 100644 examples/online_serving/text_to_speech/higgs_audio_v2/README.md create mode 100644 examples/online_serving/text_to_speech/higgs_audio_v2/batch_speech_client.py create mode 100755 examples/online_serving/text_to_speech/higgs_audio_v2/run_server.sh create mode 100644 tests/e2e/offline_inference/test_higgs_audio_v2.py create mode 100644 tests/e2e/online_serving/test_higgs_audio_v2.py create mode 100644 vllm_omni/deploy/higgs_audio_v2.yaml create mode 100644 vllm_omni/model_executor/models/higgs_audio_v2/__init__.py create mode 100644 vllm_omni/model_executor/models/higgs_audio_v2/configuration_higgs_audio_v2.py create mode 100644 vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_decoder.py create mode 100644 vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_code2wav.py create mode 100644 vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_talker.py create mode 100644 vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_tokenizer.py create mode 100644 vllm_omni/model_executor/models/higgs_audio_v2/pipeline.py create mode 100644 vllm_omni/model_executor/stage_input_processors/higgs_audio_v2.py diff --git a/.buildkite/test-merge.yml b/.buildkite/test-merge.yml index 5b8d7e104..318b872b5 100644 --- a/.buildkite/test-merge.yml +++ b/.buildkite/test-merge.yml @@ -389,6 +389,31 @@ steps: volumes: - "/fsx/hf_cache:/fsx/hf_cache" + - label: "Omni · Higgs-Audio V2 Test" + timeout_in_minutes: 30 + commands: + - | + timeout 30m bash -c " + export VLLM_LOGGING_LEVEL=DEBUG + export VLLM_WORKER_MULTIPROC_METHOD=spawn + export VLLM_USE_DEEP_GEMM=0 + export VLLM_MOE_USE_DEEP_GEMM=0 + pytest -s -v tests/e2e/online_serving/test_higgs_audio_v2.py tests/e2e/offline_inference/test_higgs_audio_v2.py -m 'advanced_model and cuda' --run-level 'advanced_model' + " + agents: + queue: "gpu_1_queue" + plugins: + - docker#v5.2.0: + image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT + always-pull: true + propagate-environment: true + shm-size: "8gb" + environment: + - "HF_HOME=/fsx/hf_cache" + - "HF_TOKEN" + volumes: + - "/fsx/hf_cache:/fsx/hf_cache" + - label: "Diffusion · Qwen Image Edit Test" timeout_in_minutes: 20 commands: diff --git a/.readthedocs.yml b/.readthedocs.yml index 5a06c6634..2b5b81bbc 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -10,6 +10,11 @@ build: jobs: post_checkout: - git fetch --unshallow || true + pre_install: + # Pre-install CPU torch so the subsequent `pip install .[docs]` does not + # pull ~2 GB of CUDA 13 libs (cuBLAS/cuDNN/NCCL/...) that exceed RTD's + # 1 GB memory cap and time out the mkdocs build. + - python -m pip install --index-url https://download.pytorch.org/whl/cpu torch mkdocs: configuration: mkdocs.yml diff --git a/docs/user_guide/examples/offline_inference/text_to_speech.md b/docs/user_guide/examples/offline_inference/text_to_speech.md index 7937b1180..ad7777014 100644 --- a/docs/user_guide/examples/offline_inference/text_to_speech.md +++ b/docs/user_guide/examples/offline_inference/text_to_speech.md @@ -23,6 +23,7 @@ list of supported architectures across all modalities, see | OmniVoice | `k2-fsa/OmniVoice` | 2 (gen + dec) | ✓ | — | voice design (`--instruct`), language (`--lang`) | 24 kHz | | Qwen3-TTS | `Qwen/Qwen3-TTS-12Hz-1.7B-{CustomVoice,VoiceDesign,Base}` | 2 (talker + code2wav) | ✓ (Base) | ✓ | 3 task variants (`--query-type`) | 24 kHz | | Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | varies | ✓ | ✓ | voice presets (`--voice`) | 24 kHz | +| higgs-audio v2 | `bosonai/higgs-audio-v2-generation-3B-base` (+ `k2-fsa/OmniVoice/audio_tokenizer/` codec) | 2 (talker + code2wav, DualFFN) | ✓ (`--ref-audio` + `--ref-text`) | — | — | 24 kHz | ## Common Quick Start @@ -333,6 +334,46 @@ Available voice presets are listed on the HF model card (`mistralai/Voxtral-4B-T - `--concurrency M` requires `--streaming` and must evenly divide `--num-prompts`. - Run `--help` for the full argument surface. +--- + +## higgs-audio v2 + +2-stage TTS at 24 kHz: a vLLM-native Llama-3.2-3B talker with a DualFFN audio expert (Stage 0) feeding a HiggsAudio codec decoder (Stage 1) over the shared-memory connector. Stage 1 builds on the HiggsAudio decoder kernel at `vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_decoder.py`. + +### Prerequisites + +Voice clone uses HF's `HiggsAudioV2TokenizerModel`, instantiated from `k2-fsa/OmniVoice/audio_tokenizer/` — the boson-ai standalone tokenizer Hub repo's `model.safetensors` is actually the 3B talker LM, so we point HF at k2's repackaged codec weights instead. Only the `audio_tokenizer/` subdir (~806 MB) is downloaded. + +```bash +pip install -U "transformers>=5.3.0" +``` + +### Quick start (plain TTS) + +```bash +python examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py \ + --texts "Hello world." "The quick brown fox jumps over the lazy dog." \ + --output-dir results/wavs +``` + +### Voice cloning + +Pass both `--ref-audio` and `--ref-text` together: + +```bash +python examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py \ + --texts "Hello, this is a cloned voice." \ + --ref-audio /path/to/reference.wav \ + --ref-text "Exact transcript spoken in reference.wav." \ + --output-dir results/wavs +``` + +### Notes + +- Deploy config auto-loads from `vllm_omni/deploy/higgs_audio_v2.yaml`. +- `--ref-text` must be the real transcript of `--ref-audio`; mismatched text degrades cloned-voice quality. +- Out of scope (rejected with 4xx by the request validator): multi-speaker `[SPEAKERn]` dialogue, `profile:` text-only speaker descriptions, the `ref_audio_in_system_message` system-block variant, chunked long-form generation, and per-request `voice` / `instructions` / `task_type` / `language` / `speed != 1.0` / `x_vector_only_mode` / `speaker_embedding`. + ## Example materials ??? abstract "cosyvoice3/end2end.py" @@ -343,6 +384,9 @@ Available voice presets are listed on the HF model card (`mistralai/Voxtral-4B-T ``````py --8<-- "examples/offline_inference/text_to_speech/fish_speech/end2end.py" `````` +??? abstract "higgs_audio_v2/end2end.py" + ``````py + --8<-- "examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py" ??? abstract "glm_tts/end2end.py" ``````py --8<-- "examples/offline_inference/text_to_speech/glm_tts/end2end.py" diff --git a/docs/user_guide/examples/online_serving/text_to_speech.md b/docs/user_guide/examples/online_serving/text_to_speech.md index 37bfde7ad..3662dc107 100644 --- a/docs/user_guide/examples/online_serving/text_to_speech.md +++ b/docs/user_guide/examples/online_serving/text_to_speech.md @@ -19,6 +19,7 @@ For the full list of supported architectures across all modalities, see |---|---|---|---|---|---| | CosyVoice3 | `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | — | | Fish Speech S2 Pro | `fishaudio/s2-pro` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | ✓ | +| higgs-audio v2 | `bosonai/higgs-audio-v2-generation-3B-base` | ✓ (`ref_audio`+`ref_text`) | ✓ (codec_streaming) | — | — | | GLM-TTS | `zai-org/GLM-TTS` | ✓ (`ref_audio`+`ref_text`, required) | ✓ (PCM stream) | — | ✓ | | OmniVoice | `k2-fsa/OmniVoice` | (offline only) | — | — | — | | Qwen3-TTS | `Qwen/Qwen3-TTS-12Hz-1.7B-{CustomVoice,VoiceDesign,Base}` | ✓ (Base) | ✓ (PCM + WebSocket) | ✓ (presets + `/v1/audio/voices` upload) | ✓ (standard + FastRTC) | @@ -193,6 +194,45 @@ python fish_speech/gradio_demo.py --api-base http://localhost:8091 # if server --- +## higgs-audio v2 + +2-stage TTS at 24 kHz: a vLLM-native Llama-3.2-3B talker with a DualFFN audio expert (Stage 0) feeding a HiggsAudio codec decoder (Stage 1) that streams chunks back to the client. + +### Prerequisites + +Voice clone uses HF's `HiggsAudioV2TokenizerModel` loaded from `k2-fsa/OmniVoice/audio_tokenizer/` (~806 MB subdir; the boson-ai standalone tokenizer Hub repo's `model.safetensors` is the 3B talker LM, not the codec): + +```bash +pip install -U "transformers>=5.3.0" +``` + +### Launch +```bash +GPUS=6,7 PORT=8094 bash examples/online_serving/text_to_speech/higgs_audio_v2/run_server.sh +``` +Deploy config auto-loads from `vllm_omni/deploy/higgs_audio_v2.yaml`. + +### Sending requests +```bash +# Plain TTS +python higgs_audio_v2/batch_speech_client.py \ + --base-url http://localhost:8094 \ + --output-dir /tmp/higgs_out \ + --prompts "Hello world." "The quick brown fox jumps over the lazy dog." + +# Voice cloning — pass a reference clip and its transcript together +python higgs_audio_v2/batch_speech_client.py \ + --base-url http://localhost:8094 \ + --output-dir /tmp/higgs_clone \ + --ref-audio /path/to/reference.wav \ + --ref-text "Exact transcript spoken in reference.wav." \ + --prompts "Hello, this is a cloned voice." +``` + +### Notes +- Output: 24 kHz mono. +- `--ref-text` must be the real transcript of `--ref-audio`; mismatched text degrades cloned-voice quality. +- Out of scope (rejected with explicit 4xx): multi-speaker `[SPEAKERn]` tags inside `input`, `profile:` text-only speaker descriptions, the `ref_audio_in_system_message` system-block variant, chunked long-form generation, and per-request `voice` / `instructions` / `task_type` / `language` / `speed != 1.0` / `x_vector_only_mode` / `speaker_embedding`. ## GLM-TTS 2-stage TTS (AR + DiT flow-matching) at 24 kHz. Every request requires `ref_audio` + `ref_text`. @@ -459,6 +499,13 @@ The demo handles voice-preset selection and reference-audio upload. `voxtral_tts ``````py --8<-- "examples/online_serving/text_to_speech/fish_speech/speech_client.py" `````` +??? abstract "higgs_audio_v2/batch_speech_client.py" + ``````py + --8<-- "examples/online_serving/text_to_speech/higgs_audio_v2/batch_speech_client.py" + `````` +??? abstract "higgs_audio_v2/run_server.sh" + ``````sh + --8<-- "examples/online_serving/text_to_speech/higgs_audio_v2/run_server.sh" ??? abstract "glm_tts/gradio_demo.py" ``````py --8<-- "examples/online_serving/text_to_speech/glm_tts/gradio_demo.py" diff --git a/examples/offline_inference/text_to_speech/higgs_audio_v2/README.md b/examples/offline_inference/text_to_speech/higgs_audio_v2/README.md new file mode 100644 index 000000000..95a200d88 --- /dev/null +++ b/examples/offline_inference/text_to_speech/higgs_audio_v2/README.md @@ -0,0 +1,38 @@ +# higgs-audio v2 — offline example + +Drives Stage 0 (DualFFN talker) + Stage 1 (HiggsAudio codec) for `bosonai/higgs-audio-v2-generation-3B-base` end-to-end through the vLLM-Omni engine and writes a 24 kHz mono WAV per prompt. + +## Prerequisites + +Voice clone needs `transformers>=5.3.0` — vllm-omni loads the audio codec via HF's `HiggsAudioV2TokenizerModel`, instantiated from the `k2-fsa/OmniVoice/audio_tokenizer/` subdirectory (only that ~806 MB subdir is downloaded). The boson-ai standalone tokenizer repo's `model.safetensors` is actually a copy of the 3B talker LM, so HF can't load it directly; the k2 bundle ships the same codec weights repackaged with HF-compatible key naming. + +```bash +pip install -U "transformers>=5.3.0" +``` + +## Quick start + +Plain TTS: +```bash +python examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py \ + --texts "Hello world." "The quick brown fox jumps over the lazy dog." \ + --output-dir results/wavs +``` + +## Voice cloning + +Pass both `--ref-audio` and `--ref-text` together: +```bash +python examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py \ + --texts "Hello, this is a cloned voice." \ + --ref-audio /path/to/reference.wav \ + --ref-text "Exact transcript spoken in reference.wav." \ + --output-dir results/wavs +``` + +## Notes + +- Output: 24 kHz mono WAV. +- Deploy config: `vllm_omni/deploy/higgs_audio_v2.yaml` (auto-loaded by HF `model_type`). +- `--ref-text` must be the real transcript of `--ref-audio`; mismatched text degrades cloned-voice quality. +- For online serving, see [`examples/online_serving/text_to_speech/higgs_audio_v2/`](../../../online_serving/text_to_speech/higgs_audio_v2/). diff --git a/examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py b/examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py new file mode 100644 index 000000000..62a346621 --- /dev/null +++ b/examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py @@ -0,0 +1,205 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Offline higgs-audio v2 inference example. + +Runs Stage 0 (DualFFN talker) + Stage 1 (HiggsAudio codec) end-to-end +through the vLLM-Omni engine without going through the HTTP server, and +saves a 24 kHz mono WAV per prompt. + +Example: + + python examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py \\ + --texts "Hello world." \\ + "The quick brown fox jumps over the lazy dog." \\ + --output-dir results/wavs +""" + +from __future__ import annotations + +import os + +# DeepGEMM FP8 kernels require an optional backend that may not be installed. +# Disable the warmup before importing vLLM so engine startup falls back to the +# regular gemm path. Users with deep_gemm installed can override these. +os.environ.setdefault("VLLM_USE_DEEP_GEMM", "0") +os.environ.setdefault("VLLM_MOE_USE_DEEP_GEMM", "0") + +import time +from pathlib import Path + +import numpy as np +import soundfile as sf +import torch +from vllm.utils.argparse_utils import FlexibleArgumentParser + +from vllm_omni import Omni +from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults + +SAMPLE_RATE = 24_000 +DEFAULT_TEXTS = ( + "Hello world.", + "The quick brown fox jumps over the lazy dog.", +) + + +def parse_args(): + parser = FlexibleArgumentParser(description="Offline higgs-audio v2 inference") + parser.add_argument( + "--model", + type=str, + default="bosonai/higgs-audio-v2-generation-3B-base", + help="Stage-0 talker model id or path.", + ) + parser.add_argument( + "--texts", + type=str, + nargs="+", + default=list(DEFAULT_TEXTS), + help="One or more plain-text prompts to synthesize.", + ) + parser.add_argument( + "--output-dir", + type=str, + default="results/wavs", + help="Directory to write per-prompt WAV files.", + ) + parser.add_argument( + "--deploy-config", + type=str, + default=None, + help="Override the deploy config path. Auto-loads " + "vllm_omni/deploy/higgs_audio_v2.yaml from the HF model_type by default.", + ) + parser.add_argument( + "--max-new-tokens", + type=int, + default=500, + help="Cap on Stage-0 codec frames per request.", + ) + parser.add_argument( + "--ref-audio", + type=str, + default=None, + help="Reference clip for voice clone (path to a WAV file). Paired with --ref-text.", + ) + parser.add_argument( + "--ref-text", + type=str, + default=None, + help="Transcript of the reference clip. Required when --ref-audio is set.", + ) + nullify_stage_engine_defaults(parser) + return parser.parse_args() + + +def _slugify(text: str) -> str: + import re + + slug = re.sub(r"\s+", "_", text.strip().lower()) + slug = re.sub(r"[^a-z0-9_]+", "", slug) + return slug[:48] or "prompt" + + +def _extract_pcm(multimodal_output: dict) -> torch.Tensor: + """Pull the final concatenated PCM tensor out of a request's multimodal_output.""" + audio = multimodal_output.get("model_outputs") + if audio is None: + audio = multimodal_output.get("audio") + if audio is None: + raise ValueError(f"no audio key in multimodal_output: {list(multimodal_output.keys())}") + if isinstance(audio, list): + valid = [torch.as_tensor(a).float().cpu().reshape(-1) for a in audio if a is not None] + if not valid: + raise ValueError("audio list is empty") + return torch.cat(valid, dim=0) if len(valid) > 1 else valid[0] + return torch.as_tensor(audio).float().cpu().reshape(-1) + + +def _pcm_to_int16(pcm: torch.Tensor) -> np.ndarray: + arr = pcm.numpy() + if arr.dtype.kind == "f": + arr = np.clip(arr, -1.0, 1.0) + arr = (arr * 32767.0).astype(np.int16) + else: + arr = arr.astype(np.int16) + return arr + + +def main(): + args = parse_args() + if (args.ref_audio is None) != (args.ref_text is None): + raise SystemExit("--ref-audio and --ref-text must be supplied together") + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + engine = Omni(model=args.model, deploy_config=args.deploy_config) + + # Build prompt_token_ids using the same path serving_speech.py takes online. + from transformers import AutoProcessor + + from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_v2_tokenizer import ( + build_plain_text_prompt, + build_voice_clone_prompt, + input_ids_to_python_list, + ) + + processor = AutoProcessor.from_pretrained(args.model, trust_remote_code=True) + + # Voice-clone path: load the reference clip once. The HF processor will + # encode it via the bundled HiggsAudioV2TokenizerModel each time we build + # a prompt below. This is cheap on CPU for a few-second clip. + ref_wav: np.ndarray | None = None + ref_sr: int | None = None + if args.ref_audio is not None: + ref_wav, ref_sr = sf.read(args.ref_audio, always_2d=False) + if ref_wav.ndim == 2: + ref_wav = ref_wav.mean(axis=1) + + print(f"Model : {args.model}") + print(f"Prompts : {len(args.texts)}") + print(f"Output dir : {output_dir}") + print(f"Voice clone : {'yes' if ref_wav is not None else 'no'}") + + # Run one prompt at a time. The Stage-0 talker's per-slot audio state is + # request-scoped; submitting multiple prompts in the same engine.generate() + # call would batch them in the AR runner and exercise a code path that is + # not validated for this model yet. + total_elapsed = 0.0 + total_dur = 0.0 + for text in args.texts: + if ref_wav is not None: + out = build_voice_clone_prompt(processor, text, ref_wav, int(ref_sr), args.ref_text) + prompt = { + "prompt_token_ids": out["prompt_token_ids"], + # Bare tensors (NOT list-wrapped): the msgspec serializer in + # vllm_omni.data_entry_keys routes torch.Tensor → tensor_data + # and list[Tensor] → list_data (which silently strips tensors). + "additional_information": { + "audio_input_ids": out["audio_input_ids"], + "audio_input_ids_mask": out["audio_input_ids_mask"], + }, + } + else: + inputs = build_plain_text_prompt(processor, text) + prompt = {"prompt_token_ids": input_ids_to_python_list(inputs)} + t_start = time.perf_counter() + outputs = engine.generate([prompt]) + elapsed = time.perf_counter() - t_start + total_elapsed += elapsed + + mm = outputs[0].outputs[0].multimodal_output + pcm = _extract_pcm(mm) + slug = _slugify(text) + out_path = output_dir / f"{slug}.wav" + sf.write(str(out_path), _pcm_to_int16(pcm), SAMPLE_RATE, format="WAV", subtype="PCM_16") + dur = pcm.numel() / SAMPLE_RATE + total_dur += dur + print(f" {slug:<50} dur={dur:5.2f}s -> {out_path}") + + rtf = total_elapsed / total_dur if total_dur > 0 else float("inf") + print(f"Total infer : {total_elapsed:.2f}s total audio: {total_dur:.2f}s RTF: {rtf:.3f}") + + +if __name__ == "__main__": + os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn") + main() diff --git a/examples/online_serving/text_to_speech/higgs_audio_v2/README.md b/examples/online_serving/text_to_speech/higgs_audio_v2/README.md new file mode 100644 index 000000000..7b5e272df --- /dev/null +++ b/examples/online_serving/text_to_speech/higgs_audio_v2/README.md @@ -0,0 +1,63 @@ +# higgs-audio v2 online example + +This directory contains the online-serving entry points for boson-ai's higgs-audio v2 as integrated by vllm-omni: a 2-stage TTS pipeline (Llama-3.2-3B talker with DualFFN audio expert + HiggsAudio codec decoder) emitting 24 kHz mono speech. + +## Prerequisites + +Voice clone uses HF's `HiggsAudioV2TokenizerModel` loaded from `k2-fsa/OmniVoice/audio_tokenizer/` (the boson-ai standalone tokenizer Hub repo's `model.safetensors` is the 3B talker LM, not the codec). Only that ~806 MB subdir is downloaded. + +```bash +pip install -U "transformers>=5.3.0" +``` + +## Files + +- `run_server.sh` — launch the vllm-omni server with the bundled `vllm_omni/deploy/higgs_audio_v2.yaml` deploy config. +- `batch_speech_client.py` — send a list of prompts to `/v1/audio/speech` and save the returned WAV / PCM bytes to a directory; optionally passes `--ref-audio` + `--ref-text` for shallow voice clone. + +## Launching the server + +```bash +GPUS=6,7 PORT=8094 bash examples/online_serving/text_to_speech/higgs_audio_v2/run_server.sh +``` + +Environment overrides: + +- `MODEL` — HF id of the talker (default `bosonai/higgs-audio-v2-generation-3B-base`). +- `PORT` — server port (default `8094`). +- `GPUS` — `CUDA_VISIBLE_DEVICES` value (default `6,7`). +- `GPU_UTIL` — `--gpu-memory-utilization` (default `0.4`). + +The script also exports `VLLM_USE_DEEP_GEMM=0` / `VLLM_MOE_USE_DEEP_GEMM=0` so the example works on images without the optional `deep_gemm` backend. + +The deploy YAML ships with `async_chunk: false` and `codec_streaming: true`, i.e. Stage 0 finishes its codec frames before Stage 1 starts decoding, and Stage 1 streams WAV/PCM bytes to the client chunk-by-chunk. + +## Driving the server + +Plain TTS: + +```bash +python examples/online_serving/text_to_speech/higgs_audio_v2/batch_speech_client.py \ + --base-url http://localhost:8094 \ + --model bosonai/higgs-audio-v2-generation-3B-base \ + --output-dir /tmp/higgs_audio_v2_batch \ + --prompts "Hello world." \ + "The quick brown fox jumps over the lazy dog." +``` + +Voice clone — pass a reference clip and its transcript (both required together): + +```bash +python examples/online_serving/text_to_speech/higgs_audio_v2/batch_speech_client.py \ + --base-url http://localhost:8094 \ + --model bosonai/higgs-audio-v2-generation-3B-base \ + --output-dir /tmp/higgs_audio_v2_clone \ + --ref-audio /path/to/reference.wav \ + --ref-text "Exact transcript spoken in reference.wav." \ + --prompts "Hello, this is a cloned voice." +``` + +## Notes + +- `--ref-text` must be the real transcript of `--ref-audio`; mismatched text degrades cloned-voice quality. +- Out of scope (rejected with explicit 4xx by the request validator): multi-speaker `[SPEAKERn]` tags inside `input`, `profile:` text-only speaker descriptions, the `ref_audio_in_system_message` system-block variant, chunked long-form generation, and per-request `voice` / `instructions` / `task_type` / `language` / `speed != 1.0` / `x_vector_only_mode` / `speaker_embedding`. diff --git a/examples/online_serving/text_to_speech/higgs_audio_v2/batch_speech_client.py b/examples/online_serving/text_to_speech/higgs_audio_v2/batch_speech_client.py new file mode 100644 index 000000000..1e227b14d --- /dev/null +++ b/examples/online_serving/text_to_speech/higgs_audio_v2/batch_speech_client.py @@ -0,0 +1,122 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Batch client for the higgs-audio v2 online server. + +Sends a fixed list of prompts to ``/v1/audio/speech`` and saves the returned +WAV files (or raw PCM bytes when ``--format pcm``) into ``--output-dir``. + +Usage (plain text -> speech): + + python examples/online_serving/text_to_speech/higgs_audio_v2/batch_speech_client.py \ + --base-url http://localhost:8094 \ + --output-dir /tmp/higgs_audio_v2_batch \ + --prompts "Hello world." "The quick brown fox jumps over the lazy dog." + +Usage (shallow voice clone — pass a reference clip + its transcript): + + python examples/online_serving/text_to_speech/higgs_audio_v2/batch_speech_client.py \ + --base-url http://localhost:8094 \ + --output-dir /tmp/higgs_audio_v2_clone \ + --ref-audio path/to/reference.wav \ + --ref-text "the transcript of the reference clip" \ + --prompts "Hello world." +""" + +from __future__ import annotations + +import argparse +import base64 +import sys +from pathlib import Path + +DEFAULT_PROMPTS = ( + "Hello world.", + "The quick brown fox jumps over the lazy dog.", + "It was the night before my birthday.", + "Innovation distinguishes between a leader and a follower.", +) + + +def _slug(text: str) -> str: + import re + + s = re.sub(r"\s+", "_", text.strip().lower()) + return re.sub(r"[^a-z0-9_]+", "", s)[:32] or "prompt" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--base-url", default="http://localhost:8094") + parser.add_argument("--model", default="higgs_audio_v2") + parser.add_argument("--prompts", nargs="+", default=list(DEFAULT_PROMPTS)) + parser.add_argument("--output-dir", type=Path, default=Path("/tmp/higgs_audio_v2_batch")) + parser.add_argument("--format", choices=("wav", "pcm"), default="wav") + parser.add_argument("--max-new-tokens", type=int, default=300) + parser.add_argument("--seed", type=int, default=42) + parser.add_argument("--timeout-s", type=float, default=120.0) + parser.add_argument( + "--ref-audio", + type=Path, + default=None, + help="Reference clip for voice clone (path to a WAV file). Must be paired with --ref-text.", + ) + parser.add_argument( + "--ref-text", + type=str, + default=None, + help="Transcript of the reference clip. Required when --ref-audio is set.", + ) + args = parser.parse_args() + + if (args.ref_audio is None) != (args.ref_text is None): + print("--ref-audio and --ref-text must be supplied together", file=sys.stderr) + return 2 + + ref_audio_data_url: str | None = None + if args.ref_audio is not None: + if not args.ref_audio.exists(): + print(f"ref-audio file not found: {args.ref_audio}", file=sys.stderr) + return 2 + mime = "audio/wav" if args.ref_audio.suffix.lower() == ".wav" else "audio/mpeg" + ref_b64 = base64.b64encode(args.ref_audio.read_bytes()).decode("ascii") + ref_audio_data_url = f"data:{mime};base64,{ref_b64}" + + try: + import httpx + except ImportError: + print( + "this client needs `httpx`. Install with `pip install httpx`.", + file=sys.stderr, + ) + return 2 + + args.output_dir.mkdir(parents=True, exist_ok=True) + url = args.base_url.rstrip("/") + "/v1/audio/speech" + failures = 0 + with httpx.Client(timeout=args.timeout_s) as client: + for prompt in args.prompts: + payload = { + "model": args.model, + "input": prompt, + "response_format": args.format, + "max_new_tokens": args.max_new_tokens, + "seed": args.seed, + } + if ref_audio_data_url is not None: + payload["ref_audio"] = ref_audio_data_url + payload["ref_text"] = args.ref_text + resp = client.post(url, json=payload) + if resp.status_code != 200: + print(f"[FAIL] {prompt!r} -> {resp.status_code}: {resp.text[:200]}", file=sys.stderr) + failures += 1 + continue + suffix = ".wav" if args.format == "wav" else ".pcm" + out = args.output_dir / f"{_slug(prompt)}{suffix}" + out.write_bytes(resp.content) + print(f"[ ok ] {prompt!r} -> {out} ({len(resp.content)} bytes)") + + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/online_serving/text_to_speech/higgs_audio_v2/run_server.sh b/examples/online_serving/text_to_speech/higgs_audio_v2/run_server.sh new file mode 100755 index 000000000..12db89558 --- /dev/null +++ b/examples/online_serving/text_to_speech/higgs_audio_v2/run_server.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Launch vLLM-Omni server for higgs-audio v2. +# +# v1 scope: plain text -> 24 kHz speech only. Voice cloning, multi-speaker, +# ChatML rich content, and language overrides are rejected by the validator +# with explicit 4xx (see vllm_omni/entrypoints/openai/serving_speech.py). +# +# Usage: +# ./run_server.sh # default port 8094, GPUs 6 and 7 +# PORT=8095 GPUS=6,7 ./run_server.sh +# MODEL=bosonai/higgs-audio-v2-generation-3B-base ./run_server.sh + +set -e + +MODEL="${MODEL:-bosonai/higgs-audio-v2-generation-3B-base}" +PORT="${PORT:-8094}" +GPUS="${GPUS:-6,7}" +GPU_UTIL="${GPU_UTIL:-0.4}" + +echo "Starting higgs-audio v2 server" +echo " MODEL=$MODEL" +echo " PORT=$PORT" +echo " CUDA_VISIBLE_DEVICES=$GPUS" + +# DeepGEMM FP8 kernels are optional and trip warmup on builds without +# the deep_gemm backend; disable them so the example works out of the box. +# Users with deep_gemm installed can re-enable via the same env vars. +CUDA_VISIBLE_DEVICES="$GPUS" \ +VLLM_USE_DEEP_GEMM=0 \ +VLLM_MOE_USE_DEEP_GEMM=0 \ +vllm-omni serve "$MODEL" \ + --deploy-config vllm_omni/deploy/higgs_audio_v2.yaml \ + --host 0.0.0.0 \ + --port "$PORT" \ + --gpu-memory-utilization "$GPU_UTIL" \ + --trust-remote-code \ + --omni diff --git a/setup.py b/setup.py index 057212d67..403d8a416 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,12 @@ def detect_target_device() -> str: else: print(f"Warning: Invalid VLLM_OMNI_TARGET_DEVICE '{target_device}', falling back to auto-detection") + # Priority 1.5: ReadTheDocs builds use CPU requirements (no GPU, 1 GB RAM cap). + # Avoids pulling ~2 GB of CUDA libs that push the mkdocs build into swap. + if os.environ.get("READTHEDOCS"): + print("Detected READTHEDOCS=1; using cpu requirements for docs build") + return "cpu" + # Priority 2: Torch backend detection # This is a code path for when user is using # --no-build-isolation flag diff --git a/tests/e2e/offline_inference/test_higgs_audio_v2.py b/tests/e2e/offline_inference/test_higgs_audio_v2.py new file mode 100644 index 000000000..fdeccc924 --- /dev/null +++ b/tests/e2e/offline_inference/test_higgs_audio_v2.py @@ -0,0 +1,231 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +""" +End-to-end offline inference tests for higgs-audio v2. + +Mirrors examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py: +build Stage-0 prompt_token_ids with the model's own processor + plain-text +chat template, drive the engine via ``Omni.generate``, and inspect the +Stage-1 PCM payload directly. The OmniRunnerHandler.send_audio_speech_request +helper applies a Qwen-style ChatML wrap that is not valid for higgs, so this +test bypasses it (same approach as test_voxtral_tts.py). + +v1 scope is plain text -> 24 kHz speech; voice cloning, multi-speaker prompts, +language overrides, and task_type selection are out of scope. +""" + +from __future__ import annotations + +import os + +# Match the example script: keep DeepGEMM FP8 off so engine init does not +# require the optional ``deep_gemm`` backend. +os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn") +os.environ.setdefault("VLLM_USE_DEEP_GEMM", "0") +os.environ.setdefault("VLLM_MOE_USE_DEEP_GEMM", "0") + +import numpy as np +import pytest +import torch +from vllm import SamplingParams + +from tests.helpers.mark import hardware_test +from tests.helpers.media import get_asset_path +from tests.helpers.runtime import OmniRunner +from tests.helpers.stage_config import get_deploy_config_path + +MODEL = "bosonai/higgs-audio-v2-generation-3B-base" +STAGE_CONFIG = get_deploy_config_path("higgs_audio_v2.yaml") +SAMPLE_RATE = 24_000 +# A short sentence at 24 kHz must produce at least ~0.5 s of audio. +MIN_AUDIO_SAMPLES = 12_000 +TEST_TEXT = "Hello world." + +# Reuse the qwen3_tts ref clip + transcript for voice clone tests (see +# tests/e2e/online_serving/test_qwen3_tts_base.py:27-28 for the source). +_REF_AUDIO_ASSET = "qwen3_tts/clone_2.wav" +_REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you." + +# (model, stage_config_path, extra_omni_kwargs) — same shape as test_voxtral_tts.py. +# Stage configs already pin enforce_eager=true per stage, so no Omni-level override. +_OMNI_RUNNER_PARAM = (MODEL, STAGE_CONFIG, {"trust_remote_code": True}) + + +def _compose_request(model_name: str, text: str) -> dict: + """Build a higgs prompt dict via the upstream processor + plain-text template. + + The serving layer's ``_build_higgs_audio_v2_params`` does the same thing + (see vllm_omni/entrypoints/openai/serving_speech.py:1520) — keeping the + offline and online prompt-construction paths identical is what guarantees + input-token parity with the HF reference. + """ + from transformers import AutoProcessor + + from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_v2_tokenizer import ( + build_plain_text_prompt, + input_ids_to_python_list, + ) + + processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True) + inputs = build_plain_text_prompt(processor, text) + return {"prompt_token_ids": input_ids_to_python_list(inputs)} + + +def _extract_pcm(outputs) -> torch.Tensor: + """Pull the concatenated PCM tensor out of the Stage-1 multimodal_output.""" + for stage_output in outputs: + mm = getattr(stage_output, "multimodal_output", None) + if not mm: + continue + audio = mm.get("model_outputs") if "model_outputs" in mm else mm.get("audio") + if audio is None: + continue + if isinstance(audio, list): + chunks = [torch.as_tensor(a).float().cpu().reshape(-1) for a in audio if a is not None] + if not chunks: + continue + return torch.cat(chunks, dim=0) if len(chunks) > 1 else chunks[0] + return torch.as_tensor(audio).float().cpu().reshape(-1) + raise AssertionError(f"no audio payload in any stage output (got {len(outputs)} stages)") + + +@pytest.mark.core_model +@pytest.mark.tts +@hardware_test(res={"cuda": "H100"}, num_cards=1) +@pytest.mark.parametrize("omni_runner_function", [_OMNI_RUNNER_PARAM], indirect=True) +def test_higgs_audio_v2_offline_plain_text(omni_runner_function: OmniRunner) -> None: + """ + Offline plain-text -> 24 kHz PCM via the bundled higgs_audio_v2 deploy. + + Deploy Setting: vllm_omni/deploy/higgs_audio_v2.yaml (codec_streaming=true, + async_chunk=false, enforce_eager=true per stage). + Input Modal: text + Output Modal: audio (PCM tensor, 24 kHz mono float) + Input Setting: single prompt, default sampling + """ + omni = omni_runner_function.omni + inputs = _compose_request(MODEL, TEST_TEXT) + + # max_tokens caps Stage-0 codec frames; Stage 1 max_tokens lifts the + # per-codebook prefill cap. Mirror the deploy yaml's defaults. + stage_sampling = [ + SamplingParams(temperature=1.0, top_p=0.95, top_k=50, max_tokens=1024, seed=42), + SamplingParams(temperature=0.0, top_p=1.0, top_k=-1, max_tokens=65536, seed=42), + ] + + outputs = list(omni.generate([inputs], stage_sampling)) + assert outputs, "no outputs returned from Omni.generate" + + pcm = _extract_pcm(outputs) + arr = pcm.numpy() + + assert arr.size > MIN_AUDIO_SAMPLES, ( + f"audio too short: {arr.size} samples, expected > {MIN_AUDIO_SAMPLES} " + f"({MIN_AUDIO_SAMPLES / SAMPLE_RATE:.2f} s @ {SAMPLE_RATE} Hz)" + ) + # Threshold 5e-4 (not 1e-3) keeps us above true silence while tolerating + # the ``load_format=dummy`` Stage-1 codec — random RVQ/DAC weights collapse + # the output to a near-constant peak around 7.4e-4. Real-weight runs reach + # 0.1+ peak so this still catches genuine silence. + assert np.max(np.abs(arr)) > 5e-4, "audio output is silent (peak abs amplitude < 5e-4)" + assert np.isfinite(arr).all(), "audio contains non-finite samples (nan/inf)" + + +@pytest.mark.core_model +@pytest.mark.tts +@hardware_test(res={"cuda": "H100"}, num_cards=1) +@pytest.mark.parametrize("omni_runner_function", [_OMNI_RUNNER_PARAM], indirect=True) +def test_higgs_audio_v2_offline_batch_two_prompts(omni_runner_function: OmniRunner) -> None: + """ + Submit two prompts in one engine call and verify both yield non-trivial PCM. + + Guards the Stage-0 talker's per-slot audio state under a batched call — + the prior example ran prompts one at a time citing 'request-scoped audio + state'; this test pins the batched-2 behavior the engine path actually + needs to support for /v1/audio/speech under concurrent traffic. + """ + omni = omni_runner_function.omni + prompts = [ + _compose_request(MODEL, "Hello world."), + _compose_request(MODEL, "The quick brown fox jumps over the lazy dog."), + ] + stage_sampling = [ + SamplingParams(temperature=1.0, top_p=0.95, top_k=50, max_tokens=1024, seed=42), + SamplingParams(temperature=0.0, top_p=1.0, top_k=-1, max_tokens=65536, seed=42), + ] + + outputs = list(omni.generate(prompts, stage_sampling)) + assert outputs, "no outputs returned from Omni.generate" + + pcm = _extract_pcm(outputs) + # Whether the engine returns one concatenated PCM or per-request tensors is + # implementation-defined; for either shape, total samples must clear the + # floor for at least one of the two prompts. + assert pcm.numel() > MIN_AUDIO_SAMPLES, ( + f"batched audio too short: {pcm.numel()} samples, expected > {MIN_AUDIO_SAMPLES}" + ) + # See the silence-threshold note in test_higgs_audio_v2_offline_plain_text. + assert np.max(np.abs(pcm.numpy())) > 5e-4, "batched audio output is silent" + + +def _compose_voice_clone_request(model_name: str, text: str, ref_text: str, ref_wav_path) -> dict: + """Build the voice-clone prompt + additional_information payload. + + Calls the same ``build_voice_clone_prompt`` the serving layer uses online — + the HF processor encodes the reference clip on the spot via the bundled + HiggsAudioV2TokenizerModel. The talker's + ``_maybe_apply_ref_audio_substitution`` consumes the tensors at prefill. + """ + import soundfile as sf + from transformers import AutoProcessor + + from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_v2_tokenizer import ( + build_voice_clone_prompt, + ) + + wav, sr = sf.read(str(ref_wav_path), always_2d=False) + if wav.ndim == 2: + wav = wav.mean(axis=1) # downmix any stereo asset to mono + processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True) + out = build_voice_clone_prompt(processor, text, wav, int(sr), ref_text) + return { + "prompt_token_ids": out["prompt_token_ids"], + # Pass tensors bare (see serving_speech._build_higgs_audio_v2_params + # — list-wrapped tensors get dropped by the msgspec serializer). + "additional_information": { + "audio_input_ids": out["audio_input_ids"], + "audio_input_ids_mask": out["audio_input_ids_mask"], + }, + } + + +@pytest.mark.advanced_model +@pytest.mark.tts +@hardware_test(res={"cuda": "H100"}, num_cards=1) +@pytest.mark.parametrize("omni_runner_function", [_OMNI_RUNNER_PARAM], indirect=True) +def test_higgs_audio_v2_offline_voice_clone(omni_runner_function: OmniRunner) -> None: + """ + Shallow voice clone via ref_audio + ref_text, offline. + + Deploy Setting: vllm_omni/deploy/higgs_audio_v2.yaml. + Input Modal: text + reference audio (qwen3_tts/clone_2.wav vendored asset) + Output Modal: audio (PCM tensor, 24 kHz mono float) + Input Setting: single prompt + """ + omni = omni_runner_function.omni + ref_path = get_asset_path(_REF_AUDIO_ASSET) + inputs = _compose_voice_clone_request(MODEL, TEST_TEXT, _REF_TEXT, ref_path) + + stage_sampling = [ + SamplingParams(temperature=1.0, top_p=0.95, top_k=50, max_tokens=1024, seed=42), + SamplingParams(temperature=0.0, top_p=1.0, top_k=-1, max_tokens=65536, seed=42), + ] + + outputs = list(omni.generate([inputs], stage_sampling)) + assert outputs, "no outputs returned from Omni.generate for voice clone" + + pcm = _extract_pcm(outputs) + arr = pcm.numpy() + assert arr.size > MIN_AUDIO_SAMPLES, f"cloned audio too short: {arr.size} samples, expected > {MIN_AUDIO_SAMPLES}" + assert np.max(np.abs(arr)) > 5e-4, "voice-clone output is silent" + assert np.isfinite(arr).all(), "voice-clone audio contains non-finite samples" diff --git a/tests/e2e/online_serving/test_higgs_audio_v2.py b/tests/e2e/online_serving/test_higgs_audio_v2.py new file mode 100644 index 000000000..92688ad0b --- /dev/null +++ b/tests/e2e/online_serving/test_higgs_audio_v2.py @@ -0,0 +1,299 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +""" +End-to-end online tests for higgs-audio v2 against /v1/audio/speech. + +v1 scope is plain text -> 24 kHz speech plus shallow voice clone via +ref_audio + ref_text (inline) or voice= (after POST /v1/audio/voices). +The model-aware request validator +(vllm_omni/entrypoints/openai/serving_speech.py::_validate_higgs_audio_v2_request) +rejects multi-speaker tags, language overrides, task_type, and bare +voice= for names that do not match an uploaded speaker — this suite +exercises both the happy path (plain text in, audio bytes out) and the +validator rejections, so a regression that loosens the schema will fail +loudly. +""" + +from __future__ import annotations + +import os + +os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn") +# Match run_server.sh: DeepGEMM FP8 kernels are optional and trip warmup on +# images without the deep_gemm backend, so disable them by default. +os.environ.setdefault("VLLM_USE_DEEP_GEMM", "0") +os.environ.setdefault("VLLM_MOE_USE_DEEP_GEMM", "0") + +import pytest + +from tests.helpers.mark import hardware_test +from tests.helpers.media import load_test_audio_data_url +from tests.helpers.runtime import OmniServerParams +from tests.helpers.stage_config import get_deploy_config_path + +MODEL = "bosonai/higgs-audio-v2-generation-3B-base" +STAGE_CONFIG = get_deploy_config_path("higgs_audio_v2.yaml") +SERVER_ARGS = ["--trust-remote-code", "--disable-log-stats"] +# DeepGEMM warmup is optional; mirror run_server.sh and switch it off in the +# server subprocess env too (parent-process os.environ above only affects this +# test driver; the engine subprocesses inherit through env_dict). +SERVER_ENV = {"VLLM_USE_DEEP_GEMM": "0", "VLLM_MOE_USE_DEEP_GEMM": "0"} + +TEST_PARAMS = [ + pytest.param( + OmniServerParams( + model=MODEL, + stage_config_path=STAGE_CONFIG, + server_args=SERVER_ARGS, + env_dict=SERVER_ENV, + ), + id="higgs_audio_v2_plain_text", + ) +] + +DEFAULT_SPEECH_TIMEOUT_S = 180.0 +# Floor for ~0.5 s of 24 kHz mono PCM_16: 24000 * 0.5 * 2 bytes ≈ 24 KiB. +# A WAV header adds 44 bytes; pick a conservative floor that catches truncated +# / silence-only outputs without flagging short legitimate clips. +_MIN_AUDIO_BYTES = 20_000 + +# Reuse the qwen3_tts vendored reference clip (clean ~5 s 24 kHz mono human +# speech) + its transcript. See tests/e2e/online_serving/test_qwen3_tts_base.py +# for the asset rationale — keeping a single shared reference clip across TTS +# tests avoids duplicating WAVs in the repo. +_REF_AUDIO_URL = load_test_audio_data_url("qwen3_tts/clone_2.wav") +_REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you." + + +@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) +class TestHiggsAudioV2OnlineHappyPath: + """Plain-text -> audio happy paths against the live HTTP server.""" + + @pytest.mark.core_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_plain_text_wav(self, omni_server, openai_client) -> None: + """Single non-streaming WAV request — covers the canonical TTS happy path.""" + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": "Hello world.", + "stream": False, + "response_format": "wav", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "min_audio_bytes": _MIN_AUDIO_BYTES, + } + ) + + @pytest.mark.core_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_plain_text_pcm_streaming(self, omni_server, openai_client) -> None: + """Streaming PCM via the shared-memory connector's codec_streaming path. + + higgs_audio_v2.yaml pins ``codec_streaming: true`` + ``async_chunk: false``, + so the only streaming surface exposed to clients is the WAV/PCM bytes + served chunk-by-chunk from Stage 1 — exercise it directly. + """ + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": "Streaming the quick brown fox over the lazy dog.", + "stream": True, + "response_format": "pcm", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "min_audio_bytes": _MIN_AUDIO_BYTES, + } + ) + + @pytest.mark.core_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_plain_text_with_max_new_tokens(self, omni_server, openai_client) -> None: + """max_new_tokens is one of the few extra fields the higgs validator accepts.""" + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": "Innovation distinguishes between a leader and a follower.", + "stream": False, + "response_format": "wav", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "max_new_tokens": 500, + "min_audio_bytes": _MIN_AUDIO_BYTES, + } + ) + + @pytest.mark.core_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_concurrent_plain_text(self, omni_server, openai_client) -> None: + """Three concurrent non-streaming requests — guards the per-slot audio state.""" + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": "It was the night before my birthday.", + "stream": False, + "response_format": "wav", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "min_audio_bytes": _MIN_AUDIO_BYTES, + }, + request_num=3, + ) + + +# --------------------------------------------------------------------------- +# Validator rejections served over HTTP. Each case targets one out-of-scope +# field; the validator returns 4xx via the OpenAI error path. We keep the +# error-message substring loose so a phrasing tweak in the validator does not +# break CI, but tight enough to catch a regression that silently accepts the +# field. +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) +class TestHiggsAudioV2OnlineValidatorRejections: + """Out-of-scope fields must come back as 4xx with a higgs-named message.""" + + @pytest.mark.core_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_rejects_ref_audio_without_ref_text(self, omni_server, openai_client) -> None: + """Voice clone needs the transcript too — half-supplied is a 4xx.""" + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": "Hello world.", + "ref_audio": _REF_AUDIO_URL, + "response_format": "wav", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "status_code": (400, 422), + "err_message": "ref_text", + } + ) + + @pytest.mark.core_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_rejects_ref_text_without_ref_audio(self, omni_server, openai_client) -> None: + """Symmetric guard: ref_text alone is not enough — must come with ref_audio.""" + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": "Hello world.", + "ref_text": "some transcript", + "response_format": "wav", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "status_code": (400, 422), + "err_message": "ref_audio", + } + ) + + @pytest.mark.core_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_rejects_task_type(self, omni_server, openai_client) -> None: + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": "Hello world.", + "task_type": "Base", + "response_format": "wav", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "status_code": (400, 422), + "err_message": "task_type", + } + ) + + @pytest.mark.core_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_rejects_language_override(self, omni_server, openai_client) -> None: + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": "Hello world.", + "language": "Chinese", + "response_format": "wav", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "status_code": (400, 422), + "err_message": "language", + } + ) + + @pytest.mark.core_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_rejects_multi_speaker_tag_in_text(self, omni_server, openai_client) -> None: + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": "[SPEAKER0] hi", + "response_format": "wav", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "status_code": (400, 422), + "err_message": "multi-speaker", + } + ) + + @pytest.mark.core_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_rejects_empty_input(self, omni_server, openai_client) -> None: + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": " ", + "response_format": "wav", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "status_code": (400, 422), + "err_message": "empty", + } + ) + + +@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) +class TestHiggsAudioV2OnlineVoiceClone: + """Shallow voice clone: ref_audio + ref_text -> speech in the cloned voice. + + The HF processor that the serving layer calls (lazy-loaded at the first + request) ships with the bundled HiggsAudioV2TokenizerModel and encodes + the reference clip in-process. The talker substitutes the encoded codes + at the prompt-side audio placeholders via + :meth:`HiggsAudioV2TalkerForConditionalGeneration._maybe_apply_ref_audio_substitution`. + """ + + @pytest.mark.advanced_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_voice_clone_basic(self, omni_server, openai_client) -> None: + """Single non-streaming WAV request driven by the qwen3_tts ref clip.""" + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": "Hello world.", + "ref_audio": _REF_AUDIO_URL, + "ref_text": _REF_TEXT, + "stream": False, + "response_format": "wav", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "min_audio_bytes": _MIN_AUDIO_BYTES, + } + ) + + @pytest.mark.advanced_model + @pytest.mark.tts + @hardware_test(res={"cuda": "H100"}, num_cards=1) + def test_voice_clone_pcm_streaming(self, omni_server, openai_client) -> None: + """Voice clone over the streaming PCM path.""" + openai_client.send_audio_speech_request( + { + "model": omni_server.model, + "input": "Innovation distinguishes a leader from a follower.", + "ref_audio": _REF_AUDIO_URL, + "ref_text": _REF_TEXT, + "stream": True, + "response_format": "pcm", + "timeout": DEFAULT_SPEECH_TIMEOUT_S, + "min_audio_bytes": _MIN_AUDIO_BYTES, + } + ) diff --git a/vllm_omni/config/pipeline_registry.py b/vllm_omni/config/pipeline_registry.py index 1474e325c..4a8f48de6 100644 --- a/vllm_omni/config/pipeline_registry.py +++ b/vllm_omni/config/pipeline_registry.py @@ -121,4 +121,8 @@ _OMNI_PIPELINES: dict[str, tuple[str, str]] = { "vllm_omni.model_executor.models.moss_tts_nano.pipeline", "MOSS_TTS_NANO_PIPELINE", ), + "higgs_audio_v2": ( + "vllm_omni.model_executor.models.higgs_audio_v2.pipeline", + "HIGGS_AUDIO_V2_PIPELINE", + ), } diff --git a/vllm_omni/deploy/higgs_audio_v2.yaml b/vllm_omni/deploy/higgs_audio_v2.yaml new file mode 100644 index 000000000..07e25d5c1 --- /dev/null +++ b/vllm_omni/deploy/higgs_audio_v2.yaml @@ -0,0 +1,83 @@ +# Higgs-audio v2 deploy: Stage 0 (talker) -> Stage 1 (code2wav) via shared memory. +# +# Phase-1 scope is plain text -> 24 kHz speech only; voice cloning, multi-speaker, +# ChatML rich messages, and music/sound-event prompts are rejected by the +# request validator with explicit 4xx (see +# vllm_omni/entrypoints/openai/serving_speech.py and +# vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_tokenizer.py). +# +# Streaming is wired through the stage-input processor's async-chunk +# callback, the shared-memory connector ``codec_streaming`` flag, and the +# WAV/PCM streaming convention in serving_speech.py. To run a strictly +# non-streaming smoke test, set ``codec_streaming: false`` in the connector +# extras below. +async_chunk: false + +connectors: + connector_of_shared_memory: + name: SharedMemoryConnector + extra: + shm_threshold_bytes: 65536 + codec_streaming: true + connector_get_sleep_s: 0.01 + connector_get_max_wait_first_chunk: 5000 + connector_get_max_wait: 500 + codec_chunk_frames: 25 + codec_left_context_frames: 25 + initial_codec_chunk_frames: 1 + +stages: + - stage_id: 0 + max_num_seqs: 4 + gpu_memory_utilization: 0.6 + # Disable cudagraph capture: the live audio-sampler dispatch is still + # iterating, and the host's flash-attn-hopper graph path tripped a CUDA + # driver vs runtime mismatch during capture on this image. + enforce_eager: true + trust_remote_code: true + enable_prefix_caching: false + async_scheduling: false + max_num_batched_tokens: 2048 + # max_model_len covers prompt + max_new_tokens. Up to ~1500 codec + # frames per request fit comfortably under 4096. + max_model_len: 4096 + devices: "0" + output_connectors: + to_stage_1: connector_of_shared_memory + default_sampling_params: + # Upstream serve_engine defaults (temperature=1.0, top_p=0.95, + # top_k=50). Greedy (temperature=0) collapses the audio codebook + # argmax to a fixed value after a few steps; sampling with diversity + # is required for natural-sounding speech. + temperature: 1.0 + top_p: 0.95 + top_k: 50 + # Cap Stage-0 to 1024 audio frames (~40 s at 25 fps). The audio-eos + # natural stop (128012, forced by the talker's _apply_audio_mode_bias + # at ramp-completion) typically fires earlier and trims this. + max_tokens: 1024 + seed: 42 + repetition_penalty: 1.0 + + - stage_id: 1 + max_num_seqs: 4 + gpu_memory_utilization: 0.25 + enforce_eager: true + trust_remote_code: true + enable_prefix_caching: false + async_scheduling: false + # Prefill length is num_codebooks * num_frames; 8 * 8192 = 65536 covers + # the upper bound at the per-request 250-frame Stage-0 cap above plus + # headroom for batching multiple requests. + max_num_batched_tokens: 65536 + max_model_len: 65536 + devices: "0" + input_connectors: + from_stage_0: connector_of_shared_memory + default_sampling_params: + temperature: 0.0 + top_p: 1.0 + top_k: -1 + max_tokens: 65536 + seed: 42 + repetition_penalty: 1.0 diff --git a/vllm_omni/entrypoints/openai/protocol/audio.py b/vllm_omni/entrypoints/openai/protocol/audio.py index 3ec2424c0..0325017b4 100644 --- a/vllm_omni/entrypoints/openai/protocol/audio.py +++ b/vllm_omni/entrypoints/openai/protocol/audio.py @@ -100,6 +100,50 @@ class OpenAICreateSpeechRequest(BaseModel): raise ValueError("'speaker_embedding' values must be finite (no NaN or Inf)") return v + @model_validator(mode="before") + @classmethod + def reject_higgs_audio_v2_unsupported_aliases(cls, data): + """Reject unsupported rich-input aliases for higgs_audio_v2 BEFORE pydantic strips them. + + OpenAICreateSpeechRequest is a permissive schema (it accepts any extra + field that the OpenAI Speech API didn't promise to ban) but several + of those aliases pull a request out of the v1 higgs_audio_v2 scope. + Catching them at parse time lets the API return a deterministic 4xx + with a model-specific error message instead of silently dropping the + field and proceeding with a degraded request. + """ + # Keys that are silently dropped by pydantic when posted to /v1/audio/speech + # (the schema doesn't declare them) but which a model-specific + # validator MUST be able to reject. Kept inline to avoid clashing + # with pydantic's ModelPrivateAttr discovery for class-level + # underscore-prefixed attributes. + higgs_audio_v2_reserved_keys = ( + "messages", # ChatML rich content (out of scope for v1) + "reference_audio", # voice-cloning alias 1 + "voice_prompt", # voice-cloning alias 2 + "speaker_audio", # voice-cloning alias 3 + "speakers", # multi-speaker dialogue + ) + if not isinstance(data, dict): + return data + model_id = data.get("model") + if not isinstance(model_id, str): + return data + # Match the "higgs_audio_v2" model_type label, the HF architecture id + # in pipeline_registry.hf_architectures, and the hyphenated HF repo id + # (e.g. "bosonai/higgs-audio-v2-generation-3B-base") by normalizing + # both underscores and hyphens out of the id before substring matching. + normalized = model_id.lower().replace("-", "").replace("_", "") + if "higgsaudiov2" not in normalized: + return data + offending = sorted(k for k in higgs_audio_v2_reserved_keys if k in data) + if offending: + raise ValueError( + "higgs_audio_v2 v1 does not support these rich-input fields: " + f"{offending}. Supply plain text via the 'input' field instead." + ) + return data + @model_validator(mode="after") def validate_embedding_constraints(self) -> "OpenAICreateSpeechRequest": if self.speaker_embedding is not None: diff --git a/vllm_omni/entrypoints/openai/serving_speech.py b/vllm_omni/entrypoints/openai/serving_speech.py index 7643ed625..4497f0409 100644 --- a/vllm_omni/entrypoints/openai/serving_speech.py +++ b/vllm_omni/entrypoints/openai/serving_speech.py @@ -69,6 +69,7 @@ _COVO_AUDIO_MODEL_STAGES = {"fused_thinker_talker"} _VOXCPM2_TTS_MODEL_STAGES = {"latent_generator"} _MING_TTS_MODEL_STAGES = {"ming_tts"} _MOSS_TTS_MODEL_STAGES = {"moss_tts_nano"} +_HIGGS_AUDIO_V2_TTS_MODEL_STAGES = {"higgs_audio_v2"} _GLM_TTS_MODEL_STAGES = {"glm_tts"} _TTS_MODEL_STAGES: set[str] = ( _VOXTRAL_TTS_MODEL_STAGES @@ -76,13 +77,21 @@ _TTS_MODEL_STAGES: set[str] = ( | _FISH_TTS_MODEL_STAGES | _COSYVOICE3_TTS_MODEL_STAGES | _OMNIVOICE_TTS_MODEL_STAGES + | _HIGGS_AUDIO_V2_TTS_MODEL_STAGES | _COVO_AUDIO_MODEL_STAGES | _VOXCPM2_TTS_MODEL_STAGES | _MING_TTS_MODEL_STAGES | _MOSS_TTS_MODEL_STAGES | _GLM_TTS_MODEL_STAGES ) -_SAMPLING_MAX_TOKENS_TTS_MODEL_TYPES = {"fish_tts", "qwen3_tts", "voxtral_tts", "cosyvoice3", "voxcpm2"} +_SAMPLING_MAX_TOKENS_TTS_MODEL_TYPES = { + "fish_tts", + "qwen3_tts", + "voxtral_tts", + "cosyvoice3", + "voxcpm2", + "higgs_audio_v2", +} _TTS_LANGUAGES: set[str] = { "Auto", "Chinese", @@ -555,6 +564,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): return "ming_flash_omni_tts" if model_stage in _MOSS_TTS_MODEL_STAGES: return "moss_tts_nano" + if model_stage in _HIGGS_AUDIO_V2_TTS_MODEL_STAGES: + return "higgs_audio_v2" if model_stage in _GLM_TTS_MODEL_STAGES: return "glm_tts" return None @@ -845,12 +856,20 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): voice_lower = request.voice.lower() if voice_lower not in self.uploaded_speakers: - if self._tts_model_type in ("cosyvoice3", "fish_tts", "omnivoice", "moss_tts_nano", "glm_tts"): + if self._tts_model_type in ( + "cosyvoice3", + "fish_tts", + "omnivoice", + "moss_tts_nano", + "glm_tts", + "higgs_audio_v2", + ): label = { "cosyvoice3": "CosyVoice3", "fish_tts": "Fish Speech", "omnivoice": "OmniVoice", "moss_tts_nano": "MOSS-TTS-Nano", + "higgs_audio_v2": "Higgs-Audio V2", "glm_tts": "GLM-TTS", }.get(self._tts_model_type, self._tts_model_type) return ( @@ -1446,6 +1465,73 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): params["prompt_audio_array"] = [[wav_list, sr]] return params + def _validate_higgs_audio_v2_request(self, request: OpenAICreateSpeechRequest) -> str | None: + """Validate higgs_audio_v2 request parameters. Returns error message or None. + + Accepted: plain text -> speech, or shallow voice clone via ``ref_audio`` + + ``ref_text`` (both required together). Still out of scope: preset + ``voice``/``speaker`` selection, ``x_vector_only_mode`` / + ``speaker_embedding`` helpers, ``task_type``/``language``/ + ``instructions``/``speed`` overrides, and multi-speaker ``[SPEAKERn]`` + tags inside the input body. + """ + from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_v2_tokenizer import ( + MULTI_SPEAKER_TAG_PATTERN, + ) + + if not request.input or not request.input.strip(): + return "higgs_audio_v2: input text cannot be empty" + + # Voice clone: ref_audio and ref_text must come together. + if request.ref_audio is not None and not request.ref_text: + return ( + "higgs_audio_v2 voice clone requires both 'ref_audio' and " + "'ref_text'; received ref_audio without ref_text" + ) + if request.ref_text and request.ref_audio is None: + return ( + "higgs_audio_v2 voice clone requires both 'ref_audio' and " + "'ref_text'; received ref_text without ref_audio" + ) + + if request.x_vector_only_mode is not None: + return "higgs_audio_v2 v1 does not support 'x_vector_only_mode' (voice-cloning helper field)" + if request.speaker_embedding is not None: + return "higgs_audio_v2 v1 does not support 'speaker_embedding' (voice-cloning helper field)" + if request.voice and request.ref_audio is None: + # _apply_uploaded_speaker runs before this validator; if voice was + # an uploaded speaker, ref_audio is now populated and ref_text is + # backfilled from the speaker entry. A bare voice= with no + # ref_audio means the name didn't resolve to an uploaded speaker + # (and higgs has no built-in preset voices). + return ( + "higgs_audio_v2 v1 does not support 'voice'/'speaker' selection for built-in voices; " + f"upload a voice first via POST /v1/audio/voices, or use ref_audio + ref_text. " + f"Got voice={request.voice!r}" + ) + if request.instructions: + return ( + "higgs_audio_v2 v1 does not support 'instructions' (voice " + "style/emotion control); supply plain text instead" + ) + if request.task_type is not None: + return "higgs_audio_v2 v1 does not support 'task_type'; the model is single-mode plain text -> speech" + if request.language is not None: + return ( + "higgs_audio_v2 v1 does not accept 'language' overrides; the model infers language from the input text" + ) + if request.speed is not None and request.speed != 1.0: + return ( + "higgs_audio_v2 v1 does not support 'speed' adjustments; the audio is rendered at native rate (24 kHz)" + ) + + if MULTI_SPEAKER_TAG_PATTERN.search(request.input): + return "higgs_audio_v2 v1 does not support multi-speaker [SPEAKERn] tags; remove the tag from the input" + + if request.max_new_tokens is not None: + if request.max_new_tokens < _TTS_MAX_NEW_TOKENS_MIN: + return f"max_new_tokens must be at least {_TTS_MAX_NEW_TOKENS_MIN}" + def _validate_glm_tts_request(self, request: OpenAICreateSpeechRequest) -> str | None: """Validate GLM-TTS request — requires ref_audio for voice cloning.""" if not request.input or not request.input.strip(): @@ -1466,6 +1552,80 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): return f"max_new_tokens cannot exceed {_TTS_MAX_NEW_TOKENS_MAX}" return None + async def _build_higgs_audio_v2_params(self, request: OpenAICreateSpeechRequest): + """Build prompt_token_ids for higgs_audio_v2 via the upstream processor. + + Plain-text path: runs ``build_plain_text_prompt`` and returns the + token-only prompt. Voice-clone path (``ref_audio`` + ``ref_text``): + resolves the reference clip via ``_resolve_ref_audio``, runs + ``build_voice_clone_prompt`` (which encodes the clip through HF's + ``HiggsAudioV2TokenizerModel`` loaded from the k2-fsa/OmniVoice + ``audio_tokenizer/`` subdirectory), and attaches the encoded + ``audio_input_ids`` + ``audio_input_ids_mask`` tensors via + ``additional_information`` so the talker substitutes them at the + prompt-side audio placeholders. + """ + from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_v2_tokenizer import ( + build_plain_text_prompt, + build_voice_clone_prompt, + input_ids_to_python_list, + ) + + processor = await self._resolve_higgs_audio_v2_processor() + + if request.ref_audio is None: + inputs = await asyncio.to_thread(build_plain_text_prompt, processor, request.input) + prompt_token_ids = input_ids_to_python_list(inputs) + return tokens_input(prompt_token_ids=prompt_token_ids) + + wav_list, sr = await self._resolve_ref_audio(request.ref_audio) + wav = np.asarray(wav_list, dtype=np.float32) + out = await asyncio.to_thread( + build_voice_clone_prompt, + processor, + request.input, + wav, + int(sr), + request.ref_text or "", + ) + prompt = tokens_input(prompt_token_ids=out["prompt_token_ids"]) + # Pass tensors at the top level of additional_information (NOT list- + # wrapped). ``vllm_omni.data_entry_keys.serialize_payload`` routes + # bare ``torch.Tensor`` values through ``_serialize_tensor``; a list + # containing tensors would fall into the ``list_data`` field which + # msgspec cannot serialize and the tensors would be dropped over the + # process boundary (silent voice-clone failure). + prompt["additional_information"] = { + "audio_input_ids": out["audio_input_ids"], + "audio_input_ids_mask": out["audio_input_ids_mask"], + } + return prompt + + async def _resolve_higgs_audio_v2_processor(self): + """Lazy-load the AutoProcessor for higgs_audio_v2 (once per serving instance).""" + cached = getattr(self, "_higgs_audio_v2_processor", None) + if cached is not None: + return cached + + from transformers import AutoProcessor + + model_path = None + for stage in self.engine_client.stage_configs: + model_path = getattr(getattr(stage, "engine_args", None), "model", None) + if model_path: + break + if model_path is None: + # Fallback: the orchestrator stores the served model id on the engine + # itself (set by AsyncOmniEngine.__init__). Stage-level engine_args + # may not surface ``model`` when the deploy yaml doesn't set it per + # stage (the CLI-passed model id is the single source of truth). + model_path = getattr(self.engine_client, "model", None) + if model_path is None: + raise RuntimeError("higgs_audio_v2 serving could not resolve the model path from the engine stage configs") + processor = AutoProcessor.from_pretrained(model_path) + self._higgs_audio_v2_processor = processor + return processor + def _validate_fish_tts_request(self, request: OpenAICreateSpeechRequest) -> str | None: """Validate Fish Speech request parameters. Returns error message or None.""" if not request.input or not request.input.strip(): @@ -2226,6 +2386,29 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): elif self._tts_model_type == "covo_audio": prompt = self._build_covo_audio_prompt(request) tts_params = {} + elif self._tts_model_type == "higgs_audio_v2": + # Explicit higgs_audio_v2 branch: do NOT fall through to the + # Qwen-style generic placeholder path; build prompt_token_ids + # directly via the upstream processor + the project's plain-text + # helper. + # + # Resolve uploaded voices first so a voice= request (after + # POST /v1/audio/voices) populates ref_audio + ref_text from the + # cached speaker entry, mirroring the cosyvoice3 / voxcpm2 flow. + err = self._apply_uploaded_speaker(request) + if err: + raise ValueError(err) + validation_error = self._validate_higgs_audio_v2_request(request) + if validation_error: + raise ValueError(validation_error) + prompt = await self._build_higgs_audio_v2_params(request) + if request.voice: + voice_lower = request.voice.lower() + if voice_lower in self.uploaded_speakers and not has_inline_ref_audio: + additional = prompt.setdefault("additional_information", {}) + additional["voice_name"] = voice_lower + additional["voice_created_at"] = self._voice_created_at(voice_lower) + tts_params = {} elif self._tts_model_type == "voxcpm2": # voxcpm2 doesn't use `_apply_uploaded_speaker` because the prompt builder needs the # raw waveform tuple for prefill-length accounting, not a base64 data URL. @@ -2338,6 +2521,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin): model_type = "ming_flash_omni_tts" elif self._tts_model_type == "moss_tts_nano": model_type = "moss_tts_nano" + elif self._tts_model_type == "higgs_audio_v2": + model_type = "higgs_audio_v2" elif self._tts_model_type == "glm_tts": model_type = "glm_tts" elif self._is_tts: diff --git a/vllm_omni/model_executor/models/higgs_audio_v2/__init__.py b/vllm_omni/model_executor/models/higgs_audio_v2/__init__.py new file mode 100644 index 000000000..1a7c23eba --- /dev/null +++ b/vllm_omni/model_executor/models/higgs_audio_v2/__init__.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""vllm-omni integration for boson-ai's higgs-audio v2 (two-stage TTS).""" + +from vllm_omni.model_executor.models.higgs_audio_v2.configuration_higgs_audio_v2 import ( + HiggsAudioV2Config, +) + +__all__ = ["HiggsAudioV2Config"] diff --git a/vllm_omni/model_executor/models/higgs_audio_v2/configuration_higgs_audio_v2.py b/vllm_omni/model_executor/models/higgs_audio_v2/configuration_higgs_audio_v2.py new file mode 100644 index 000000000..dc5eb12b4 --- /dev/null +++ b/vllm_omni/model_executor/models/higgs_audio_v2/configuration_higgs_audio_v2.py @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Configuration class for higgs-audio v2 in vllm-omni. + +Mirrors `bosonai/higgs-audio-v2-generation-3B-base/config.json` and exposes the +Stage-1 codec decoder reference (sample rate, codebook layout, stream-special +token ids) so the rest of the integration can read a single typed object. + +The text-LM backbone constants match `Llama-3.2-3B` (hidden=3072, 28 layers, +GQA 24Q/8KV, head_dim=128, vocab=128256, max_position_embeddings=2048, RoPE +``llama3`` scaling with factor=32). The audio side adds 8 codebooks of size +1026 (real codes ``[0..1023]``, stream specials ``audio_stream_bos_id=1024`` +and ``audio_stream_eos_id=1025``) plus the DualFFN audio expert. +""" + +from __future__ import annotations + +from typing import Any + +from transformers import PretrainedConfig + +__all__ = ["HiggsAudioV2Config"] + + +class HiggsAudioV2Config(PretrainedConfig): + """Typed wrapper around the upstream `higgs_audio_v2` config. + + The field set here is a strict subset of the upstream + ``transformers.models.higgs_audio_v2.HiggsAudioV2Config``: only the + knobs that vllm-omni reads directly are surfaced. Anything extra coming + from the HF config dict is preserved via ``PretrainedConfig`` so + ``AutoConfig.from_pretrained(...)`` round-trips correctly. + """ + + model_type: str = "higgs_audio_v2" + keys_to_ignore_at_inference = ("past_key_values",) + + # Architecture identifier persisted in config.json and used by both + # vllm_omni.config.pipeline_registry and vllm_omni.model_executor.models.registry. + DEFAULT_ARCHITECTURES = ("HiggsAudioV2ForConditionalGeneration",) + + def __init__( + self, + # --- Text LM (Llama-3.2-3B) --- + vocab_size: int = 128256, + hidden_size: int = 3072, + intermediate_size: int = 8192, + num_hidden_layers: int = 28, + num_attention_heads: int = 24, + num_key_value_heads: int = 8, + head_dim: int = 128, + hidden_act: str = "silu", + max_position_embeddings: int = 2048, + rms_norm_eps: float = 1e-5, + rope_parameters: dict[str, Any] | None = None, + initializer_range: float = 0.02, + attention_bias: bool = False, + attention_dropout: float = 0.0, + mlp_bias: bool = False, + tie_word_embeddings: bool = False, + pretraining_tp: int = 1, + # --- Audio side --- + num_codebooks: int = 8, + codebook_size: int = 1026, + audio_token_id: int = 128016, + audio_bos_token_id: int = 128013, + audio_eos_token_id: int = 128012, + audio_delay_token_id: int = 128014, + audio_stream_bos_id: int = 1024, + audio_stream_eos_id: int = 1025, + sample_rate: int = 24000, + frame_rate: int = 25, + # --- Standard LM specials --- + bos_token_id: int = 1, + eos_token_id: int = 128009, + pad_token_id: int = 128001, + # --- DualFFN --- + # Upstream `HiggsAudioV2DecoderLayer` always carries a parallel `audio_mlp` + # expert; this flag is kept for forward-compat in case a future config + # disables it (no-op for the boson-ai checkpoint). + use_audio_dual_ffn: bool = True, + # --- Stage-1 tokenizer / codec decoder --- + # Path to the audio-tokenizer subdir (relative to the model directory). + # Empty string means the model_dir itself contains config.json + + # model.safetensors (the layout used by the standalone + # ``bosonai/higgs-audio-v2-tokenizer`` HF repo). Non-empty means the + # tokenizer files live in ``//`` (the layout used by + # the 3B Stage-0 checkpoint which bundles its tokenizer under + # ``audio_tokenizer/``). + audio_tokenizer_subdir: str = "", + # HF repo id (or local path) of a STANDALONE audio tokenizer. When set, + # Stage-1 resolves this via ``huggingface_hub.snapshot_download`` and + # loads the codec from there, ignoring ``audio_tokenizer_subdir``. This + # matches the boson-ai release layout where the codec lives in a + # separate repo (``bosonai/higgs-audio-v2-tokenizer``). + audio_tokenizer_id: str | None = "bosonai/higgs-audio-v2-tokenizer", + # --- Misc --- + use_cache: bool = True, + **kwargs: Any, + ) -> None: + if rope_parameters is None: + rope_parameters = { + "factor": 32.0, + "high_freq_factor": 0.5, + "low_freq_factor": 0.125, + "original_max_position_embeddings": 1024, + "rope_theta": 500000.0, + "rope_type": "llama3", + } + + super().__init__( + bos_token_id=bos_token_id, + eos_token_id=eos_token_id, + pad_token_id=pad_token_id, + tie_word_embeddings=tie_word_embeddings, + **kwargs, + ) + + self.vocab_size = vocab_size + self.hidden_size = hidden_size + self.intermediate_size = intermediate_size + self.num_hidden_layers = num_hidden_layers + self.num_attention_heads = num_attention_heads + self.num_key_value_heads = num_key_value_heads + self.head_dim = head_dim + self.hidden_act = hidden_act + self.max_position_embeddings = max_position_embeddings + self.rms_norm_eps = rms_norm_eps + self.rope_parameters = dict(rope_parameters) + self.initializer_range = initializer_range + self.attention_bias = attention_bias + self.attention_dropout = attention_dropout + self.mlp_bias = mlp_bias + self.pretraining_tp = pretraining_tp + + self.num_codebooks = num_codebooks + self.codebook_size = codebook_size + self.audio_token_id = audio_token_id + self.audio_bos_token_id = audio_bos_token_id + self.audio_eos_token_id = audio_eos_token_id + self.audio_delay_token_id = audio_delay_token_id + self.audio_stream_bos_id = audio_stream_bos_id + self.audio_stream_eos_id = audio_stream_eos_id + self.sample_rate = sample_rate + self.frame_rate = frame_rate + + self.use_audio_dual_ffn = use_audio_dual_ffn + self.audio_tokenizer_subdir = audio_tokenizer_subdir + self.audio_tokenizer_id = audio_tokenizer_id + self.use_cache = use_cache + + # Codes [0..codebook_size-1] include 2 stream specials at indices + # `audio_stream_bos_id` and `audio_stream_eos_id`. Real codes ready for the + # codec decoder are the strict prefix [0..audio_stream_bos_id). + @property + def num_real_codes(self) -> int: + return self.audio_stream_bos_id # i.e. 1024 for the boson-ai checkpoint + + # The full vocab size of each per-codebook output head. + @property + def codebook_output_size(self) -> int: + return self.codebook_size # i.e. 1026 for the boson-ai checkpoint + + # Canonical MusicGen-style delay pattern used by + # `HiggsAudioV2DelayPatternLogitsProcessor`: codebook k starts emitting + # real codes only after k frames. Kept as a function so subclasses can + # override if upstream ever publishes a different convention. + def default_delay_pattern(self) -> list[int]: + return list(range(self.num_codebooks)) diff --git a/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_decoder.py b/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_decoder.py new file mode 100644 index 000000000..74cdbb2db --- /dev/null +++ b/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_decoder.py @@ -0,0 +1,471 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""HiggsAudio codec decoder kernel for higgs-audio v2. + +This module hosts the parameter-side building blocks for the higgs-audio-v2 +audio tokenizer's decoder path: + + audio_codes [B, 8, T] + -> RVQ codebook lookup + project_out -> sum -> [B, hidden_size, T] + -> fc2 Linear(hidden_size, 256) -> [B, 256, T] + -> DAC acoustic decoder (conv-transpose upsampling) -> [B, 1, T*960] + -> 24 kHz waveform (25 fps x 960 samples/frame) +""" + +from __future__ import annotations + +import json +import os +from typing import Any + +import torch +import torch.nn as nn + +__all__ = [ + "HiggsAudioVQLayer", + "HiggsAudioRVQ", + "adjust_conv_transpose_output_padding", + "build_higgs_audio_acoustic_decoder", + "BosonDacDecoder", + "build_boson_dac_decoder", + "load_higgs_audio_codec", + "_remap_boson_model_pth_state_dict", # exported for unit-testing the mapper +] + + +class HiggsAudioVQLayer(nn.Module): + """Single VQ layer: codebook lookup + project_out.""" + + def __init__(self, codebook_size: int = 1024, codebook_dim: int = 64, hidden_size: int = 1024): + super().__init__() + self.codebook = nn.Embedding(codebook_size, codebook_dim) + self.project_out = nn.Linear(codebook_dim, hidden_size) + + def decode(self, indices: torch.Tensor) -> torch.Tensor: + """indices: [B, T] -> [B, hidden_size, T].""" + quantized = self.codebook(indices) + quantized = self.project_out(quantized) + return quantized.permute(0, 2, 1) + + +class HiggsAudioRVQ(nn.Module): + """Residual Vector Quantizer with ``num_quantizers`` codebook layers.""" + + def __init__( + self, + num_quantizers: int = 8, + codebook_size: int = 1024, + codebook_dim: int = 64, + hidden_size: int = 1024, + ): + super().__init__() + self.quantizers = nn.ModuleList( + [HiggsAudioVQLayer(codebook_size, codebook_dim, hidden_size) for _ in range(num_quantizers)] + ) + + def decode(self, codes: torch.Tensor) -> torch.Tensor: + """codes: [num_quantizers, B, T] -> [B, hidden_size, T].""" + result = torch.zeros( + codes.shape[1], + self.quantizers[0].project_out.out_features, + codes.shape[2], + device=codes.device, + dtype=torch.float32, + ) + for i, quantizer in enumerate(self.quantizers): + result = result + quantizer.decode(codes[i]) + return result + + +@torch.jit.script +def _snake(x: torch.Tensor, alpha: torch.Tensor) -> torch.Tensor: + shape = x.shape + x = x.reshape(shape[0], shape[1], -1) + x = x + (alpha + 1e-9).reciprocal() * torch.sin(alpha * x).pow(2) + return x.reshape(shape) + + +class _Snake1d(nn.Module): + """Snake activation (per-channel learnable alpha). Mirrors the boson-ai DAC + Snake1d implementation byte-for-byte so the boson ``model.pth`` state-dict + loads directly into ``self.alpha``. + """ + + def __init__(self, channels: int): + super().__init__() + self.alpha = nn.Parameter(torch.ones(1, channels, 1)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return _snake(x, self.alpha) + + +def _wn_conv1d(*args: Any, **kwargs: Any) -> nn.Module: + from torch.nn.utils import weight_norm + + return weight_norm(nn.Conv1d(*args, **kwargs)) + + +def _wn_conv_transpose1d(*args: Any, **kwargs: Any) -> nn.Module: + from torch.nn.utils import weight_norm + + return weight_norm(nn.ConvTranspose1d(*args, **kwargs)) + + +class _BosonResidualUnit(nn.Module): + def __init__(self, dim: int, dilation: int = 1): + super().__init__() + pad = ((7 - 1) * dilation) // 2 + self.block = nn.Sequential( + _Snake1d(dim), + _wn_conv1d(dim, dim, kernel_size=7, dilation=dilation, padding=pad), + _Snake1d(dim), + _wn_conv1d(dim, dim, kernel_size=1), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + y = self.block(x) + pad = (x.shape[-1] - y.shape[-1]) // 2 + if pad > 0: + x = x[..., pad:-pad] + return x + y + + +class _BosonDecoderBlock(nn.Module): + def __init__(self, input_dim: int, output_dim: int, stride: int): + super().__init__() + import math + + self.block = nn.Sequential( + _Snake1d(input_dim), + _wn_conv_transpose1d( + input_dim, + output_dim, + kernel_size=2 * stride, + stride=stride, + padding=math.ceil(stride / 2), + output_padding=stride % 2, + ), + _BosonResidualUnit(output_dim, dilation=1), + _BosonResidualUnit(output_dim, dilation=3), + _BosonResidualUnit(output_dim, dilation=9), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.block(x) + + +class BosonDacDecoder(nn.Module): + """Vendored copy of ``boson_multimodal``'s standalone DAC decoder, with the + exact ``decoder_2.model..{...}`` key layout the boson-ai + ``bosonai/higgs-audio-v2-tokenizer`` checkpoint expects. + + See ``boson_multimodal/audio_processing/descriptaudiocodec/dac/model/dac.py`` + (``class Decoder``) in the upstream package for the reference implementation + we mirror. + + Architecture for the v2 24 kHz tokenizer: + input_channel=256, channels=1024, rates=[8, 5, 4, 2, 3], d_out=1 + Cumulative upsample factor is ``prod(rates) = 960`` (25 fps -> 24 kHz). + """ + + def __init__( + self, + input_channel: int = 256, + channels: int = 1024, + rates: tuple[int, ...] = (8, 5, 4, 2, 3), + d_out: int = 1, + ): + super().__init__() + layers: list[nn.Module] = [_wn_conv1d(input_channel, channels, kernel_size=7, padding=3)] + last_dim = channels + for i, stride in enumerate(rates): + input_dim = channels // 2**i + output_dim = channels // 2 ** (i + 1) + layers.append(_BosonDecoderBlock(input_dim, output_dim, stride)) + last_dim = output_dim + layers.append(_Snake1d(last_dim)) + layers.append(_wn_conv1d(last_dim, d_out, kernel_size=7, padding=3)) + self.model = nn.Sequential(*layers) + # Cache hop length (= prod(rates)) so callers don't need to recompute it. + hop = 1 + for s in rates: + hop *= int(s) + self._hop_length = hop + + @property + def hop_length(self) -> int: + return self._hop_length + + # Expose conv1 with a ``.in_channels`` attribute so the existing fc2 + # fallback shape inference (acoustic_decoder.conv1.in_channels) works + # unchanged for the boson decoder too. + @property + def conv1(self) -> nn.Module: + return self.model[0] + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.model(x) + + +def build_boson_dac_decoder(device: torch.device) -> BosonDacDecoder: + """Construct the vendored boson DAC decoder on ``device``. + + The architectural constants (input_channel=256, channels=1024, + rates=[8,5,4,2,3], d_out=1) are baked in because they're invariant for the + v2 24 kHz tokenizer ``bosonai/higgs-audio-v2-tokenizer`` checkpoint we + target. Other channel counts are not supported until we encounter a + different boson checkpoint in the wild. + """ + return BosonDacDecoder().to(device) + + +def adjust_conv_transpose_output_padding(decoder: nn.Module) -> None: + """Set ConvTranspose1d output_padding = stride % 2 (HiggsAudioV2 modification). + + The vanilla DAC decoder ships with the default output_padding (0); the + boson-ai checkpoint expects ``stride % 2`` instead. This is a no-op for + even strides and adds a single sample for odd strides. + """ + for module in decoder.modules(): + if isinstance(module, nn.ConvTranspose1d): + stride = module.stride[0] if isinstance(module.stride, tuple) else module.stride + module.output_padding = (stride % 2,) + + +def build_higgs_audio_acoustic_decoder( + tokenizer_config: dict[str, Any], + device: torch.device, +) -> nn.Module: + """Build the DAC acoustic decoder used by the HiggsAudioV2 tokenizer. + + Returns the decoder sub-module of ``transformers.DacModel`` with the + HiggsAudioV2 output-padding fix already applied. The tanh activation is + replaced with ``Identity`` so the network matches the boson-ai checkpoint. + Weights are NOT loaded here; the caller is responsible for copying them in. + """ + from transformers import DacConfig, DacModel + + dac_cfg = DacConfig(**tokenizer_config["acoustic_model_config"]) + dac_model = DacModel(dac_cfg) + decoder = dac_model.decoder.to(device) + adjust_conv_transpose_output_padding(decoder) + if hasattr(decoder, "tanh"): + decoder.tanh = nn.Identity() + return decoder + + +def _load_higgs_audio_state_dict(audio_tokenizer_dir: str, device: torch.device) -> dict[str, torch.Tensor]: + """Load the codec state dict from either layout used in the wild. + + Tries layouts in order: + 1. ``

/model.safetensors`` (OmniVoice-bundled layout used by + ``k2-fsa/OmniVoice/audio_tokenizer/``). + 2. ``/model.pth`` (boson-ai standalone ``bosonai/higgs-audio-v2-tokenizer`` + layout). The state-dict keys differ structurally + (``quantizer.vq.layers.._codebook.embed`` etc.) and are remapped to + the OmniVoice-style names this kernel expects via + :func:`_remap_boson_model_pth_state_dict`. + """ + safetensors_path = os.path.join(audio_tokenizer_dir, "model.safetensors") + pth_path = os.path.join(audio_tokenizer_dir, "model.pth") + # Preference order: try model.pth first when present (boson-ai standalone + # ships the codec there; its model.safetensors is a different artefact). + # Fall back to model.safetensors (the OmniVoice-bundled layout). + sd: dict[str, torch.Tensor] + if os.path.exists(pth_path): + sd = torch.load(pth_path, map_location=device, weights_only=False) + elif os.path.exists(safetensors_path): + from safetensors.torch import load_file + + sd = load_file(safetensors_path, device=str(device)) + else: + raise FileNotFoundError(f"Audio tokenizer weights not found at {pth_path} or {safetensors_path}") + # Run the boson-layout remap if needed; for the OmniVoice-bundled layout + # it is a no-op (no ``quantizer.vq.layers.*`` keys to rewrite). + if any(k.startswith("quantizer.vq.layers.") for k in sd): + sd = _remap_boson_model_pth_state_dict(sd) + return sd + + +def _remap_boson_model_pth_state_dict(sd: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: + """Translate boson-ai's standalone ``model.pth`` keys into OmniVoice-style names + that the shared kernel's RVQ + fc2 + DAC sites consume. + + Best-effort: only the RVQ-side keys map cleanly. The boson-ai decoder + uses ``decoder_2.model..weight_g/weight_v`` (DAC with weight-norm + + Snake activations) rather than the OmniVoice DAC layout; that side + requires either vendoring the upstream decoder module or rewriting the + DAC builder to consume weight-normed tensors. This function returns the + mapped RVQ keys plus any acoustic_decoder.* / fc2.* / fc.* tensors that + happen to share the OmniVoice names, and leaves the decoder-2 keys + untouched (the caller will see them as MISSING when copying into the + OmniVoice DAC decoder). + + Mapping (RVQ side only): + quantizer.vq.layers.._codebook.embed -> quantizer.quantizers..codebook.embed + quantizer.vq.layers..project_out.weight -> quantizer.quantizers..project_out.weight + quantizer.vq.layers..project_out.bias -> quantizer.quantizers..project_out.bias + """ + if not isinstance(sd, dict): + raise TypeError(f"expected a state dict, got {type(sd)!r}") + remapped: dict[str, torch.Tensor] = {} + for key, tensor in sd.items(): + if not isinstance(tensor, torch.Tensor): + continue + # Quantizer rewrite: vq.layers.._codebook.embed -> quantizers..codebook.embed + if key.startswith("quantizer.vq.layers."): + parts = key.split(".") + # parts[3] is the layer index, parts[4]+ is the tail + if len(parts) >= 5: + idx = parts[3] + tail = ".".join(parts[4:]) + # The actual codebook centroids the decode path uses live at + # ``_codebook.embed`` (a [codebook_size, codebook_dim] buffer). + # ``_codebook.embed_avg`` is the un-normalised EMA accumulator + # used during training (~cluster_size x bigger); MATCHING IT + # HERE produces a 4-5x amplified codebook and unintelligible + # PCM downstream. Use an EXACT-match check, not startswith. + if tail == "_codebook.embed": + new_key = f"quantizer.quantizers.{idx}.codebook.embed" + remapped[new_key] = tensor + continue + if tail.startswith("project_out."): + new_key = f"quantizer.quantizers.{idx}.{tail}" + remapped[new_key] = tensor + continue + # project_in / _codebook.cluster_size / _codebook.embed_avg / inited + # are encoder-side or training-only state that the decode path + # does not need; drop them. + continue + # boson decoder + fc_post2 -> our vendored kernel names. + if key.startswith("decoder_2."): + # decoder_2.model..{...} -> acoustic_decoder.model..{...} + new_key = "acoustic_decoder." + key[len("decoder_2.") :] + remapped[new_key] = tensor + continue + if key.startswith("fc_post2."): + # fc_post2.{weight,bias} -> fc2.{weight,bias} + new_key = "fc2." + key[len("fc_post2.") :] + remapped[new_key] = tensor + continue + # Anything else (acoustic_decoder.*, fc2.*, fc.*, fc_post*.*, decoder_2.*, + # decoder_semantic.*, semantic_model.*) passes through unchanged so + # the caller's lookup is unambiguous. + remapped[key] = tensor + return remapped + + +def load_higgs_audio_codec( + audio_tokenizer_dir: str, + device: torch.device, +) -> tuple[HiggsAudioRVQ, nn.Linear, nn.Module, dict[str, Any]]: + """Load the HiggsAudioV2 RVQ + fc2 + DAC decoder from a checkpoint folder. + + Accepts both layouts: ``/model.safetensors`` (OmniVoice-bundled) and + ``/model.pth`` (boson-ai standalone). The standalone path remaps + quantizer keys before consumption; structural decoder differences + (boson uses Snake + weight-norm) still leave some DAC parameters missing + when loading the standalone path -- those entries log warnings but the + RVQ side completes successfully. + + Args: + audio_tokenizer_dir: Path to a directory containing ``config.json`` + and EITHER ``model.safetensors`` (OmniVoice layout) OR + ``model.pth`` (boson-ai standalone layout). + device: Device to place the loaded modules and state dict on. + + Returns: + (quantizer, fc2, acoustic_decoder, tokenizer_config) + - quantizer: ``HiggsAudioRVQ`` with ``num_quantizers`` discovered from + the state dict (defaults to 8 for boson-ai checkpoints). + - fc2: ``nn.Linear`` projecting RVQ output (1024) into the DAC's + hidden dimension (typically 256). May be uninitialized when loaded + from the boson-ai standalone layout (no ``fc2.*`` keys present). + - acoustic_decoder: DAC decoder with HiggsAudioV2 output-padding fix + and tanh-replaced-by-Identity. Fully initialized for the OmniVoice + layout; partially initialized for boson-ai standalone (missing + ``decoder_2.*`` -> ``acoustic_decoder.*`` mapping is not yet + implemented; full upstream-decoder vendoring is the next step). + - tokenizer_config: The loaded ``config.json`` dict; useful for + callers that need ``sample_rate`` or other tokenizer metadata. + """ + config_path = os.path.join(audio_tokenizer_dir, "config.json") + + with open(config_path) as f: + tokenizer_config: dict[str, Any] = json.load(f) + + state_dict = _load_higgs_audio_state_dict(audio_tokenizer_dir, device) + + codebook_dim = tokenizer_config.get("codebook_dim", 64) + codebook_size = tokenizer_config.get("codebook_size", 1024) + # Discover hidden_size and num_quantizers from the (possibly remapped) state dict. + if "quantizer.quantizers.0.project_out.weight" not in state_dict: + raise KeyError( + "Codec state dict is missing 'quantizer.quantizers.0.project_out.weight'. " + "If you loaded a boson-ai standalone tokenizer, ensure the model.pth " + "remap fired (see _remap_boson_model_pth_state_dict)." + ) + hidden_size = state_dict["quantizer.quantizers.0.project_out.weight"].shape[0] + num_quantizers = sum( + 1 for k in state_dict if k.startswith("quantizer.quantizers.") and k.endswith(".codebook.embed") + ) + + quantizer = HiggsAudioRVQ( + num_quantizers=num_quantizers, + codebook_size=codebook_size, + codebook_dim=codebook_dim, + hidden_size=hidden_size, + ).to(device) + for i in range(num_quantizers): + prefix = f"quantizer.quantizers.{i}" + embed_key = f"{prefix}.codebook.embed" + if embed_key in state_dict: + quantizer.quantizers[i].codebook.weight.data.copy_(state_dict[embed_key]) + proj_out_w = f"{prefix}.project_out.weight" + proj_out_b = f"{prefix}.project_out.bias" + if proj_out_w in state_dict: + quantizer.quantizers[i].project_out.weight.data.copy_(state_dict[proj_out_w]) + if proj_out_b in state_dict: + quantizer.quantizers[i].project_out.bias.data.copy_(state_dict[proj_out_b]) + + # Decide which acoustic decoder we need. A boson-layout state dict (after + # _remap_boson_model_pth_state_dict) carries ``acoustic_decoder.model.0.weight_v`` + # (the WNConv first conv) which the OmniVoice DacModel doesn't have; the + # OmniVoice layout carries ``acoustic_decoder.block.0.*`` instead. + is_boson_layout = "acoustic_decoder.model.0.weight_v" in state_dict + if is_boson_layout: + acoustic_decoder = build_boson_dac_decoder(device) + boson_sd = { + k[len("acoustic_decoder.") :]: v for k, v in state_dict.items() if k.startswith("acoustic_decoder.") + } + load_report = acoustic_decoder.load_state_dict(boson_sd, strict=False) + if load_report.missing_keys: + raise RuntimeError( + "Boson DAC decoder is missing keys after load: " + f"{load_report.missing_keys[:5]}... ({len(load_report.missing_keys)} total)" + ) + # ``unexpected_keys`` is informational only. + else: + acoustic_decoder = build_higgs_audio_acoustic_decoder(tokenizer_config, device) + for name, param in acoustic_decoder.named_parameters(): + higgs_name = f"acoustic_decoder.{name}" + if higgs_name in state_dict: + param.data.copy_(state_dict[higgs_name]) + acoustic_decoder.eval() + + if "fc2.weight" in state_dict and "fc2.bias" in state_dict: + fc2_w = state_dict["fc2.weight"] + fc2_b = state_dict["fc2.bias"] + fc2 = nn.Linear(fc2_w.shape[1], fc2_w.shape[0]).to(device) + fc2.weight.data.copy_(fc2_w) + fc2.bias.data.copy_(fc2_b) + else: + # Last-resort fallback: shape fc2 from the decoder's first conv input + # channels so the matmul produces compatible shapes. Weights stay + # uninitialised — PCM quality will be noise on this path. + first_conv = getattr(acoustic_decoder, "conv1", None) + if first_conv is None or not hasattr(first_conv, "in_channels"): + raise RuntimeError("acoustic_decoder has no conv1.in_channels; cannot size fc2 fallback") + fc2 = nn.Linear(hidden_size, int(first_conv.in_channels)).to(device) + + return quantizer, fc2, acoustic_decoder, tokenizer_config diff --git a/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_code2wav.py b/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_code2wav.py new file mode 100644 index 000000000..6a14707ee --- /dev/null +++ b/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_code2wav.py @@ -0,0 +1,413 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Stage 1 (codec decoder) for higgs-audio v2. + +Two surfaces exposed: + +1. **Direct decode API** (kept for offline / unit tests): + ``decode_codes(audio_codes: [B, num_codebooks=8, T])`` -> ``[B, 1, T*hop]`` PCM. + ``forward_chunk(audio_codes, *, left_context_size, hop_length=960)`` + trims overlap from streamed PCM. ``forward(audio_codes)`` retains the same + single-tensor signature for backward compatibility with code that imports + the class directly. + +2. **vLLM stage runtime API** (used by the engine): + ``__init__(*, vllm_config, prefix)``, ``embed_input_ids``, + ``compute_logits=None``, plus a runtime ``forward(input_ids=..., positions=..., + runtime_additional_information=...)`` that takes flat codebook-major + ``input_ids`` (``[Q * num_frames]``) per request and returns an + :class:`vllm_omni.model_executor.models.output_templates.OmniOutput`. + +The kernel is the HiggsAudio codec helper in +``vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_decoder.py``; both +surfaces share the same RVQ + DAC weights. +""" + +from __future__ import annotations + +import os +from typing import Any + +import torch +import torch.nn as nn +from vllm.config import VllmConfig +from vllm.logger import init_logger + +from vllm_omni.model_executor.models.higgs_audio_v2.configuration_higgs_audio_v2 import ( + HiggsAudioV2Config, +) +from vllm_omni.model_executor.models.higgs_audio_v2.higgs_audio_decoder import ( + HiggsAudioRVQ, + load_higgs_audio_codec, +) +from vllm_omni.model_executor.models.output_templates import OmniOutput + +__all__ = [ + "HiggsAudioV2Code2Wav", + "HiggsAudioV2Code2WavForConditionalGeneration", +] + +logger = init_logger(__name__) + + +class HiggsAudioV2Code2Wav(nn.Module): + """Stage-1 codec decoder for higgs-audio v2. + + Constructor accepts either the vLLM-runtime signature (``*, vllm_config, + prefix``) or the direct config-object form (``HiggsAudioV2Code2Wav(config)``) + so both engine-side and unit-test callers work. When the runtime form is + used we read the higgs_audio_v2 config out of ``vllm_config.model_config.hf_config`` + and the model_path off ``vllm_config.model_config.model`` so the codec can + be loaded directly from disk. + """ + + input_modalities = "audio" + + def __init__( + self, + config: HiggsAudioV2Config | None = None, + *, + vllm_config: VllmConfig | None = None, + prefix: str = "", + ): + super().__init__() + + # Resolve config from either positional (unit-test) or kwarg (engine) form. + if vllm_config is not None: + hf_config = vllm_config.model_config.hf_config + if isinstance(hf_config, HiggsAudioV2Config): + self.config = hf_config + else: + self.config = HiggsAudioV2Config(**hf_config.to_dict()) + self._model_path: str | None = vllm_config.model_config.model + self.vllm_config: VllmConfig | None = vllm_config + else: + if config is None: + raise TypeError( + "HiggsAudioV2Code2Wav: provide either positional `config` " + "(HiggsAudioV2Config) or keyword `vllm_config` (VllmConfig)." + ) + self.config = config + self._model_path = None + self.vllm_config = None + + self.sample_rate: int = int(self.config.sample_rate) + self.num_codebooks: int = int(self.config.num_codebooks) + self.num_real_codes: int = int(self.config.num_real_codes) + # Each codec frame upsamples to 960 24 kHz samples (= 25 fps * 960 = 24000). + self.hop_length: int = 960 + + # Engine-runner hooks (Stage 1 has no token sampling). + self.have_multimodal_outputs = True + self.has_preprocess = False + self.has_postprocess = False + self.enable_update_additional_information = True + self.requires_raw_input_tokens = True + + # Populated by load_weights(). + self.quantizer: HiggsAudioRVQ | None = None + self.fc2: nn.Linear | None = None + self.acoustic_decoder: nn.Module | None = None + self._loaded: bool = False + + # When constructed via the engine path, eagerly load codec weights from + # the model directory. The engine's subsequent ``load_weights(weights)`` + # call (with the Stage-0 safetensors iterator) is then a cheap no-op. + if self._model_path is not None: + try: + self.load_codec_from_disk(self._model_path) + except FileNotFoundError as exc: + logger.warning("HiggsAudioV2Code2Wav: eager codec load deferred (%s)", exc) + + # ------------------------------------------------------------- engine hooks + def embed_input_ids(self, input_ids: torch.Tensor, **_: Any) -> torch.Tensor: + """Stage 1 ignores embeddings; vLLM's runner still needs a stable shape.""" + if input_ids.numel() == 0: + return torch.empty((0, 1), device=input_ids.device, dtype=torch.float32) + return torch.zeros((input_ids.shape[0], 1), device=input_ids.device, dtype=torch.float32) + + def compute_logits(self, hidden_states: Any, sampling_metadata: Any = None) -> None: + return None + + # ------------------------------------------------------------------ load + def load_weights(self, weights_or_model_dir, device: torch.device | None = None): + """Dual-signature load: + + - ``load_weights(weights_iterator)`` -- the vLLM engine path. The iterator + yields ``(name, tensor)`` pairs from the Stage-0 safetensors which DO NOT + contain codec weights, so we ignore them and eagerly load the codec from + ``self._model_path``. Returns the set of all our params as "initialized" + so the engine's loader passes its sanity check. + - ``load_weights(model_dir: str)`` -- the legacy file-based call used by + offline unit tests and ``end2end.py --mode stage1_only``. Returns ``None``. + """ + if not isinstance(weights_or_model_dir, (str, bytes, os.PathLike)): + # Engine call: consume the iterator (we don't need it) and eagerly + # load the codec from disk. + try: + for _ in weights_or_model_dir: + pass + except TypeError: + pass + if self._model_path is None: + raise RuntimeError( + "HiggsAudioV2Code2Wav.load_weights called via the engine path but " + "no model directory is known. Construct with vllm_config so the " + "constructor caches vllm_config.model_config.model." + ) + self.load_codec_from_disk(self._model_path, device=device) + return {name for name, _ in self.named_parameters()} + + # Legacy file-based call. + self.load_codec_from_disk(weights_or_model_dir, device=device) + return None + + def load_codec_from_disk(self, model_dir: str, device: torch.device | None = None) -> None: + """Load codec weights from disk (RVQ + fc2 + DAC decoder). + + Resolution order for the codec directory: + 1. If :attr:`HiggsAudioV2Config.audio_tokenizer_id` is set, treat it as + either a local path (if it exists on disk) or an HF repo id that we + resolve via ``huggingface_hub.snapshot_download``. This is the + default for the boson-ai release (codec in a separate repo). + 2. Otherwise fall back to ``/``. + """ + if device is None: + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + audio_tokenizer_path = self._resolve_audio_tokenizer_path(model_dir) + quantizer, fc2, acoustic_decoder, _tokenizer_config = load_higgs_audio_codec(audio_tokenizer_path, device) + if len(quantizer.quantizers) != self.num_codebooks: + raise ValueError( + f"checkpoint has {len(quantizer.quantizers)} quantizers but config.num_codebooks={self.num_codebooks}" + ) + self.quantizer = quantizer + self.fc2 = fc2 + self.acoustic_decoder = acoustic_decoder + self._loaded = True + logger.info( + "Loaded HiggsAudioV2Code2Wav: %d quantizers, fc2(%d->%d), sample_rate=%d", + len(self.quantizer.quantizers), + self.fc2.in_features, + self.fc2.out_features, + self.sample_rate, + ) + + def _resolve_audio_tokenizer_path(self, model_dir: str) -> str: + """Return a local filesystem path containing ``config.json`` + codec + weights. See :meth:`load_codec_from_disk` for the resolution rules.""" + tokenizer_id = getattr(self.config, "audio_tokenizer_id", None) + if tokenizer_id: + if os.path.isdir(tokenizer_id): + return tokenizer_id + return self._resolve_hf_id_to_local(tokenizer_id) + subdir = self.config.audio_tokenizer_subdir or "" + if os.path.isdir(model_dir): + return os.path.join(model_dir, subdir) if subdir else model_dir + local = self._resolve_hf_id_to_local(model_dir) + return os.path.join(local, subdir) if subdir else local + + @staticmethod + def _resolve_hf_id_to_local(repo_id: str) -> str: + """Resolve a HF repo id to a local snapshot directory. + + Prefers a read-only cache lookup (no write locks) so a quota-constrained + Lustre cache can still serve already-downloaded weights. Falls back to + ``snapshot_download`` only when nothing is cached locally. + """ + from huggingface_hub import try_to_load_from_cache + + cached = try_to_load_from_cache(repo_id=repo_id, filename="config.json") + if isinstance(cached, str) and os.path.isfile(cached): + return os.path.dirname(cached) + + from huggingface_hub.constants import HF_HUB_CACHE + + safe = repo_id.replace("/", "--") + snapshots_dir = os.path.join(HF_HUB_CACHE, f"models--{safe}", "snapshots") + if os.path.isdir(snapshots_dir): + for rev in os.listdir(snapshots_dir): + candidate = os.path.join(snapshots_dir, rev) + if os.path.isfile(os.path.join(candidate, "config.json")): + return candidate + + from huggingface_hub import snapshot_download + + return snapshot_download(repo_id=repo_id) + + # ------------------------------------------------------ direct decode API + @torch.inference_mode() + def decode_codes(self, audio_codes: torch.Tensor) -> torch.Tensor: + """Decode a ``[B, num_codebooks=8, T]`` code tensor to PCM ``[B, 1, T*hop]``.""" + if not self._loaded: + raise RuntimeError("HiggsAudioV2Code2Wav not loaded. Call load_weights() first.") + + codes = self._validate_codes(audio_codes) + device = codes.device + + rvq_codes = codes.transpose(0, 1).long() # [num_codebooks, B, T] + quantized = self.quantizer.decode(rvq_codes) # [B, hidden, T] + # Match fc2's parameter dtype — vLLM may auto-cast the codec to the + # stage's model dtype (e.g. bf16), while the quantizer's RVQ codebooks + # often stay in float32 because they're embedded lookups. A defensive + # cast keeps the matmul homogeneous. + quantized = quantized.to(dtype=self.fc2.weight.dtype) + quantized = self.fc2(quantized.transpose(1, 2)).transpose(1, 2) + # Acoustic decoder may have parameters in yet another dtype — match + # its first parameter so the linear/conv ops stay consistent. + first_param = next(self.acoustic_decoder.parameters(), None) + if first_param is not None and quantized.dtype != first_param.dtype: + quantized = quantized.to(dtype=first_param.dtype) + audio = self.acoustic_decoder(quantized) + if audio.dim() == 2: + audio = audio.unsqueeze(1) + return audio.to(device) + + @torch.inference_mode() + def forward_chunk( + self, + audio_codes: torch.Tensor, + *, + left_context_size: int = 0, + hop_length: int | None = None, + ) -> torch.Tensor: + """Chunked decode that trims ``left_context_size * hop_length`` samples + off the leading edge of the upsampled PCM. Mirrors the qwen3_tts + ``talker2code2wav_async_chunk`` overlap contract. + """ + if left_context_size < 0: + raise ValueError(f"left_context_size must be >= 0; got {left_context_size}") + hop = int(hop_length) if hop_length is not None else self.hop_length + if hop <= 0: + raise ValueError(f"hop_length must be > 0; got {hop}") + pcm = self.decode_codes(audio_codes) + if left_context_size == 0: + return pcm + trim = left_context_size * hop + if pcm.shape[-1] <= trim: + return pcm[..., :0] + return pcm[..., trim:] + + # ---------------------------------------------------- vLLM runtime forward + @torch.no_grad() + def forward( + self, + input_ids: torch.Tensor | None = None, + positions: torch.Tensor | None = None, + intermediate_tensors: Any = None, + inputs_embeds: torch.Tensor | None = None, + runtime_additional_information: list[dict[str, Any]] | None = None, + **kwargs: Any, + ) -> torch.Tensor | OmniOutput: + """Dual-mode forward. + + - When ``input_ids`` is a 3-D ``[B, num_codebooks, T]`` tensor we treat + this as a direct decode call (legacy API) and return raw PCM. + - When ``input_ids`` is a flat 1-D / 2-D tensor we treat it as the + engine runtime payload (codebook-major flat per request) and return + an :class:`OmniOutput` carrying multimodal audio. + """ + # Legacy direct-decode signature: caller passed a 3-D code tensor as input_ids. + if isinstance(input_ids, torch.Tensor) and input_ids.ndim == 3: + return self.decode_codes(input_ids) + + sr_val = int(self.sample_rate) + sr_tensor = torch.tensor(sr_val, dtype=torch.int32) + empty = torch.zeros((0,), dtype=torch.float32) + + if input_ids is None or input_ids.numel() == 0: + return OmniOutput( + text_hidden_states=None, + multimodal_outputs={"model_outputs": [empty], "sr": [sr_tensor]}, + ) + + ids = input_ids.reshape(-1).to(dtype=torch.long) + request_ids_list = self._split_request_ids(ids, kwargs.get("seq_token_counts")) + + left_context_size = [0] * len(request_ids_list) + if runtime_additional_information is not None: + for i, info in enumerate(runtime_additional_information): + if i >= len(left_context_size): + break + meta = info.get("meta", {}) if isinstance(info, dict) else {} + if "left_context_size" in meta: + left_context_size[i] = int(meta["left_context_size"]) + + wavs: list[torch.Tensor] = [] + for i, req_ids in enumerate(request_ids_list): + n = int(req_ids.numel()) + if n == 0: + wavs.append(empty) + continue + if n % self.num_codebooks != 0: + logger.warning( + "HiggsAudioV2Code2Wav: flat code length %d not divisible by num_codebooks=%d; dropping", + n, + self.num_codebooks, + ) + wavs.append(empty) + continue + frames = n // self.num_codebooks + codes_qf = req_ids.reshape(self.num_codebooks, frames) + codes_bqf = codes_qf.unsqueeze(0) + try: + pcm = self.forward_chunk( + codes_bqf, + left_context_size=left_context_size[i], + hop_length=self.hop_length, + ) + except ValueError as exc: + logger.warning("HiggsAudioV2Code2Wav: decode skipped (%s)", exc) + wavs.append(empty) + continue + wavs.append(pcm.squeeze(0).squeeze(0).to(torch.float32).cpu()) + + return OmniOutput( + text_hidden_states=None, + multimodal_outputs={ + "model_outputs": wavs, + "sr": [sr_tensor] * len(wavs), + }, + ) + + # --------------------------------------------------------------- helpers + def _validate_codes(self, audio_codes: torch.Tensor) -> torch.Tensor: + """Ensure shape and value range; reject stream specials with ValueError.""" + if not isinstance(audio_codes, torch.Tensor): + raise TypeError(f"audio_codes must be a torch.Tensor, got {type(audio_codes)!r}") + if audio_codes.ndim != 3: + raise ValueError( + f"audio_codes must have shape [B, num_codebooks={self.num_codebooks}, T]; " + f"got shape {tuple(audio_codes.shape)}" + ) + if int(audio_codes.shape[1]) != self.num_codebooks: + raise ValueError( + f"audio_codes second dim must equal num_codebooks={self.num_codebooks}; got {int(audio_codes.shape[1])}" + ) + if audio_codes.numel() > 0: + max_val = int(audio_codes.max().item()) + min_val = int(audio_codes.min().item()) + if max_val >= self.num_real_codes or min_val < 0: + raise ValueError( + "audio_codes contains stream-special or out-of-range IDs: " + f"min={min_val}, max={max_val}; real code range is " + f"[0, {self.num_real_codes - 1}]. Filter audio_stream_bos_id=" + f"{self.config.audio_stream_bos_id} and audio_stream_eos_id=" + f"{self.config.audio_stream_eos_id} (and anything above) at " + "Stage 0 before sending codes to the codec decoder." + ) + return audio_codes + + @staticmethod + def _split_request_ids(ids: torch.Tensor, seq_token_counts: list[int] | None = None) -> list[torch.Tensor]: + """Split a concatenated flat-codes tensor into per-request segments.""" + if seq_token_counts is not None and len(seq_token_counts) > 1: + boundaries = [0] + for count in seq_token_counts: + boundaries.append(boundaries[-1] + int(count)) + n = int(ids.numel()) + return [ids[boundaries[i] : min(boundaries[i + 1], n)] for i in range(len(seq_token_counts))] + return [ids] + + +# Engine-side architecture identifier alias (mirrors Qwen3TTSCode2Wav usage). +HiggsAudioV2Code2WavForConditionalGeneration = HiggsAudioV2Code2Wav diff --git a/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_talker.py b/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_talker.py new file mode 100644 index 000000000..5e3883fd2 --- /dev/null +++ b/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_talker.py @@ -0,0 +1,1684 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Stage-0 talker for higgs-audio v2 (vLLM-native, DualFFN-aware). + +Architecture summary (verified against `transformers.models.higgs_audio_v2` +source and the boson-ai checkpoint config.json): + +- Backbone: Llama-3.2-3B (hidden=3072, 28 layers, GQA 24Q/8KV, head_dim=128, + vocab=128256, max_position_embeddings=2048, RoPE llama3 scaling factor=32). + We reuse vLLM's compiled :class:`vllm.model_executor.models.llama.LlamaModel` + for the attention path so PagedAttention scheduling stays intact. + +- DualFFN: every transformer block carries a parallel audio expert + (``audio_input_layernorm`` + ``audio_post_attention_layernorm`` + ``audio_mlp``) + next to the standard text path (``input_layernorm`` + ``post_attention_layernorm`` + + ``mlp``). A per-position ``audio_token_mask`` of shape ``[B, S]`` selects + between the two. The mask is the union of positions where the input token id + equals ``audio_token_id=128016`` or ``audio_delay_token_id=128014``. + +- Multi-codebook output head: at audio positions, Stage 0 emits one ID per + codebook (8 codebooks of vocabulary 1026 each) in parallel via a single + fused ``audio_lm_head: Linear(hidden, num_codebooks * codebook_size)`` + whose output is reshaped to ``[N_audio, num_codebooks, codebook_size]``. + +- Delay pattern: codebook k is staggered by k frames using + ``audio_delay_token_id=128014`` as a filler at the LM-token positions where + codebook k has not yet started emitting real codes. This matches + ``HiggsAudioV2DelayPatternLogitsProcessor`` upstream and is the canonical + MusicGen pattern ``[0, 1, 2, 3, 4, 5, 6, 7]``. + +This file delivers the structural pieces (module classes, weight mapping, +forward pass, AR sampler dispatch, and the upstream delay-pattern logits +processor) to reproduce the upstream behavior under vLLM's compiled forward +path. +""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any + +import torch +import torch.nn as nn +from vllm.config import VllmConfig +from vllm.logger import init_logger +from vllm.model_executor.layers.layernorm import RMSNorm +from vllm.model_executor.layers.logits_processor import LogitsProcessor +from vllm.model_executor.layers.vocab_parallel_embedding import ( + ParallelLMHead, + VocabParallelEmbedding, +) +from vllm.model_executor.models.llama import LlamaDecoderLayer, LlamaMLP + +from vllm_omni.model_executor.models.higgs_audio_v2.configuration_higgs_audio_v2 import ( + HiggsAudioV2Config, +) +from vllm_omni.model_executor.models.output_templates import OmniOutput + +__all__ = [ + "HiggsAudioV2DecoderLayer", + "HiggsAudioV2TalkerForConditionalGeneration", +] + +logger = init_logger(__name__) + + +class HiggsAudioRMSNorm(nn.Module): + """Minimal RMSNorm matching the upstream HiggsAudioV2RMSNorm semantics. + + Kept self-contained so the parameter names (``weight``) line up with the + upstream state dict for direct load. + """ + + def __init__(self, hidden_size: int, eps: float = 1e-5): + super().__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.eps = eps + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + input_dtype = hidden_states.dtype + hidden_states = hidden_states.to(torch.float32) + variance = hidden_states.pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.eps) + return self.weight * hidden_states.to(input_dtype) + + +class HiggsAudioV2DecoderLayer(LlamaDecoderLayer): + """One transformer block: vLLM-native attention + DualFFN-routed MLPs. + + Wraps a stock :class:`vllm.model_executor.models.llama.LlamaDecoderLayer` + for the self-attention path (so PagedAttention KV caches keep working) and + overlays the upstream HiggsAudioV2 DualFFN routing for the layernorm + MLP + pairs. The implementation follows the upstream rule from + ``transformers.models.higgs_audio_v2.HiggsAudioV2DecoderLayer.forward``: + + 1. Pre-attention norm: per-position split into ``audio_input_layernorm`` + (audio mask True) vs ``input_layernorm`` (audio mask False). The mixed + output is fed to a single shared self-attention. + 2. Post-attention residual + dual MLP: text positions go through + ``mlp(post_attention_layernorm(.))`` and audio positions go through + ``audio_mlp(audio_post_attention_layernorm(.))``. Both deltas are added + to the residual. + + The classical (non-fused-residual) transformer pattern is used so the per- + position split is straightforward. vLLM's compiled forward path passes + ``audio_token_mask`` through as an extra layer kwarg via + :class:`HiggsAudioV2TalkerForConditionalGeneration.forward`. + """ + + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): + # Subclass LlamaDecoderLayer so self_attn registers at the + # CANONICAL ``layers.{i}.self_attn.attn`` prefix (not nested under + # ``.base``). This is the architecturally-correct registration path + # for vLLM's compilation_config.static_forward_context. + super().__init__(vllm_config=vllm_config, prefix=prefix) + config = vllm_config.model_config.hf_config + hidden_size = int(config.hidden_size) + intermediate_size = int(config.intermediate_size) + hidden_act = str(config.hidden_act) + mlp_bias = bool(getattr(config, "mlp_bias", False)) + rms_norm_eps = float(config.rms_norm_eps) + # Parallel audio-expert norms + MLP (mirrors upstream parameter names). + self.audio_input_layernorm = HiggsAudioRMSNorm(hidden_size, eps=rms_norm_eps) + self.audio_post_attention_layernorm = HiggsAudioRMSNorm(hidden_size, eps=rms_norm_eps) + self.audio_mlp = LlamaMLP( + hidden_size=hidden_size, + intermediate_size=intermediate_size, + hidden_act=hidden_act, + bias=mlp_bias, + prefix=f"{prefix}.audio_mlp", + ) + + # ------------------------------------------------------------------ helpers + def _routed_norm( + self, + hidden: torch.Tensor, + text_norm: nn.Module, + audio_norm: HiggsAudioRMSNorm, + audio_mask: torch.Tensor | None, + residual: torch.Tensor | None = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + """Per-position routed RMSNorm with residual handling. + + Matches the vLLM fused-residual pattern: takes (hidden, residual) and + returns (normed_hidden, post_add_residual). Routes per position: + audio positions through ``audio_norm`` (HiggsAudioRMSNorm, plain RMS), + text positions through ``text_norm`` (vLLM compiled RMSNorm). + + For consistency we explicitly compute the residual sum once, then call + the vLLM text_norm on text positions WITHOUT its residual argument + (else it'd add the residual again). + """ + # Fold residual into hidden once. + if residual is not None: + hidden = hidden + residual + new_residual = hidden + if audio_mask is None: + return audio_norm(hidden), new_residual + mask_flat = audio_mask.reshape(-1) + if hidden.ndim == 3: + hidden_flat = hidden.reshape(-1, hidden.shape[-1]) + else: + hidden_flat = hidden + out = hidden_flat.clone() + if (~mask_flat).any(): + text_out = text_norm(hidden_flat[~mask_flat]) + # vLLM compiled RMSNorm.forward(x) returns Tensor; some variants + # return (Tensor, residual). Normalize to Tensor. + if isinstance(text_out, tuple): + text_out = text_out[0] + out[~mask_flat] = text_out.to(out.dtype) + if mask_flat.any(): + audio_out = audio_norm(hidden_flat[mask_flat]) + out[mask_flat] = audio_out.to(out.dtype) + return out.reshape_as(hidden), new_residual + + def _routed_mlp( + self, + hidden: torch.Tensor, + audio_mask: torch.Tensor | None, + ) -> torch.Tensor: + """Apply ``audio_mlp`` to mask positions and ``mlp`` to non-mask positions.""" + if audio_mask is None: + return self.audio_mlp(hidden) + mask_flat = audio_mask.reshape(-1) + if hidden.ndim == 3: + hidden_flat = hidden.reshape(-1, hidden.shape[-1]) + else: + hidden_flat = hidden + out = torch.zeros_like(hidden_flat) + if (~mask_flat).any(): + out[~mask_flat] = self.mlp(hidden_flat[~mask_flat]).to(out.dtype) + if mask_flat.any(): + out[mask_flat] = self.audio_mlp(hidden_flat[mask_flat]).to(out.dtype) + return out.reshape_as(hidden) + + # ------------------------------------------------------------------ forward + def forward( + self, + positions: torch.Tensor, + hidden_states: torch.Tensor, + residual: torch.Tensor | None, + *, + audio_token_mask: torch.Tensor | None = None, + ) -> tuple[torch.Tensor, torch.Tensor]: + # Fused-residual pattern (compatible with vLLM's LlamaModel forward + # loop which calls ``hidden, residual = self.norm(hidden, residual)`` + # at the end). + attn_input, residual = self._routed_norm( + hidden_states, + self.input_layernorm, + self.audio_input_layernorm, + audio_token_mask, + residual, + ) + attn_out = self.self_attn(positions=positions, hidden_states=attn_input) + + mlp_input, residual = self._routed_norm( + attn_out, + self.post_attention_layernorm, + self.audio_post_attention_layernorm, + audio_token_mask, + residual, + ) + mlp_out = self._routed_mlp(mlp_input, audio_token_mask) + return mlp_out, residual + + +class HiggsAudioV2TalkerForConditionalGeneration(nn.Module): + """Stage-0 talker class registered under + ``HiggsAudioV2ForConditionalGeneration`` (canonical HF arch identifier) + AND ``HiggsAudioV2TalkerForConditionalGeneration`` (explicit alias). + + The class wires together: + 1. vLLM-native ``LlamaModel`` backbone (PagedAttention scheduling). + 2. A parallel set of DualFFN modules (one per decoder layer) for the + audio-expert path. + 3. Multi-codebook output via a single fused ``audio_lm_head`` of shape + ``[num_codebooks * codebook_size, hidden]`` (all codebooks emitted + in parallel per audio position). + 4. ``load_weights`` HF -> vLLM mapping (GQA reshape, llama3 RoPE, + text/audio MLP split, audio code heads, audio token embeddings). + + The forward path interleaves DualFFN routing with vLLM's compiled + attention path so the talker integrates with the rest of the wiring + (pipeline_registry, serving_speech, deploy yaml, stage_input_processor). + """ + + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): + super().__init__() + hf_config = vllm_config.model_config.hf_config + if isinstance(hf_config, HiggsAudioV2Config): + self.config: HiggsAudioV2Config = hf_config + else: + # When loaded via AutoConfig the class is the upstream + # transformers config; wrap it in our typed shell preserving + # every attribute via PretrainedConfig. + self.config = HiggsAudioV2Config(**hf_config.to_dict()) + + # ------------------------------------------------------------------ embedding + final norm + # directly instantiate the text-side embedding and final norm + # so we don't pull in a full LlamaModel.layers list that the engine's + # weight loader would require initialized. Each HiggsAudioV2DecoderLayer + # below already owns its own LlamaDecoderLayer for the attention path; + # duplicating those via a parallel LlamaModel made the loader fail. + self.embed_tokens = VocabParallelEmbedding( + num_embeddings=self.config.vocab_size, + embedding_dim=self.config.hidden_size, + org_num_embeddings=self.config.vocab_size, + quant_config=None, + prefix=f"{prefix}.embed_tokens", + ) + self.norm = RMSNorm(self.config.hidden_size, eps=self.config.rms_norm_eps) + + # ------------------------------------------------------------------ DualFFN-aware layer stack + # use vLLM's ``make_layers`` helper to register attention layers + # at canonical ``layers.{i}.self_attn.attn`` paths in + # ``compilation_config.static_forward_context``. This is what gets + # picked up by ``bind_kv_cache`` so prefill writes K/V to the cache + # (root cause for empty cache + zero attention output). + from vllm.model_executor.models.utils import make_layers + + _, _, self.layers = make_layers( + self.config.num_hidden_layers, + lambda prefix: HiggsAudioV2DecoderLayer(vllm_config=vllm_config, prefix=prefix), + prefix=f"{prefix}.layers", + ) + + # ------------------------------------------------------------------ audio embedding (shared) + # Audio frames are embedded by summing per-codebook lookups; the LM + # input_ids stream uses ``audio_token_id`` / ``audio_delay_token_id`` + # placeholders at the corresponding positions, and the talker + # substitutes the audio embedding via the audio_codebook_embeddings + # table declared further below. + self.register_buffer( + "audio_tokens_offsets", + torch.arange(self.config.num_codebooks) * self.config.codebook_size, + persistent=False, + ) + + # ------------------------------------------------------------------ heads + # Standard text LM head (Llama 128256-wide vocab). + self.lm_head = ParallelLMHead( + self.config.vocab_size, + self.config.hidden_size, + org_num_embeddings=self.config.vocab_size, + quant_config=None, + prefix=f"{prefix}.lm_head", + ) + + # Audio side: the upstream ``audio_decoder_proj.audio_lm_head`` is a + # single fused ``Linear(hidden, num_codebooks * (codebook_size + 2))``. + # In our v2 config ``codebook_size = 1026`` ALREADY includes the two + # stream specials, so the fused head is shape ``(8 * 1026, 3072) = + # (8208, 3072)``. At sample time we run it once and reshape + # ``[N, 8208] -> [N, 8, 1026]`` to read each codebook's logits. + self.audio_lm_head = nn.Linear( + self.config.hidden_size, + self.config.num_codebooks * self.config.codebook_size, + bias=False, + ) + # Multi-codebook input embedding used both for prompt-side audio frame + # substitution and for next-step audio-input-embedding feedback during + # decode. Mirrors upstream ``HiggsAudioModel.audio_codebook_embeddings``. + self.audio_codebook_embeddings = nn.Embedding( + self.config.num_codebooks * self.config.codebook_size, + self.config.hidden_size, + ) + + # vLLM's logits processor wires sampling metadata into the lm_head. + self.logits_processor = LogitsProcessor(self.config.vocab_size) + + # ----------------------------------------------------------------- masks + @torch.inference_mode() + def audio_token_mask(self, input_ids: torch.Tensor) -> torch.Tensor: + """Compute the per-position audio_token_mask used by DualFFN routing. + + Matches ``HiggsAudioV2Model.get_placeholder_mask``: + + mask = (input_ids == audio_token_id) | (input_ids == audio_delay_token_id) + """ + return (input_ids == self.config.audio_token_id) | (input_ids == self.config.audio_delay_token_id) + + # ------------------------------------------------------------ ref-audio + @torch.inference_mode() + def _maybe_apply_ref_audio_substitution( + self, + hidden_states: torch.Tensor, + input_ids: torch.Tensor, + info_dicts: Any, + ) -> torch.Tensor: + """Substitute reference-audio embeddings at prompt-side audio placeholders. + + Voice-clone path: the serving layer threads per-request + ``audio_input_ids`` (shape ``[T_frames, num_codebooks]``) + + ``audio_input_ids_mask`` (``[T_frames]``) into + ``model_intermediate_buffer`` (one dict per request in batch order). + For each request that has those tensors, we look up its span in the + flat ``input_ids`` via ``query_start_loc`` from the forward context, + embed the codes through ``audio_codebook_embeddings`` (with the + per-codebook ``audio_tokens_offsets`` shift, mirroring + ``HiggsAudioV2Embeddings.forward`` upstream), and overwrite the + text-side embedding at every audio_token / audio_delay placeholder + position in that span. + + Decode steps (span length 1) are skipped here — the audio-feedback + path in ``_maybe_apply_audio_feedback`` takes over from the moment + the talker emits its first codebook frame via ``sample()``. + """ + if not info_dicts: + return hidden_states + + # Pull the per-request (codes, mask) tensors in batch order. Values + # may be on CPU after IPC serialization (msgspec ships tensors as raw + # bytes); the loop below moves them to the input_ids device on use. + per_req_codes: list[torch.Tensor | None] = [] + any_codes = False + for info in info_dicts: + if not isinstance(info, dict): + per_req_codes.append(None) + continue + ids = info.get("audio_input_ids") + mask = info.get("audio_input_ids_mask") + if isinstance(ids, list): + ids = ids[0] if ids else None + if isinstance(mask, list): + mask = mask[0] if mask else None + if not isinstance(ids, torch.Tensor) or not isinstance(mask, torch.Tensor): + per_req_codes.append(None) + continue + if ids.ndim == 3: + ids = ids[0] + if mask.ndim == 2: + mask = mask[0] + valid = ids[mask.to(dtype=torch.bool)] # [T_valid, num_codebooks] + if valid.numel() == 0: + per_req_codes.append(None) + continue + per_req_codes.append(valid) + any_codes = True + + if not any_codes: + return hidden_states + + # Per-request slices in the flat input_ids are recovered from the + # forward context's attention metadata (mimo_audio_llm.py:1022-1025 + # uses the same pattern). Without spans we cannot route codes to the + # right placeholder positions, so we no-op instead of risking a + # cross-request mix-up. + from vllm.forward_context import get_forward_context + + try: + attn_metadata = get_forward_context().attn_metadata + except Exception: + return hidden_states + if isinstance(attn_metadata, dict): + if not attn_metadata: + return hidden_states + attn = next(iter(attn_metadata.values())) + else: + attn = attn_metadata + q_start = getattr(attn, "query_start_loc", None) + if not isinstance(q_start, torch.Tensor): + return hidden_states + q_start_cpu = q_start.detach().to("cpu").tolist() + if len(q_start_cpu) != len(info_dicts) + 1: + return hidden_states + + audio_token_id = int(self.config.audio_token_id) + audio_delay_id = int(self.config.audio_delay_token_id) + offsets = self.audio_tokens_offsets.to(input_ids.device) + + new_hidden: torch.Tensor | None = None + for i, codes in enumerate(per_req_codes): + if codes is None: + continue + s = int(q_start_cpu[i]) + e = int(q_start_cpu[i + 1]) + if e - s <= 1: + # Decode step — let _maybe_apply_audio_feedback handle this row. + continue + slice_ids = input_ids[s:e] + mask = (slice_ids == audio_token_id) | (slice_ids == audio_delay_id) + placeholders = mask.nonzero(as_tuple=True)[0] + n_real = int(codes.shape[0]) + if int(placeholders.numel()) < n_real: + # Mismatch (likely a truncated prompt): skip this request rather + # than corrupt the batch. The user-visible failure surfaces as + # the model not honoring the ref-audio rather than a crash. + continue + target = placeholders[:n_real] + s + embeds = self.audio_codebook_embeddings(codes.to(device=input_ids.device, dtype=torch.long) + offsets).sum( + dim=-2 + ) + if new_hidden is None: + new_hidden = hidden_states.clone() + new_hidden[target] = embeds.to(new_hidden.dtype) + + return new_hidden if new_hidden is not None else hidden_states + + # ----------------------------------------------------------------- weights + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: + """Map HF state dict names to the vLLM-native layout. + + The upstream tensor names live under ``model.layers....`` plus a few + top-level entries. The vLLM-native layout uses fused QKV/MLP tensors, + so we transform the relevant pairs at load time: + + * Attention: HF ``q_proj/k_proj/v_proj`` -> vLLM ``qkv_proj`` (packed + ``[hidden + 2 * kv_dim, hidden]``; for GQA with 24 Q heads, 8 KV + heads, head_dim=128 the slabs are + ``q_proj[3072, 3072]``, ``k_proj[1024, 3072]``, ``v_proj[1024, 3072]`` + -> ``qkv_proj[5120, 3072]``). + * RoPE parameters are read from ``self.config.rope_parameters`` and + consumed by vLLM's LlamaAttention without any weight transform + (``rope_type="llama3"``, factor=32, low_freq_factor=0.125, + high_freq_factor=0.5, original_max_position_embeddings=1024). + * Text MLP: HF ``mlp.gate_proj`` + ``mlp.up_proj`` -> + vLLM ``mlp.gate_up_proj`` (concatenated on dim 0). + * Audio MLP: HF ``audio_mlp.gate_proj`` + ``audio_mlp.up_proj`` -> + vLLM ``dual_ffns..audio_mlp.gate_up_proj`` (same shape transform). + * Layer norms: HF ``input_layernorm`` / ``post_attention_layernorm`` + -> ``dual_ffns..{input,post_attention}_layernorm``. Audio norms + -> ``dual_ffns..{audio_input,audio_post_attention}_layernorm``. + * Audio token embedding: HF ``model.embed_audio_tokens.weight`` (with + optional outer ``embed_audio_tokens.`` prefix from upstream's + HiggsAudioV2Embeddings) -> ``self.audio_codebook_embeddings.weight`` + (see ``_map_simple_name``). + * Audio codebook heads: HF ``audio_lm_head.weight`` -> the fused 8x1026 + head from which Stage 0 reads codebook 0 directly and codebook 1..7 + via the fast-AR code predictor's residual heads. (Boson-ai's actual + checkpoint exposes a single fused ``audio_lm_head`` of shape + ``[num_codebooks * codebook_size, hidden]``, not one head per + codebook; we split it into the per-codebook heads at load time.) + + this routine covers the simple, fused-QKV, and fused-MLP + transcriptions. The remaining gap (full Stage-0 forward integration with + vLLM's PagedAttention scheduler so the loaded weights drive a working + greedy decode) is tracked as an Open Issue in the goal tracker. + """ + loaded: set[str] = set() + own_params = dict(self.named_parameters()) + # Debug: log the first few HF keys we receive so we can verify the + # mapper hits them. Also log our own param-name shape so missing + # mappings are obvious. + logger.info( + "higgs_audio_v2 load_weights: %d own params (sample: %s)", + len(own_params), + sorted(list(own_params.keys()))[:5], + ) + debug_seen: list[str] = [] + + # First pass: collect tensors that need fusing (q/k/v, gate/up for each + # MLP slot). We accumulate into per-(layer, slot, kind) buckets and + # commit the fused tensor once all parts have arrived. + fuse_buckets: dict[tuple[int, str, str], dict[str, torch.Tensor]] = {} + + def _strip_model_prefix(name: str) -> str: + """vLLM's DefaultModelLoader strips the leading ``model.`` from HF + state-dict keys before invoking ``load_weights``, but we also want + to accept raw HF names (for unit tests that pass the state dict + directly). Normalize by stripping at most one ``model.`` prefix. + """ + if name.startswith("model."): + return name[len("model.") :] + return name + + def _try_simple(name: str, tensor: torch.Tensor) -> bool: + normalized = _strip_model_prefix(name) + # Direct match: many keys (embed_tokens.weight, norm.weight, + # layers..{audio_,}input_layernorm.weight, layers..{audio_,}post_attention_layernorm.weight, + # layers..self_attn.o_proj.weight, layers..{,audio_}mlp.down_proj.weight) already match + # our named_parameters() keys after the model-prefix strip. + if normalized in own_params: + target = own_params[normalized] + if tuple(target.shape) == tuple(tensor.shape): + with torch.no_grad(): + target.copy_(tensor) + loaded.add(normalized) + return True + logger.warning( + "higgs_audio_v2 load_weights: shape mismatch %s -> %s: %s vs %s", + name, + normalized, + tuple(tensor.shape), + tuple(target.shape), + ) + return False + + # After making HiggsAudioV2DecoderLayer inherit from + # LlamaDecoderLayer, text-side params live directly at + # ``layers..`` (no ``.base`` indirection). audio side + # remains at ``layers..audio_<...>``. + parts = normalized.split(".") + if len(parts) >= 3 and parts[0] == "layers": + tail = ".".join(parts[2:]) + # Audio_mlp.down_proj lives at our top-level audio_mlp slot. + if tail == "audio_mlp.down_proj.weight": + audio_name = f"layers.{parts[1]}.audio_mlp.down_proj.weight" + if audio_name in own_params: + target = own_params[audio_name] + if tuple(target.shape) == tuple(tensor.shape): + with torch.no_grad(): + target.copy_(tensor) + loaded.add(audio_name) + return True + + # Special simple cases (nested audio embedding, text_lm_head alias, etc.) + mapped = self._map_simple_name(name) + if mapped is None: + # Also try after stripping the model. prefix so the _map_simple_name + # branches written for un-stripped names continue to work. + mapped = self._map_simple_name("model." + normalized) + if mapped is None or mapped not in own_params: + return False + target = own_params[mapped] + if tuple(target.shape) != tuple(tensor.shape): + logger.warning( + "higgs_audio_v2 load_weights: shape mismatch %s -> %s: %s vs %s", + name, + mapped, + tuple(tensor.shape), + tuple(target.shape), + ) + return False + with torch.no_grad(): + target.copy_(tensor) + loaded.add(mapped) + return True + + def _stash_fusion(name: str, tensor: torch.Tensor) -> bool: + normalized = _strip_model_prefix(name) + parts = normalized.split(".") + if len(parts) < 4 or parts[0] != "layers": + return False + try: + layer_idx = int(parts[1]) + except ValueError: + return False + tail = ".".join(parts[2:]) + for slot in ("mlp", "audio_mlp"): + for kind in ("gate_proj", "up_proj"): + if tail == f"{slot}.{kind}.weight": + fuse_buckets.setdefault((layer_idx, slot, "gate_up_proj"), {})[kind] = tensor + return True + for kind in ("q_proj", "k_proj", "v_proj"): + if tail == f"self_attn.{kind}.weight": + fuse_buckets.setdefault((layer_idx, "self_attn", "qkv_proj"), {})[kind] = tensor + return True + return False + + unhandled: list[str] = [] + for name, tensor in weights: + if len(debug_seen) < 30: + debug_seen.append(name) + if _try_simple(name, tensor): + continue + if _stash_fusion(name, tensor): + continue + # Audio code head: upstream has a fused [num_codebooks*codebook_size, hidden] + # head; split into codebook-0 head + residual heads. Accept both + # ``audio_lm_head.weight`` and ``model.audio_lm_head.weight``. + if name in ( + "audio_lm_head.weight", + "model.audio_lm_head.weight", + "audio_decoder_proj.audio_lm_head.weight", + "model.audio_decoder_proj.audio_lm_head.weight", + ): + self._consume_fused_audio_head(tensor, loaded) + continue + unhandled.append(name) + + # Second pass: emit fused tensors. Targets are the per-layer slots on + # the HiggsAudioV2DecoderLayer stack: text-side params at + # ``layers..`` directly (HiggsAudioV2DecoderLayer inherits + # from LlamaDecoderLayer); ``layers..audio_mlp...`` for the audio + # expert. + for (layer_idx, slot, fused_kind), parts in fuse_buckets.items(): + if fused_kind == "gate_up_proj": + gate = parts.get("gate_proj") + up = parts.get("up_proj") + if gate is None or up is None: + continue + fused = torch.cat([gate, up], dim=0) + if slot == "mlp": + target_name = f"layers.{layer_idx}.mlp.gate_up_proj.weight" + else: # audio_mlp + target_name = f"layers.{layer_idx}.audio_mlp.gate_up_proj.weight" + elif fused_kind == "qkv_proj": + q = parts.get("q_proj") + k = parts.get("k_proj") + v = parts.get("v_proj") + if q is None or k is None or v is None: + continue + fused = torch.cat([q, k, v], dim=0) + target_name = f"layers.{layer_idx}.self_attn.qkv_proj.weight" + else: + continue + if target_name in own_params and tuple(own_params[target_name].shape) == tuple(fused.shape): + with torch.no_grad(): + own_params[target_name].copy_(fused) + loaded.add(target_name) + else: + logger.warning( + "higgs_audio_v2 load_weights: fused %s not found or shape mismatch (%s vs target %s)", + target_name, + tuple(fused.shape), + tuple(own_params[target_name].shape) if target_name in own_params else "", + ) + logger.info( + "higgs_audio_v2 load_weights: %d/%d params initialized " + "(sample seen: %s; unhandled count=%d, sample unhandled=%s)", + len(loaded), + len(own_params), + debug_seen[:10], + len(unhandled), + unhandled[:20], + ) + return loaded + + def _consume_fused_audio_head(self, fused: torch.Tensor, loaded: set[str]) -> None: + """Load the fused ``audio_lm_head[num_codebooks*codebook_size, hidden]`` + directly into ``self.audio_lm_head.weight``. + + Upstream uses a single big head; there is no separate codebook-0 + head or residual predictor in the actual checkpoint. + """ + own_params = dict(self.named_parameters()) + num_codebooks = int(self.config.num_codebooks) + codebook_size = int(self.config.codebook_size) + hidden = int(self.config.hidden_size) + target = own_params.get("audio_lm_head.weight") + if target is None: + logger.warning("higgs_audio_v2: no audio_lm_head.weight slot to load into") + return + expected = (num_codebooks * codebook_size, hidden) + if tuple(fused.shape) != expected: + logger.warning( + "higgs_audio_v2: unexpected audio_lm_head shape %s; expected %s", + tuple(fused.shape), + expected, + ) + return + with torch.no_grad(): + target.copy_(fused) + loaded.add("audio_lm_head.weight") + + # --------------------------------------------------------------- helpers + def _map_simple_name(self, hf_name: str) -> str | None: + """Translate the simple (non-fused) HF parameter names to vLLM names.""" + # Upstream nests the audio embedding as + # ``model.embed_audio_tokens.embed_audio_tokens.weight``; the boson-ai + # checkpoint also uses this exact key. Accept both nested and flat. + # the upstream multi-codebook embedding is ``audio_codebook_embeddings`` + # (a flat ``Embedding(8 * codebook_size, hidden)``). also had a separate + # ``embed_audio_tokens`` (used in prefill-time DualFFN routing helpers); + # we keep that slot so the existing prompt-side embedding logic stays + # functional. Both slots are loaded from the same upstream tensor. + if hf_name in ( + "embed_audio_tokens.weight", + "embed_audio_tokens.embed_audio_tokens.weight", # stripped form + "model.embed_audio_tokens.embed_audio_tokens.weight", # raw HF form + "model.embed_audio_tokens.weight", + # Actual key used in the boson-ai checkpoint: + "audio_codebook_embeddings.weight", + "model.audio_codebook_embeddings.weight", + ): + return "audio_codebook_embeddings.weight" + if hf_name in ( + "lm_head.weight", + "text_lm_head.weight", + "model.text_lm_head.weight", + # Actual key used in the boson-ai checkpoint: + "audio_decoder_proj.text_lm_head.weight", + "model.audio_decoder_proj.text_lm_head.weight", + ): + return "lm_head.weight" + # Dropped LlamaModel wrapper; text embedding + final norm now + # live directly on the talker (self.embed_tokens / self.norm). + if hf_name == "model.embed_tokens.weight": + return "embed_tokens.weight" + if hf_name == "model.norm.weight": + return "norm.weight" + # Per-layer audio norms -> our HiggsAudioV2DecoderLayer side. + if hf_name.startswith("model.layers.") and hf_name.endswith(".audio_input_layernorm.weight"): + layer_idx = hf_name.split(".")[2] + return f"layers.{layer_idx}.audio_input_layernorm.weight" + if hf_name.startswith("model.layers.") and hf_name.endswith(".audio_post_attention_layernorm.weight"): + layer_idx = hf_name.split(".")[2] + return f"layers.{layer_idx}.audio_post_attention_layernorm.weight" + # Per-layer text-side params now live directly at + # ``layers.{i}.`` (no ``.base`` indirection) since + # HiggsAudioV2DecoderLayer inherits from LlamaDecoderLayer. + if hf_name.startswith("model.layers.") and hf_name.endswith(".input_layernorm.weight"): + layer_idx = hf_name.split(".")[2] + return f"layers.{layer_idx}.input_layernorm.weight" + if hf_name.startswith("model.layers.") and hf_name.endswith(".post_attention_layernorm.weight"): + layer_idx = hf_name.split(".")[2] + return f"layers.{layer_idx}.post_attention_layernorm.weight" + if hf_name.startswith("model.layers.") and hf_name.endswith(".self_attn.o_proj.weight"): + layer_idx = hf_name.split(".")[2] + return f"layers.{layer_idx}.self_attn.o_proj.weight" + if hf_name.startswith("model.layers.") and hf_name.endswith(".mlp.down_proj.weight"): + layer_idx = hf_name.split(".")[2] + return f"layers.{layer_idx}.mlp.down_proj.weight" + if hf_name.startswith("model.layers.") and hf_name.endswith(".audio_mlp.down_proj.weight"): + layer_idx = hf_name.split(".")[2] + return f"layers.{layer_idx}.audio_mlp.down_proj.weight" + return None + + # ----------------------------------------------------------------- forward + def forward( + self, + input_ids: torch.Tensor, + positions: torch.Tensor, + intermediate_tensors: Any | None = None, + inputs_embeds: torch.Tensor | None = None, + **kwargs: Any, + ) -> torch.Tensor: + """DualFFN-routed Stage-0 forward. + + Drives the custom :class:`HiggsAudioV2DecoderLayer` stack with a + per-position ``audio_token_mask`` derived from the input ``input_ids`` + (matching :func:`HiggsAudioV2Model.get_placeholder_mask`). Each layer + applies the upstream DualFFN routing internally; this method only + composes the embedding and final-norm steps around the layer loop. + """ + if inputs_embeds is None: + hidden_states = self.embed_tokens(input_ids) + else: + hidden_states = inputs_embeds + + # Stash the step's input_ids for :meth:`_apply_audio_mode_bias` to read + # in :meth:`sample`. ``sampling_metadata.prompt_token_ids`` is only + # populated when penalties or token-id-aware logits processors are + # active, so for our use case we have to keep our own copy. + if input_ids is not None: + self._last_step_input_ids = input_ids + + # Stash query_start_loc here (inside the forward context) so + # _apply_audio_mode_bias (which runs LATER in sample(), after the + # forward context exits) can recover each request's last token + # position from the flat input_ids — see the per-row prev-token + # lookup below. Without this, mixed prefill+decode batches would + # fall back to ``input_ids[-num_rows:]``, which silently misroutes + # the audio_out_bos detection in batch>1 voice clone (was the root + # cause of the slot-shifted gibberish at batch=4). + try: + from vllm.forward_context import get_forward_context + + attn_metadata = get_forward_context().attn_metadata + if isinstance(attn_metadata, dict) and attn_metadata: + attn = next(iter(attn_metadata.values())) + else: + attn = attn_metadata + qsl = getattr(attn, "query_start_loc", None) + if isinstance(qsl, torch.Tensor): + self._last_step_query_start_loc = qsl.detach().clone() + else: + self._last_step_query_start_loc = None + except Exception: + self._last_step_query_start_loc = None + + # Voice-clone ref-audio substitution: at the prompt-side audio + # placeholder span we overwrite the text-embed lookups with the + # multi-codebook embedding of the encoded reference clip carried in + # ``model_intermediate_buffer``. No-op for plain-text requests and + # for decode steps (which fall through to the audio-feedback path). + if input_ids is not None and inputs_embeds is None: + info_dicts = kwargs.get("model_intermediate_buffer") + if info_dicts is None: + info_dicts = kwargs.get("runtime_additional_information") + hidden_states = self._maybe_apply_ref_audio_substitution( + hidden_states, + input_ids, + info_dicts, + ) + + # Audio-feedback substitution: vLLM's AR runner calls forward() + # with ``inputs_embeds=None`` and lets the model embed its own + # input_ids inline. ``embed_input_ids`` is only invoked via the + # supports_mm_inputs/prompt_embeds branches, neither of which fires + # for the talker. So we apply the substitution directly here: at + # decode positions where input_ids == audio_token_id, swap the text + # embedding with embed_audio_codes(last audio frame stashed in + # ``self._audio_state[req_id]`` by :meth:`sample`). + if input_ids is not None and inputs_embeds is None: + hidden_states = self._maybe_apply_audio_feedback(hidden_states, input_ids) + + audio_mask = self.audio_token_mask(input_ids) if input_ids is not None else None + + # Fused-residual loop: layers return (hidden, residual) + # where ``residual`` carries everything up to the post-MLP add. + residual: torch.Tensor | None = None + for layer in self.layers: + hidden_states, residual = layer( + positions, + hidden_states, + residual, + audio_token_mask=audio_mask, + ) + + # Final fused norm: combine final mlp_out with carried residual. + norm_out = self.norm(hidden_states, residual) + if isinstance(norm_out, tuple): + norm_out = norm_out[0] + return norm_out + + def compute_logits( + self, + hidden_states: torch.Tensor, + sampling_metadata: Any = None, + ) -> torch.Tensor: + """Sampler-compatible logits tensor. + + Returns a single ``[N, vocab_size]`` tensor that the stock + vLLM AR runner pipes through ``.contiguous()`` and a generic + sampler. The contract is intentionally tensor-only: + + - Text-position logits come from ``lm_head`` (128256-wide Llama vocab). + - Audio-position logits at audio_token_id placeholders are also + read from ``lm_head``; the per-position codebook routing path + lives in :meth:`audio_codebook_logits` and is consumed by a + separate codebook sampling adapter. + """ + # Stash for the audio-sampler dispatch in :meth:`sample`. Limited to a + # single step at a time; cleared in :meth:`sample` after consumption + # so a missed call doesn't leak stale tensors. + self._last_logits_hidden = hidden_states + return self.logits_processor(self.lm_head, hidden_states, sampling_metadata) + + def audio_codebook_logits( + self, + hidden_states: torch.Tensor, + audio_token_mask: torch.Tensor, + ) -> torch.Tensor: + """Per-codebook logits at audio positions, ``[N_audio, num_codebooks, codebook_size]``. + + Runs the single fused ``audio_lm_head: Linear(hidden, num_codebooks * + codebook_size)`` and reshapes the output. This mirrors upstream's + ``HiggsAudioDecoderProjector.audio_lm_head`` + the + ``view(-1, num_codebooks, codebook_size)`` reshape in + ``HiggsAudioModel.forward``. + + Returns an empty tensor with the correct trailing shape when the mask + selects no positions. + """ + num_codebooks = int(self.config.num_codebooks) + codebook_size = int(self.config.codebook_size) + mask = audio_token_mask.reshape(-1).to(hidden_states.device) + hidden_flat = hidden_states.reshape(-1, hidden_states.shape[-1]) + if not mask.any(): + return torch.empty( + (0, num_codebooks, codebook_size), + dtype=hidden_states.dtype, + device=hidden_states.device, + ) + flat = self.audio_lm_head(hidden_flat[mask]) + return flat.view(-1, num_codebooks, codebook_size) + + def embed_audio_codes(self, audio_ids: torch.Tensor) -> torch.Tensor: + """Embed a ``[num_codebooks, T]`` audio-ids slice into a ``[T, hidden]`` tensor. + + Mirrors upstream ``HiggsAudioModel._embed_audio_ids``: + + shift = arange(num_codebooks) * codebook_size # 1026 per slot + audio_embed = sum_over_codebooks(audio_codebook_embeddings(audio_ids + shift)) + + The 8 codebooks share a single ``Embedding(8 * codebook_size, hidden)`` + table; each codebook indexes into its own per-codebook slot via the + shift. The output is the per-frame mixture of 8 codebook embeddings + and is what the model re-feeds at the next AR step's + ``<|AUDIO_OUT|>`` placeholder position. + """ + if audio_ids.ndim != 2: + raise ValueError(f"audio_ids must be [num_codebooks, T]; got shape {tuple(audio_ids.shape)}") + codebook_size = int(self.config.codebook_size) + num_codebooks = int(self.config.num_codebooks) + shift = torch.arange(num_codebooks, device=audio_ids.device) * codebook_size + shifted = audio_ids + shift.unsqueeze(-1) # [num_codebooks, T] + embed = self.audio_codebook_embeddings(shifted) # [num_codebooks, T, hidden] + return embed.sum(dim=0) # [T, hidden] + + def _maybe_apply_audio_feedback(self, hidden_states: torch.Tensor, input_ids: torch.Tensor) -> torch.Tensor: + """Return ``hidden_states`` with audio-token positions replaced by the + multi-codebook embedding of the LAST audio frame stashed by + :meth:`sample`. + + Called from :meth:`forward` when ``inputs_embeds`` is ``None`` (vLLM's + AR runner path) — the equivalent of upstream's + ``_embed_audio_ids(audio_out_ids)`` injection at audio positions. + We use ``index_copy`` (out-of-place) instead of mutating the original + ``hidden_states`` because the embed-tokens output may be a view of a + compiled buffer that doesn't tolerate in-place writes. + """ + audio_token_id = int(self.config.audio_token_id) + flat_ids = input_ids.reshape(-1) + audio_positions = (flat_ids == audio_token_id).nonzero(as_tuple=False).reshape(-1) + if audio_positions.numel() == 0: + return hidden_states + state = getattr(self, "_audio_state", None) + if not state: + return hidden_states + # Build a list of (pos, audio_emb) overrides and apply via index_copy + # on a cloned hidden_states. + rep_positions: list[int] = [] + rep_embeds: list[torch.Tensor] = [] + for pos in audio_positions.tolist(): + req_state = state.get(int(pos)) + if req_state is None: + continue + audio_out_ids = req_state.get("audio_out_ids") + if not isinstance(audio_out_ids, torch.Tensor) or audio_out_ids.numel() == 0: + continue + last_frame = audio_out_ids[:, -1:].to(hidden_states.device) + audio_emb = self.embed_audio_codes(last_frame) # [1, hidden] + rep_positions.append(int(pos)) + rep_embeds.append(audio_emb[0].to(dtype=hidden_states.dtype)) + if not rep_positions: + return hidden_states + new_hidden = hidden_states.clone() + flat_hidden = new_hidden.reshape(-1, new_hidden.shape[-1]) + idx = torch.tensor(rep_positions, dtype=torch.long, device=new_hidden.device) + rep = torch.stack(rep_embeds, dim=0) + flat_hidden.index_copy_(0, idx, rep) + return new_hidden + + def embed_input_ids(self, input_ids: torch.Tensor, **_: Any) -> torch.Tensor: + """Embed the LM token stream, swapping in audio embeddings at placeholders. + + Text positions use the standard ``embed_tokens`` lookup. At audio + positions (input id equals ``audio_token_id``) we substitute the + text embedding with the multi-codebook embedding of the LAST audio + frame stashed by :meth:`sample` in ``self._audio_state[req_id]``. + This mirrors upstream's ``HiggsAudioModel.forward`` flow where + ``_embed_audio_ids(audio_out_ids)`` is plugged into the input at + ``<|AUDIO_OUT|>`` placeholder positions. + + Without this feedback the model receives the SAME embedding (just + the text ``<|AUDIO_OUT|>`` embedding) at every audio step and has no + way to attend to its own audio history — the resulting codes + collapse to a fixed loop and the ASR cannot recover the original + prompt. + """ + text_embed = self.embed_tokens(input_ids) + audio_token_id = int(self.config.audio_token_id) + flat_ids = input_ids.reshape(-1) + audio_positions = (flat_ids == audio_token_id).nonzero(as_tuple=False).reshape(-1) + if audio_positions.numel() == 0: + return text_embed + state = getattr(self, "_audio_state", None) + if not state: + return text_embed + # We expect one audio position per active request per decode step. + # Match rows by walking through the per-request audio_out_ids buffers. + # The runner concatenates requests in batch order; ``audio_positions`` + # lists their indices into the flat ``input_ids``. For each position we + # look up the corresponding request's last audio frame. + # NOTE: this O(N_audio) loop is small (N_audio == active requests in a + # decode step) and runs once per step. + flat_embed = text_embed.reshape(-1, text_embed.shape[-1]) + for pos in audio_positions.tolist(): + # ``state`` keys are the absolute batch row indices the sampler + # saw, which match the positions in flat ``input_ids`` during a + # decode step (one token per active request). For a prefill + # step's prompt-side audio_token_id positions, no state entry + # exists yet — that's fine, we leave the text embed in place. + req_state = state.get(int(pos)) + if req_state is None: + continue + audio_out_ids = req_state.get("audio_out_ids") + if not isinstance(audio_out_ids, torch.Tensor) or audio_out_ids.numel() == 0: + continue + last_frame = audio_out_ids[:, -1:].to(text_embed.device) # [Q, 1] + audio_emb = self.embed_audio_codes(last_frame) # [1, hidden] + flat_embed[pos] = audio_emb[0].to(dtype=text_embed.dtype) + return text_embed + + def make_omni_output( + self, + model_outputs: torch.Tensor | OmniOutput, + **kwargs: Any, + ) -> OmniOutput: + """Wrap raw decoder outputs into the :class:`OmniOutput` contract. + + Mirrors the canonical Qwen3-TTS / Fish-Speech recovery pattern: + the runner threads per-request ``codes.audio`` into + ``model_intermediate_buffer`` (a list of dicts in batch order); we + concatenate those and trim ``text_hidden_states`` to the emitted + audio span. Falls back to the deprecated ``runtime_additional_information`` + kwarg for older runners, and finally to ``audio_codes`` / + ``model_kwargs[audio_codes]`` for direct callers. + """ + if isinstance(model_outputs, OmniOutput): + return model_outputs + hidden = model_outputs + + # Primary contract: model_intermediate_buffer (Qwen3-TTS / Fish-Speech). + info_dicts = kwargs.get("model_intermediate_buffer") + if info_dicts is None: + info_dicts = kwargs.get("runtime_additional_information") + if info_dicts is None: + info_dicts = [] + + # Build a per-request audio-codes list whose length exactly equals + # ``len(info_dicts)`` (== batch size). Requests with no emitted codes + # this step get an empty tensor placeholder — that way the runner's + # ``to_payload_element`` (which indexes ``element[idx]`` per request) + # can never silently fall back to ``element[0]`` for higher slots, + # which was the root of the batch>1 voice-clone slot-shift bug + # (slot k ended up speaking slot k-1's text). + audio_codes_list: list[torch.Tensor] = [] + any_nonempty = False + for info in info_dicts: + ac: torch.Tensor | None = None + if isinstance(info, dict): + codes_field = info.get("codes") + if isinstance(codes_field, dict): + ac = codes_field.get("audio") + else: + ac = info.get("audio_codes") + if isinstance(ac, torch.Tensor) and ac.numel() > 0: + audio_codes_list.append(ac) + any_nonempty = True + else: + audio_codes_list.append(torch.empty(0, dtype=torch.long)) + + if any_nonempty: + return OmniOutput( + text_hidden_states=hidden, + multimodal_outputs={"codes": {"audio": audio_codes_list}}, + ) + + # Fallbacks: explicit kwarg or wrapped model_kwargs dicts (preserved for + # direct-API callers that don't go through the runner buffer). + audio_codes = kwargs.get("audio_codes") + if audio_codes is None: + for source_name in ("model_kwargs", "model_kwargs_extra"): + source = kwargs.get(source_name) + if isinstance(source, dict) and "audio_codes" in source: + audio_codes = source["audio_codes"] + break + if audio_codes is None: + audio_codes = torch.empty(0, dtype=torch.long) + return OmniOutput( + text_hidden_states=hidden, + multimodal_outputs={"codes": {"audio": audio_codes}}, + ) + + # ----------------------------------------------------- model-owned sampler + # Opt into the AR runner's model-sampler hook (see + # vllm_omni/worker/gpu_ar_model_runner.py:_sample). The runner calls + # ``self.model.sample(logits, sampling_metadata)`` when this flag is True; + # we delegate to the stock vLLM sampler for now and document the + # per-position audio-codebook-0 dispatch as the immediate follow-up. The + # hook itself is what unblocks an external integrator from plugging in the + # final audio-side sampler without touching the runner. + prefer_model_sampler: bool = True + + # Tell the runner's :func:`extract_multimodal_outputs` to forward the OmniOutput + # produced by :meth:`make_omni_output`. Without this flag the multimodal + # payload (audio codes) is dropped and the downstream stage receives only + # text hidden states. + have_multimodal_outputs: bool = True + + def sample(self, logits: torch.Tensor, sampling_metadata: Any) -> Any: + """Model-owned sampler with delay-pattern audio dispatch. + + Each generation step the model emits one LM token. At audio positions + (where the previous LM token was ``audio_bos_token_id`` or + ``audio_token_id``), the same hidden state also drives the fused + ``audio_lm_head`` to produce ``[num_codebooks, codebook_size]`` + per-codebook logits. We argmax those per-codebook, then APPLY THE + UPSTREAM DELAY PATTERN: + + - For the first ``num_codebooks - 1`` audio steps, force codebooks + ``[num_delay + 1 :]`` to ``audio_stream_bos_id``; only codebooks + up to ``num_delay`` are real. + - When ``audio_stream_eos_id`` first appears in a codebook k, force + all earlier codebooks to ``eos`` too and start the trailing + ``num_codebooks - k - 1`` "ramp-down" steps where successive + codebooks also get ``eos``. + - When all 8 codebooks have ramped down, switch the LM next-token + from ``audio_token_id`` to ``audio_eos_token_id`` so the engine + sees a stop signal at the right place. + + Per-request state (``num_delay``, ``num_remaining_delays``, + cumulative ``audio_out_ids: [num_codebooks, T_so_far]``) lives in + ``self._audio_state[req_id]``; ``embed_input_ids`` reads it on the + next step to substitute the audio-token embedding via + :meth:`embed_audio_codes`. + """ + sampler = getattr(self, "_stock_sampler", None) + if sampler is None: + from vllm.v1.sample.sampler import Sampler + + sampler = Sampler() + self._stock_sampler = sampler + + # Bias the text-vocab logits so the LM stays on the audio path. This + # forces ``audio_token_id`` at audio positions; the delay-pattern + # logic below later overrides the LM token to ``audio_eos_token_id`` + # when the ramp-down completes. + self._apply_audio_mode_bias(logits, sampling_metadata) + sampler_output = sampler(logits=logits, sampling_metadata=sampling_metadata) + + hidden = getattr(self, "_last_logits_hidden", None) + self._last_logits_hidden = None + if hidden is None: + self._last_audio_codes = None + return sampler_output + + sampled = getattr(sampler_output, "sampled_token_ids", None) + if sampled is None: + self._last_audio_codes = None + return sampler_output + sampled_flat = sampled.reshape(-1) + if int(sampled_flat.numel()) != int(hidden.shape[0]): + self._last_audio_codes = None + return sampler_output + + audio_token_id = int(self.config.audio_token_id) + audio_delay_id = int(self.config.audio_delay_token_id) + is_audio = (sampled_flat == audio_token_id) | (sampled_flat == audio_delay_id) + + if not hasattr(self, "_audio_state"): + self._audio_state: dict[int, dict[str, Any]] = {} + if not hasattr(self, "_slot_output_len"): + self._slot_output_len: dict[int, int] = {} + + # Skip the audio-code sampling for rows that just transitioned + # OUT of audio_out_bos. The hidden state at this prefill-end step is + # at the audio_out_bos position, where upstream's audio_out_mask is + # False and no audio_logits are computed. The LM bias above already + # forced ``audio_token_id`` at this step, and the next forward will + # produce a hidden at the new audio_token_id position — that is what + # upstream samples its first audio frame from. Also reset any stale + # ``_audio_state`` and ``_slot_output_len`` for the slot, so a fresh + # request reusing a finished slot starts clean. + first_after_bos = getattr(self, "_last_first_audio_after_bos", None) + self._last_first_audio_after_bos = None + num_codebooks_local = int(self.config.num_codebooks) + bos_local = int(self.config.audio_stream_bos_id) + seeded_rows: list[int] = [] + seeded_frame: torch.Tensor | None = None + # HF seeds frame 0 with all-BOS at the audio_bos position and ignores + # the model's prediction there; the first real audio frame is sampled + # one step later at the audio_token_id position, so we skip the + # prediction at the bos row and emit the all-BOS frame instead. + if isinstance(first_after_bos, torch.Tensor) and first_after_bos.numel() == is_audio.shape[0]: + first_after_bos = first_after_bos.to(is_audio.device) + skip_rows = first_after_bos & is_audio + if bool(skip_rows.any()): + # Upstream's AUDIO_INIT step seeds ``audio_out_ids`` with + # a single all-``audio_stream_bos_id`` frame and the NEXT + # forward feeds ``embed_audio_codes(all_bos)`` at the new + # audio_out_token_idx position. Without this, our audio MLP + # receives a text embedding at the audio_token_id position + # and produces NaN logits. Seed per-slot state with the + # all-BOS frame here, AND emit it as the first output frame + # so the downstream delay-pattern revert sees the leading + # BOS-only frame upstream emits. + seeded_frame = torch.full((num_codebooks_local,), bos_local, dtype=torch.long, device=hidden.device) + for bi in torch.nonzero(skip_rows, as_tuple=False).reshape(-1).tolist(): + bi = int(bi) + # Reset stale state from a finished prior request. + self._slot_output_len[bi] = 0 + self._audio_state[bi] = { + "num_delay": 0, + "num_remaining_delays": None, + "audio_out_ids": seeded_frame.unsqueeze(-1).clone(), # [Q, 1] + } + seeded_rows.append(bi) + is_audio = is_audio & ~first_after_bos + + if not bool(is_audio.any()) and not seeded_rows: + self._last_audio_codes = None + return sampler_output + + # Map audio-position rows to their absolute batch indices. The + # delay-pattern + state-update loop below uses this for per-request + # state lookup; the RAS check between the head sample and the + # delay-pattern apply also reads it. + audio_row_indices = torch.nonzero(is_audio, as_tuple=False).reshape(-1).tolist() + + # Per-codebook logits at audio positions only. + cb_logits = self.audio_codebook_logits(hidden, is_audio) # [N_audio, Q, V] + + # Emulate upstream's ``HiggsAudioV2DelayPatternLogitsProcessor``. + # The raw audio_lm_head has very large weights at index 1025 + # (audio_stream_eos): cb1 |w[1025]|=9.25 vs mean content 1.53 (6x). + # Without masking, the model naturally fires 1025 at codebook 1..6 + # with huge confidence (logit 32 vs runner-up 6) and triggers + # premature ramp-down at frame 2. Upstream's processor MASKS + # forbidden vocab BEFORE sampling so each codebook k is bound to: + # - emit ``audio_stream_bos_id`` (1024) for k leading frames, + # - emit content tokens [0..1023] thereafter, + # - emit ``audio_stream_eos_id`` (1025) only during ramp-down. + bos_pre = int(self.config.audio_stream_bos_id) + eos_pre = int(self.config.audio_stream_eos_id) + for local_i, batch_i in enumerate(audio_row_indices): + state_pre = self._audio_state.get(int(batch_i)) + num_delay_pre = int(state_pre.get("num_delay", 0)) if state_pre else 0 + num_rem_pre = state_pre.get("num_remaining_delays") if state_pre else None + if num_rem_pre is not None: + # Ramp-down: mask all but EOS for the EOS-locked codebooks; the + # remaining ones still emit content. Locked range: codebooks + # [0 : num_codebooks - num_rem_pre]. + lock_until = int(self.config.num_codebooks) - int(num_rem_pre) + for q in range(int(self.config.num_codebooks)): + row = cb_logits[local_i, q] + if q < lock_until: + mask = torch.full_like(row, float("-inf")) + mask[eos_pre] = row[eos_pre] + cb_logits[local_i, q] = mask + else: + # Disallow stream specials during ramp-down content frames + cb_logits[local_i, q, bos_pre] = float("-inf") + cb_logits[local_i, q, eos_pre] = float("-inf") + else: + # Leading delay phase. Codebook k starts emitting real content + # at step k (delay = k). Steps with k > num_delay_pre are still + # in BOS phase — force 1024. Steps with k <= num_delay_pre are + # content — disallow 1024 (BOS) and 1025 (EOS). + for q in range(int(self.config.num_codebooks)): + row = cb_logits[local_i, q] + if q > num_delay_pre: + mask = torch.full_like(row, float("-inf")) + mask[bos_pre] = row[bos_pre] + cb_logits[local_i, q] = mask + else: + cb_logits[local_i, q, bos_pre] = float("-inf") + # Allow EOS at codebook 0 only (it gates ramp-down). + # Disallow at all other content codebooks. + if q != 0: + cb_logits[local_i, q, eos_pre] = float("-inf") + + # Sample per-codebook, following the upstream + # ``HiggsAudioModel._sample_audio_tokens`` pipeline byte-for-byte. + cb_logits_2d = cb_logits.reshape(-1, cb_logits.shape[-1]) + codes_2d = self._sample_audio_codes_upstream( + cb_logits_2d, + temperature=1.0, + top_k=50, + top_p=0.95, + ) + codes_flat = codes_2d.view(cb_logits.shape[0], cb_logits.shape[1]).to(torch.long) + + # (PyTorch align): Repetition-Aware Sampling (RAS). Upstream + # ``HiggsAudioModel._sample_audio_tokens`` re-samples any codebook + # that just emitted the same token >= ras_win_max_num_repeat times + # in the last ras_win_len steps. This breaks codebook collapse loops + # — a 3B model with greedy/low-temp sampling often locks into one + # value otherwise. + ras_win_len = 7 + ras_win_max_num_repeat = 2 + # Codes_flat is [N_audio, Q]; we re-sample PER (audio_position, codebook) + # row that hits the repeat threshold over the last ras_win_len audio + # frames of that codebook's history (from state.audio_out_ids). + if codes_flat.shape[0] > 0: + audio_idx_to_batch = audio_row_indices # already a list[int] + for local_i in range(codes_flat.shape[0]): + bi = int(audio_idx_to_batch[local_i]) + req_state = self._audio_state.get(bi) + if req_state is None: + continue + aoi = req_state.get("audio_out_ids") + if not isinstance(aoi, torch.Tensor) or aoi.shape[-1] < ras_win_len: + continue + window = aoi[:, -ras_win_len:].to(codes_flat.device) # [Q, W] + this_codes = codes_flat[local_i] # [Q] + rep_count = (window == this_codes.unsqueeze(-1)).sum(dim=-1) # [Q] + row_indices = torch.nonzero(rep_count >= ras_win_max_num_repeat, as_tuple=False).reshape(-1) + if row_indices.numel() == 0: + continue + # Re-sample those codebooks from cb_logits directly (no top-k/top-p + # in RAS resample per upstream — pure softmax + multinomial). + resample_logits = cb_logits[local_i, row_indices].float() + resample_probs = resample_logits.softmax(dim=-1) + resampled = torch.multinomial(resample_probs, num_samples=1).squeeze(-1) + codes_flat[local_i, row_indices] = resampled.to(codes_flat.dtype) + + # Apply the upstream delay pattern + EOS ramp-down per request. + num_codebooks = int(self.config.num_codebooks) + bos = int(self.config.audio_stream_bos_id) + eos_stream = int(self.config.audio_stream_eos_id) + audio_eos_vocab = int(getattr(self.config, "audio_eos_token_id", -1)) + + # ``audio_row_indices`` and ``self._audio_state`` were initialised + # earlier (right after ``is_audio`` was computed) so the RAS check + # can reference them. + + # Per-request reset: detect "new request occupied this slot" by + # tracking the per-slot output_token_ids length across sample() + # calls. Within a request the length monotonically grows; when it + # resets to 0 (or goes BELOW the previously-seen high-water mark), + # the previous occupant has finished and a new request now owns + # the slot. Drop the stale ``_audio_state`` for that slot. + if not hasattr(self, "_slot_output_len"): + self._slot_output_len: dict[int, int] = {} + output_ids = getattr(sampling_metadata, "output_token_ids", None) + for batch_i in audio_row_indices: + bi = int(batch_i) + current_len = len(output_ids[bi]) if output_ids is not None and bi < len(output_ids) else 0 + prior_len = self._slot_output_len.get(bi, -1) + # A fresh request has either never been seen (prior_len == -1 + # and we're at step 0) or has length less than the prior + # high-water mark. + is_new_request = (prior_len > current_len) or (prior_len == -1 and current_len == 0) + if is_new_request and bi in self._audio_state: + self._audio_state.pop(bi, None) + self._slot_output_len[bi] = current_len + + # Simplification: we DON'T override ``sampler_output.sampled_token_ids`` + # back to ``audio_eos_token_id`` at ramp-down completion. The vLLM + # ``SamplerOutput.sampled_token_ids`` is plumbed through several + # downstream structures that may have already cached the GPU pointer; + # an in-place mutation here was crashing the worker with exit code + # None (silent CUDA error). Instead, ``max_tokens`` (set in the deploy + # yaml) is the stop bound while the audio span ramps down naturally. + new_codes_flat: list[torch.Tensor] = [] + for local_i, batch_i in enumerate(audio_row_indices): + state = self._audio_state.setdefault( + int(batch_i), + {"num_delay": 0, "num_remaining_delays": None, "audio_out_ids": None}, + ) + num_delay: int = state["num_delay"] + num_remaining_delays: int | None = state["num_remaining_delays"] + this_codes = codes_flat[local_i].clone() # [Q] + + # Leading delay-pattern BOS pad. + if num_delay + 1 < num_codebooks: + this_codes[num_delay + 1 :] = bos + num_delay += 1 + + # Trailing eos ramp-down. + if num_remaining_delays is not None: + this_codes[: num_codebooks - num_remaining_delays] = eos_stream + num_remaining_delays -= 1 + else: + eos_positions = (this_codes == eos_stream).nonzero(as_tuple=False).reshape(-1) + if eos_positions.numel() > 0: + last_eos_idx = int(eos_positions[-1].item()) + this_codes[: last_eos_idx + 1] = eos_stream + num_remaining_delays = num_codebooks - last_eos_idx - 1 + + if num_remaining_delays is not None and num_remaining_delays <= 0: + # Audio ramp-down finished. Set a per-slot termination flag + # so the NEXT step's bias forces audio_eos_token_id (upstream's + # ``next_tokens[...] = audio_eos_token_id`` override at the + # ramp-completion point — see modeling_higgs_audio.py:1564). + _ = audio_eos_vocab + num_delay = 0 + num_remaining_delays = None + state["num_delay"] = num_delay + state["num_remaining_delays"] = num_remaining_delays + state["should_terminate"] = True + new_codes_flat.append(torch.full_like(this_codes, -1)) + continue + + state["num_delay"] = num_delay + state["num_remaining_delays"] = num_remaining_delays + # Append to the per-request audio_out_ids buffer. + if state["audio_out_ids"] is None: + state["audio_out_ids"] = this_codes.unsqueeze(-1).clone() + else: + state["audio_out_ids"] = torch.cat([state["audio_out_ids"], this_codes.unsqueeze(-1)], dim=-1) + new_codes_flat.append(this_codes) + + # Reassemble the per-position codes tensor with the post-delay-pattern + # values; `-1` rows mark "no audio code at this position". + codes_full = torch.full( + (int(sampled_flat.numel()), num_codebooks), + -1, + dtype=torch.long, + device=hidden.device, + ) + if new_codes_flat: + stacked = torch.stack(new_codes_flat, dim=0).to(hidden.device) + codes_full[is_audio] = stacked + # Emit the seeded all-BOS frame at the prefill-end (AUDIO_INIT) + # rows so the talker output has the leading delay-pattern frame the + # downstream revert helper expects. + if seeded_rows and seeded_frame is not None: + seeded_rows_idx = torch.tensor(seeded_rows, dtype=torch.long, device=hidden.device) + codes_full.index_copy_( + 0, + seeded_rows_idx, + seeded_frame.unsqueeze(0).expand(len(seeded_rows), -1).to(hidden.device), + ) + + self._last_audio_codes = codes_full + self._postprocess_cursor = 0 + return sampler_output + + @staticmethod + def _sample_audio_codes_upstream( + logits: torch.Tensor, + temperature: float = 1.0, + top_k: int | None = None, + top_p: float | None = None, + ) -> torch.Tensor: + """Replicate the upstream HF ``LogitsProcessorList`` pipeline that + ``HiggsAudioModel._sample_audio_tokens`` calls via + ``logits_processor(None, next_audio_token_logits)``. + + Order (matching transformers' standard ``TemperatureLogitsWarper``, + ``TopKLogitsWarper``, ``TopPLogitsWarper``): + + 1. Divide by temperature. + 2. Top-k: keep the K largest, set others to -inf. + 3. Top-p (nucleus): keep the smallest prefix whose cumulative + softmax probability is <= top_p; mask the rest. Always keep at + least the top-1. + 4. Softmax → multinomial sample. + + ``logits`` shape: ``[N, V]``. Returns ``[N]`` of int sampled indices. + """ + x = logits.float() + if temperature is not None and temperature > 0.0 and temperature != 1.0: + x = x / temperature + if top_k is not None and 0 < top_k < x.shape[-1]: + kth = x.topk(top_k, dim=-1).values[..., -1:] + x = x.masked_fill(x < kth, float("-inf")) + if top_p is not None and 0.0 < top_p < 1.0: + sorted_logits, sorted_idx = x.sort(dim=-1, descending=True) + cumprobs = sorted_logits.softmax(dim=-1).cumsum(dim=-1) + sorted_mask = cumprobs > top_p + # Shift right by one so we always keep at least the top-1 token. + sorted_mask[..., 1:] = sorted_mask[..., :-1].clone() + sorted_mask[..., 0] = False + # Scatter back to original index space and apply. + mask = torch.zeros_like(x, dtype=torch.bool) + mask.scatter_(-1, sorted_idx, sorted_mask) + x = x.masked_fill(mask, float("-inf")) + if temperature is None or temperature <= 0.0: + return torch.argmax(x, dim=-1) + probs = x.softmax(dim=-1) + # Guard against any all-masked rows (shouldn't happen with the shift + # above) — fall back to argmax of original logits. + valid = probs.sum(dim=-1) > 0 + sampled = torch.empty(x.shape[0], dtype=torch.long, device=x.device) + if valid.any(): + sampled[valid] = torch.multinomial(probs[valid], num_samples=1).squeeze(-1) + if (~valid).any(): + sampled[~valid] = torch.argmax(logits[~valid].float(), dim=-1) + return sampled + + def _apply_audio_mode_bias(self, logits: torch.Tensor, sampling_metadata: Any) -> int: + """Mask non-audio tokens at audio-mode positions, in-place on ``logits``. + + Heuristic: per-request, find the last token seen so far (last of + ``output_token_ids[i]`` if non-empty, else last of + ``prompt_token_ids[i]``). If that token is ``audio_bos_token_id`` or + ``audio_token_id``, force the next emit to be one of + ``{audio_token_id, audio_eos_token_id}``. This unblocks live audio + generation since the un-biased argmax over the 128k vocab favours + text-token-id ranges (in particular the global ``eos_token_id``). + + We deliberately do NOT include the standard ``eos_token_id`` in the + allowed set — letting it through would end the whole sequence at the + first step. Stopping inside the audio span is the ``audio_eos`` token's + job; once it fires we drop the bias and the next call falls back to + the stock sampler. + """ + if logits is None or logits.ndim != 2: + return 0 + audio_bos = int(self.config.audio_bos_token_id) + audio_id = int(self.config.audio_token_id) + # (PyTorch align, corrected): upstream NEVER lets the model + # naturally emit ``audio_eos_token_id`` in the LM stream. The LM + # next-token is force-set to ``audio_out_token_idx`` every step; + # the audio span only ends when the delay-pattern ramp-down completes + # (driven by ``audio_stream_eos_id`` appearing in a codebook), at + # which point upstream OVERRIDES the LM token to ``audio_eos``. + # Our diag showed the model's ``audio_eos`` logit is very high right + # after the first audio step; allowing it via sampling stops the + # audio span with 1 frame and the rest of the LM stream goes off-rails. + # Force ONLY ``audio_token_id``. + allowed_extra: list[int] = [] + # Walk per-request to decide which rows to mask. + prompt_ids = getattr(sampling_metadata, "prompt_token_ids", None) + output_ids = getattr(sampling_metadata, "output_token_ids", None) + num_rows = int(logits.shape[0]) + biased = 0 + # Fallback "previous token" source: read each request's LAST input + # position from ``self._last_step_input_ids`` using ``query_start_loc`` + # from the forward context. The previous version of this code took + # ``stash_ids[-num_rows:]`` which only works for pure-decode batches + # (where each row contributes exactly one token to the flat input_ids). + # In MIXED prefill+decode batches the last N tokens of flat input_ids + # all belong to the LAST prefilling request, so the "prev token" for + # other rows came back as random tokens from inside that request's + # prompt — which caused the audio_out_bos detection to fail for some + # rows, the audio_token_id bias to skip them, and the per-row codes to + # collapse to -1 silently (see the batch>1 voice-clone gibberish bug). + stash_ids = getattr(self, "_last_step_input_ids", None) + stash_tail: list[int] | None = None + if isinstance(stash_ids, torch.Tensor) and stash_ids.numel() > 0: + # forward() stashed query_start_loc for us inside the forward + # context — get_forward_context() would fail here because we're + # past the with-block by the time sample() runs. + q_start = getattr(self, "_last_step_query_start_loc", None) + if isinstance(q_start, torch.Tensor) and int(q_start.numel()) == num_rows + 1: + # Per-request last position: q_start_loc[i+1] - 1. + q_start_cpu = q_start.detach().to("cpu").tolist() + tail_idx = [max(0, int(q_start_cpu[i + 1]) - 1) for i in range(num_rows)] + flat_ids = stash_ids.detach().to("cpu").tolist() + stash_tail = [int(flat_ids[idx]) if idx < len(flat_ids) else -1 for idx in tail_idx] + elif int(stash_ids.numel()) >= num_rows: + # Pure-decode fallback when query_start_loc is unavailable: + # input_ids has exactly num_rows tokens (one per request) and + # the slice is order-preserving. + stash_tail = stash_ids[-num_rows:].detach().to("cpu").tolist() + # Also stash a per-row "is this the first audio-mode step right + # after audio_out_bos" flag. Upstream NEVER samples audio codes at + # the audio_out_bos position (its audio_out_mask is False there); + # the first audio frame is sampled one step later, at the new + # audio_out_token_idx position. ``sample()`` reads this flag to + # suppress the audio-code sampling at the prefill-end step so the + # first audio frame is sampled from the correct hidden state. + first_after_bos = torch.zeros(num_rows, dtype=torch.bool, device=logits.device) + for i in range(num_rows): + prev: int | None = None + if output_ids is not None and i < len(output_ids): + hist = output_ids[i] + if hist: + prev = int(hist[-1]) + if prev is None and prompt_ids is not None: + # Prompt_token_ids may be a tensor or a list-of-lists + try: + p_i = prompt_ids[i] + if hasattr(p_i, "tolist"): + p_i = p_i.tolist() + if p_i: + prev = int(p_i[-1]) + except (IndexError, TypeError): + prev = None + if prev is None and stash_tail is not None and i < len(stash_tail): + prev = int(stash_tail[i]) + if prev is None: + continue + if prev not in (audio_bos, audio_id): + continue + if prev == audio_bos: + first_after_bos[i] = True + # If this slot's audio ramp finished last step, force LM token + # to audio_eos so the engine stops naturally. Matches upstream's + # ``next_tokens[...] = audio_eos_token_id`` override. + audio_state = getattr(self, "_audio_state", {}) or {} + slot_state = audio_state.get(i) + should_terminate = bool(isinstance(slot_state, dict) and slot_state.get("should_terminate")) + audio_eos_vocab = int(getattr(self.config, "audio_eos_token_id", -1)) + if should_terminate and 0 <= audio_eos_vocab < int(logits.shape[-1]): + # Force audio_eos at this position; upstream override at + # ramp-down completion (modeling_higgs_audio.py:1564). + row = logits[i] + mask = torch.full_like(row, float("-inf")) + mask[audio_eos_vocab] = row[audio_eos_vocab] + logits[i].copy_(mask) + slot_state["should_terminate"] = False + biased += 1 + continue + # Always force ``audio_token_id`` — upstream mirror. + allowed: set[int] = {audio_id, *allowed_extra} + row = logits[i] + mask = torch.full_like(row, float("-inf")) + for tok in allowed: + if 0 <= tok < row.shape[-1]: + mask[tok] = row[tok] + logits[i].copy_(mask) + biased += 1 + self._last_first_audio_after_bos = first_after_bos + return biased + + has_postprocess: bool = True + + def postprocess( + self, + hidden_states_slice: torch.Tensor, + multimodal_outputs: Any = None, + **req_infos: Any, + ) -> dict[str, Any]: + """Publish per-request audio codes into model_intermediate_buffer. + + The runner calls this once per request in batch order. We index into + ``self._last_audio_codes`` (a per-batch ``[num_requests, num_codebooks]`` + tensor produced by :meth:`sample`, one row per request slot regardless + of prompt span length) by a running cursor — exactly one row per + request. Using ``hidden_states_slice.shape[0]`` as the stride is + WRONG at prefill: prompt span length is hundreds of tokens, but each + request still contributes exactly one sampled row to ``_last_audio_codes``. + That bug used to drop the first audio frame at prefill-end (the + all-BOS seed) and, in mixed prefill+decode batches, scrambled the + per-request → codes mapping so batch=4 output came back shifted / + cross-talked across slots. + """ + _ = multimodal_outputs # consumed by the runner directly + codes_full = getattr(self, "_last_audio_codes", None) + if codes_full is None: + return {} + + # The runner walks requests in batch order; pop from the cursor as we go. + # ``_last_audio_codes`` is one row per request slot (== one logits_indices + # row), not one row per prompt token, so advance by 1 per call. + cursor = int(getattr(self, "_postprocess_cursor", 0)) + if cursor >= int(codes_full.shape[0]): + # Defensive: shape drift between forward and postprocess. + self._postprocess_cursor = 0 + return {} + slice_codes = codes_full[cursor : cursor + 1] + self._postprocess_cursor = cursor + 1 + # Drop placeholder rows (-1) — only emit codes for actual audio positions. + audio_rows = slice_codes[:, 0] >= 0 + if not bool(audio_rows.any()): + return {} + new_codes = slice_codes[audio_rows].to(torch.int32) + + # Return ONLY this step's frames. The engine's + # MultimodalOutputProcessor.add_multimodal_tensor accumulates per-step + # OmniOutput.multimodal_outputs across all steps and consolidates at + # request finish via torch.cat(dim=0). Returning cumulative codes here + # caused N(N+1)/2 duplication (prior bug: engine internal accumulation). + return {"codes": {"audio": new_codes}} diff --git a/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_tokenizer.py b/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_tokenizer.py new file mode 100644 index 000000000..a51d692b9 --- /dev/null +++ b/vllm_omni/model_executor/models/higgs_audio_v2/higgs_audio_v2_tokenizer.py @@ -0,0 +1,332 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""TTS prompt builder + scope validators for higgs-audio v2. + +vllm-omni's higgs path supports two request shapes: + +* **Plain text -> 24 kHz speech.** :func:`build_plain_text_prompt` runs the + upstream HF processor with a bare ``"Generate audio following instruction."`` + system prompt; the emitted ``input_ids`` are byte-identical to upstream. +* **Voice clone (shallow).** :func:`build_voice_clone_prompt` runs the + upstream HF processor with both a target text turn and a reference + ``(ref_audio, ref_text)`` ICL turn; the processor returns ``input_ids`` + pre-expanded with audio placeholders plus an ``audio_input_ids`` tensor + carrying the encoded reference codes (HF tokenizer encodes audio on the + spot — no encoder vendored in vllm-omni). + +Still out of scope and rejected with explicit 4xx: multi-speaker +``[SPEAKERn]`` dialogue, ``profile:`` text-only speaker descriptions, the +``ref_audio_in_system_message`` system-block variant, and chunked long-form +generation. +""" + +from __future__ import annotations + +import os +import re +from typing import Any + +import numpy as np +import torch + +__all__ = [ + "UnsupportedInputError", + "MULTI_SPEAKER_TAG_PATTERN", + "validate_plain_text_input", + "build_plain_text_conversation", + "build_plain_text_prompt", + "build_voice_clone_conversation", + "build_voice_clone_prompt", + "input_ids_to_python_list", +] + + +class UnsupportedInputError(ValueError): + """Raised when a request asks for an out-of-scope higgs_audio_v2 feature.""" + + +# Matches the upstream multi-speaker SPEAKERn tag, e.g. [SPEAKER0], [SPEAKER12]. +MULTI_SPEAKER_TAG_PATTERN = re.compile(r"\[SPEAKER\d+\]", re.IGNORECASE) + + +def validate_plain_text_input(text: str) -> None: + """Reject multi-speaker tags inside the user text body. + + Phase-1 explicitly does NOT support multi-speaker dialogue. Catching the + pattern here means the rejection happens at the tokenizer boundary and is + visible to both offline (`pipeline.py`) and online (`serving_speech.py`) + code paths. + """ + if not isinstance(text, str): + raise UnsupportedInputError(f"higgs_audio_v2 expects plain text input; got {type(text).__name__}") + if MULTI_SPEAKER_TAG_PATTERN.search(text): + raise UnsupportedInputError( + "higgs_audio_v2 v1 does not support multi-speaker [SPEAKERn] tags; received text contains a speaker tag" + ) + + +def build_plain_text_conversation(text: str) -> list[dict[str, Any]]: + """Build the canonical single-speaker plain-text conversation. + + Uses the bare system prompt ``"Generate audio following instruction."`` + that matches the upstream HF reference's input formatting; this exact + wording is required for input-token parity with the upstream processor. + """ + validate_plain_text_input(text) + system_prompt = "Generate audio following instruction." + return [ + { + "role": "system", + "content": [{"type": "text", "text": system_prompt}], + }, + { + "role": "user", + "content": [{"type": "text", "text": text}], + }, + ] + + +def build_plain_text_prompt( + processor: Any, + text: str, + *, + sampling_rate: int = 24000, + return_tensors: str | None = "pt", +) -> dict[str, Any]: + """Run the upstream processor's chat template on a plain-text input. + + Returns the processor output dict (``input_ids`` plus any auxiliary tensors + such as ``attention_mask``). The serving layer passes ``input_ids`` to + Stage 0 as ``prompt_token_ids`` after a ``.tolist()``. + + Using the upstream processor verbatim (no system-prompt rewriting) is + what preserves input-token parity with the HF reference. + """ + conversation = build_plain_text_conversation(text) + inputs = processor.apply_chat_template( + conversation, + add_generation_prompt=True, + tokenize=True, + return_dict=True, + sampling_rate=sampling_rate, + return_tensors=return_tensors, + ) + if "input_ids" not in inputs: + raise RuntimeError(f"HiggsAudioV2 processor returned no input_ids; got keys {list(inputs.keys())!r}") + return inputs + + +def input_ids_to_python_list(inputs: dict[str, Any]) -> list[int]: + """Convenience: pull a flat ``list[int]`` of token IDs from a processor output.""" + ids = inputs["input_ids"] + if isinstance(ids, torch.Tensor): + if ids.ndim == 2 and int(ids.shape[0]) != 1: + raise ValueError(f"expected batch=1 prompt; got input_ids shape {tuple(ids.shape)}") + return ids.reshape(-1).tolist() + return list(ids) + + +_AUDIO_OUT_TOKEN = "<|AUDIO_OUT|>" +_AUDIO_OUT_BOS_TOKEN = "<|audio_out_bos|>" +_AUDIO_EOS_TOKEN = "<|audio_eos|>" +_AUDIO_DELAY_TOKEN = "<|reserved_special_token_6|>" +_AUDIO_STREAM_BOS_ID = 1024 +_AUDIO_STREAM_EOS_ID = 1025 + + +def _build_delay_pattern(codes: torch.Tensor) -> torch.Tensor: + """Apply the upstream delay-pattern wrap to a ``[num_codebooks, T]`` code tensor. + + Mirrors ``HiggsAudioV2Processor.build_delay_pattern``: prepend BOS, append + EOS, then arrange in a triangular pattern that stretches the sequence by + ``num_codebooks - 1`` frames so codebook k starts emitting real codes at + frame k. + """ + num_codebooks, seq_len = codes.shape + bos = codes.new_full((num_codebooks, 1), _AUDIO_STREAM_BOS_ID) + eos = codes.new_full((num_codebooks, 1), _AUDIO_STREAM_EOS_ID) + wrapped = torch.cat([bos, codes, eos], dim=1) + wrapped_len = wrapped.shape[1] + new_seq_len = wrapped_len + num_codebooks - 1 + + output = torch.ones((1, num_codebooks, new_seq_len), dtype=codes.dtype, device=codes.device) + bos_mask = torch.tril(output, -1) > 0 + eos_mask = torch.triu(output, wrapped_len) > 0 + data_mask = ~(bos_mask | eos_mask) + output[bos_mask] = _AUDIO_STREAM_BOS_ID + output[data_mask] = wrapped.reshape(-1) + output[eos_mask] = _AUDIO_STREAM_EOS_ID + return output[0] + + +_ENCODER_CACHE: Any | None = None +_K2_OMNIVOICE_REPO = "k2-fsa/OmniVoice" +_K2_OMNIVOICE_SUBDIR = "audio_tokenizer" + + +def _load_audio_tokenizer(): + """Load the higgs-audio v2 codec via HF ``HiggsAudioV2TokenizerModel``. + + The boson-ai standalone tokenizer repo (``bosonai/higgs-audio-v2-tokenizer``) + ships a ``model.safetensors`` that is actually a copy of the 3B talker LM, + not the codec — pointing HF's ``from_pretrained`` at it yields an + encoder/decoder with randomly initialized structural weights, producing + noise codes at encode time. The k2-fsa OmniVoice repo bundles the same + codec weights repackaged under + ``audio_tokenizer/{config.json,model.safetensors,preprocessor_config.json}`` + with key naming aligned to ``HiggsAudioV2TokenizerModel``'s class layout + (``acoustic_encoder.*`` / ``acoustic_decoder.*`` / ``quantizer.quantizers.*`` + / ``fc/fc2`` / ``semantic_model.*``), so HF can load it directly. + + Only the ``audio_tokenizer/`` subdirectory (~806 MB) is downloaded. + """ + from huggingface_hub import snapshot_download + from transformers import HiggsAudioV2TokenizerModel + + repo_path = snapshot_download( + _K2_OMNIVOICE_REPO, + allow_patterns=[f"{_K2_OMNIVOICE_SUBDIR}/*"], + ) + audio_tokenizer_dir = os.path.join(repo_path, _K2_OMNIVOICE_SUBDIR) + device = "cuda" if torch.cuda.is_available() else "cpu" + model = HiggsAudioV2TokenizerModel.from_pretrained(audio_tokenizer_dir).to(device) + return model.eval() + + +def _encode_ref_audio_codes( + wav: np.ndarray, + sr: int, +) -> torch.Tensor: + """Encode a single ref clip to codec codes via HF ``HiggsAudioV2TokenizerModel``. + + Returns shape ``[num_codebooks, T_raw]`` (before BOS/EOS + delay-pattern wrap). + """ + import torchaudio + + global _ENCODER_CACHE + if _ENCODER_CACHE is None: + try: + _ENCODER_CACHE = _load_audio_tokenizer() + except ImportError as exc: + raise RuntimeError( + "higgs_audio_v2 voice clone needs `transformers>=5.3.0` " + "(which ships the HiggsAudioV2TokenizerModel class). " + "Install via `pip install -U 'transformers>=5.3.0'`." + ) from exc + + target_sr = int(getattr(_ENCODER_CACHE.config, "sample_rate", 24000)) + wav_t = torch.as_tensor(wav, dtype=torch.float32).reshape(-1) + if sr and sr != target_sr: + wav_t = torchaudio.functional.resample(wav_t, sr, target_sr) + # HF encode() expects (batch=1, channels=1, num_samples). + input_values = wav_t.unsqueeze(0).unsqueeze(0).to(_ENCODER_CACHE.device) + with torch.inference_mode(): + codes = _ENCODER_CACHE.encode(input_values, return_dict=False) + # codes shape: (batch=1, num_quantizers, codes_length) -> (num_quantizers, T). + if codes.dim() == 3: + codes = codes.squeeze(0) + return codes.detach().to("cpu").long() + + +def build_voice_clone_conversation( + text: str, + ref_text: str, +) -> list[dict[str, Any]]: + """ChatML conversation for voice clone (HF jinja-template-compatible). + + The assistant turn is a list with a single ``{"type": "audio"}`` content + block — no ``"audio"``/``"url"``/``"path"`` key, so ``apply_chat_template`` + won't try to extract audio data (that path would collide with our explicit + audio encoder). The template renders the assistant block as the literal + ``<|audio_out_bos|><|AUDIO_OUT|><|audio_eos|>``; we expand the single + ``<|AUDIO_OUT|>`` token to ``N × audio_token + (num_codebooks-1) × delay_token`` + in :func:`build_voice_clone_prompt` to match the reference clip's frame count. + """ + validate_plain_text_input(text) + validate_plain_text_input(ref_text) + return [ + {"role": "system", "content": "Generate audio following instruction."}, + {"role": "user", "content": ref_text}, + {"role": "assistant", "content": [{"type": "audio"}]}, + {"role": "user", "content": text}, + ] + + +def build_voice_clone_prompt( + processor: Any, + text: str, + ref_audio_wav: np.ndarray | torch.Tensor, + ref_audio_sr: int, + ref_text: str, + *, + return_tensors: str | None = "pt", +) -> dict[str, Any]: + """Build a voice-clone prompt using the upstream boson audio tokenizer. + + Returns a dict carrying: + - ``prompt_token_ids``: ``list[int]`` — input_ids with the ref-audio + ``<|AUDIO_OUT|>`` + ``<|reserved_special_token_6|>`` placeholders + already expanded to match the encoded reference clip's frame count. + - ``audio_input_ids``: ``Tensor[T_frames, num_codebooks]`` — encoded + reference codes with BOS/EOS + delay pattern, transposed to ``[T, Q]`` + for parity with the HF processor output shape. + - ``audio_input_ids_mask``: ``Tensor[T_frames]`` — all-True bool mask. + """ + if isinstance(ref_audio_wav, torch.Tensor): + wav = ref_audio_wav.detach().to("cpu").float().reshape(-1).numpy() + else: + wav = np.asarray(ref_audio_wav, dtype=np.float32).reshape(-1) + + # 1. Encode via the upstream boson tokenizer (real weights from model.pth). + codes_qt = _encode_ref_audio_codes(wav, ref_audio_sr) # [num_codebooks, T_raw] + if codes_qt.ndim == 3: + codes_qt = codes_qt[0] + num_codebooks = int(codes_qt.shape[0]) + + # 2. Apply BOS/EOS + delay-pattern wrap. + audio_input_ids = _build_delay_pattern(codes_qt) # [num_codebooks, T_full] + T_full = int(audio_input_ids.shape[1]) + + # 3. Render the chat template, then expand the single <|AUDIO_OUT|> marker + # in the assistant turn into the full placeholder block. After expansion + # the text contains exactly T_full audio-mask positions, matching the + # audio_input_ids time axis. + conversation = build_voice_clone_conversation(text, ref_text) + rendered = processor.tokenizer.apply_chat_template( + conversation, + add_generation_prompt=True, + tokenize=False, + ) + n_delay = num_codebooks - 1 + n_audio = T_full - n_delay + if n_audio < 0: + raise RuntimeError(f"ref clip too short ({T_full} frames) for delay pattern with num_codebooks={num_codebooks}") + placeholders = _AUDIO_OUT_TOKEN * n_audio + _AUDIO_DELAY_TOKEN * n_delay + expanded_assistant = f"{_AUDIO_OUT_BOS_TOKEN}{placeholders}{_AUDIO_EOS_TOKEN}" + if _AUDIO_OUT_TOKEN not in rendered: + raise RuntimeError( + f"Voice-clone chat-template render is missing the assistant " + f"audio placeholder marker. conversation={conversation!r}" + ) + rendered = rendered.replace(_AUDIO_OUT_TOKEN, expanded_assistant, 1) + + # 4. Tokenize the rendered prompt. ``apply_chat_template`` already emits + # ``<|begin_of_text|>``, so disable add_special_tokens. + encoded = processor.tokenizer( + rendered, + add_special_tokens=False, + return_tensors=return_tensors, + ) + prompt_token_ids = encoded["input_ids"] + if isinstance(prompt_token_ids, torch.Tensor): + prompt_token_ids = prompt_token_ids.reshape(-1).tolist() + + # 5. Transpose codes to [T_full, num_codebooks] for HF parity. + audio_input_ids_t = audio_input_ids.transpose(0, 1).contiguous().to(torch.long) + audio_input_ids_mask = torch.ones(T_full, dtype=torch.bool) + + return { + "prompt_token_ids": prompt_token_ids, + "audio_input_ids": audio_input_ids_t, + "audio_input_ids_mask": audio_input_ids_mask, + } diff --git a/vllm_omni/model_executor/models/higgs_audio_v2/pipeline.py b/vllm_omni/model_executor/models/higgs_audio_v2/pipeline.py new file mode 100644 index 000000000..fb6832569 --- /dev/null +++ b/vllm_omni/model_executor/models/higgs_audio_v2/pipeline.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""higgs-audio v2 pipeline: Talker (text -> 8-codebook codec) -> Code2Wav (codec -> 24 kHz PCM).""" + +from vllm_omni.config.stage_config import ( + PipelineConfig, + StageExecutionType, + StagePipelineConfig, +) + +_PROC = "vllm_omni.model_executor.stage_input_processors.higgs_audio_v2" + +HIGGS_AUDIO_V2_PIPELINE = PipelineConfig( + model_type="higgs_audio_v2", + model_arch="HiggsAudioV2ForConditionalGeneration", + hf_architectures=("HiggsAudioV2ForConditionalGeneration",), + stages=( + StagePipelineConfig( + stage_id=0, + model_stage="higgs_audio_v2", + execution_type=StageExecutionType.LLM_AR, + input_sources=(), + owns_tokenizer=True, + engine_output_type="latent", + async_chunk_process_next_stage_input_func=(f"{_PROC}.talker2code2wav_async_chunk"), + sampling_constraints={ + "detokenize": False, + # Two stop signals for Stage-0: + # * 128009 = standard LM eos_token_id (sequence-level stop) + # * 128012 = audio_eos_token_id (forced by the talker's + # _apply_audio_mode_bias at audio-ramp completion; matches + # upstream HiggsAudioModel._sample_audio_tokens override). + "stop_token_ids": [128009, 128012], + }, + ), + StagePipelineConfig( + stage_id=1, + model_stage="code2wav", + execution_type=StageExecutionType.LLM_GENERATION, + input_sources=(0,), + final_output=True, + final_output_type="audio", + engine_output_type="audio", + model_arch="HiggsAudioV2Code2WavForConditionalGeneration", + sync_process_input_func=f"{_PROC}.talker2code2wav", + sampling_constraints={"detokenize": True}, + ), + ), +) diff --git a/vllm_omni/model_executor/models/registry.py b/vllm_omni/model_executor/models/registry.py index 4c49afc36..ff61d6e6c 100644 --- a/vllm_omni/model_executor/models/registry.py +++ b/vllm_omni/model_executor/models/registry.py @@ -102,6 +102,22 @@ _OMNI_MODELS = { "qwen3_tts_code2wav", "Qwen3TTSCode2Wav", ), + ## higgs-audio v2 + "HiggsAudioV2ForConditionalGeneration": ( + "higgs_audio_v2", + "higgs_audio_v2_talker", + "HiggsAudioV2TalkerForConditionalGeneration", + ), + "HiggsAudioV2TalkerForConditionalGeneration": ( + "higgs_audio_v2", + "higgs_audio_v2_talker", + "HiggsAudioV2TalkerForConditionalGeneration", + ), + "HiggsAudioV2Code2WavForConditionalGeneration": ( + "higgs_audio_v2", + "higgs_audio_v2_code2wav", + "HiggsAudioV2Code2WavForConditionalGeneration", + ), ## mimo_audio "MiMoAudioModel": ( "mimo_audio", diff --git a/vllm_omni/model_executor/stage_input_processors/higgs_audio_v2.py b/vllm_omni/model_executor/stage_input_processors/higgs_audio_v2.py new file mode 100644 index 000000000..0d6781586 --- /dev/null +++ b/vllm_omni/model_executor/stage_input_processors/higgs_audio_v2.py @@ -0,0 +1,252 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Stage-input processor for higgs-audio v2: Talker -> Code2Wav. + +Mirrors the qwen3_tts contract: + +- ``talker2code2wav`` is the sync (non-async-chunk) adapter that runs once, + after Stage 0 has finished, collecting every audio frame into a single + ``OmniTokensPrompt`` for Stage 1. +- ``talker2code2wav_async_chunk`` is the streaming adapter invoked once per + emitted codec frame; it accumulates frames in a per-request buffer and + flushes an ``OmniPayloadStruct`` once a chunk is ready (or at end-of-stream). + +Stage 1 strictly rejects any code id >= ``audio_stream_bos_id`` (1024). This +processor filters frames that still carry stream specials so the codec sees +only real codes in [0, 1023], regardless of where Stage 0 emitted them. +""" + +from __future__ import annotations + +from typing import Any + +import torch +from vllm.logger import init_logger + +from vllm_omni.data_entry_keys import ( + CodesStruct, + MetaStruct, + OmniPayload, + OmniPayloadStruct, +) + +__all__ = ["talker2code2wav", "talker2code2wav_async_chunk"] + +logger = init_logger(__name__) + +# Default codec chunk-shape knobs for async_chunk mode. These match the +# canonical Qwen3-TTS defaults (25 frames = 1 s at 25 fps). Real values +# come from the connector config; these are only fallbacks. +_DEFAULT_CODEC_CHUNK_FRAMES = 25 +_DEFAULT_CODEC_LEFT_CONTEXT_FRAMES = 25 + +# Boson-ai constants. Kept local (rather than imported from the model package) +# so this module remains usable without circular imports during +# stage-input-processor discovery. +_NUM_CODEBOOKS = 8 +_AUDIO_STREAM_BOS_ID = 1024 +_AUDIO_STREAM_EOS_ID = 1025 +_NUM_REAL_CODES = _AUDIO_STREAM_BOS_ID # codes in [0, 1023] are real + + +def _filter_real_code_frames(audio_codes: torch.Tensor) -> torch.Tensor: + """Keep only frames whose codes are entirely in [0, 1023]. + + ``audio_codes`` has shape ``[num_frames, num_codebooks]`` after the + standard Stage-0 transpose. We drop frames containing stream specials + or anything padded outside the real-code range. + """ + if audio_codes.numel() == 0: + return audio_codes + if audio_codes.ndim != 2: + raise ValueError(f"expected [num_frames, num_codebooks] audio_codes; got shape {tuple(audio_codes.shape)}") + valid = (audio_codes >= 0).all(dim=1) & (audio_codes < _NUM_REAL_CODES).all(dim=1) + return audio_codes[valid] + + +def _revert_delay_pattern(audio_codes_qt: torch.Tensor) -> torch.Tensor: + """Reverse the delay-pattern shift, mirroring upstream's + ``boson_multimodal.model.higgs_audio.utils.revert_delay_pattern``. + + Input shape: ``[num_codebooks, seq_len + num_codebooks - 1]``. + Output shape: ``[num_codebooks, seq_len]``. + + For each codebook ``i``, slice rows ``[i:i+1]`` from columns + ``[i : seq_len + i]`` to remove ``i`` leading BOS pads and ``Q-1-i`` + trailing PAD entries. + """ + if audio_codes_qt.ndim != 2: + raise ValueError(f"_revert_delay_pattern expects [Q, T] input; got {tuple(audio_codes_qt.shape)}") + q, t = audio_codes_qt.shape + if t < q: + # Not enough frames to revert delay pattern; return as-is. + return audio_codes_qt + seq_len = t - q + 1 + out_l = [] + for i in range(q): + out_l.append(audio_codes_qt[i : i + 1, i : seq_len + i]) + return torch.cat(out_l, dim=0) + + +def talker2code2wav( + source_outputs: list[Any], + prompt: Any = None, + _requires_multimodal_data: bool = False, +) -> list[Any]: + """Sync: collect all talker codes, then pass to code2wav at once.""" + from vllm_omni.inputs.data import OmniTokensPrompt + + code2wav_inputs: list[OmniTokensPrompt] = [] + for talker_output in source_outputs: + if not talker_output.finished: + continue + output = talker_output.outputs[0] + mm = output.multimodal_output + mm_codes = mm.get("codes", {}) + + audio_codes = mm_codes.get("audio") + if audio_codes is None or not isinstance(audio_codes, torch.Tensor) or audio_codes.numel() == 0: + # Nothing to decode for this request; emit an empty payload so the + # downstream stage can still close the response. + code2wav_inputs.append( + OmniTokensPrompt( + prompt_token_ids=[], + multi_modal_data=None, + mm_processor_kwargs=None, + additional_information=None, + ) + ) + continue + + audio_codes = audio_codes.to(torch.long) + if audio_codes.ndim == 1: + # Stage 0 emitted a flat sequence; reshape to [num_frames, num_codebooks]. + if audio_codes.numel() % _NUM_CODEBOOKS != 0: + raise ValueError( + f"flat audio_codes length {audio_codes.numel()} not divisible by num_codebooks={_NUM_CODEBOOKS}" + ) + audio_codes = audio_codes.reshape(-1, _NUM_CODEBOOKS) + + if audio_codes.ndim != 2: + raise ValueError(f"audio_codes must be 1D or 2D; got shape {tuple(audio_codes.shape)}") + + # Audio_codes from Stage-0 are emitted in DELAY-PATTERN layout. + # Revert the shift to recover the canonical ``[Q, seq_len]`` form, + # then clip to the real-code range and trim the first/last frame + # (the codec doesn't consume the BOS/EOS edge frames). This mirrors + # the upstream ``HiggsAudioServeEngine.generate`` decode line: + # vq_code = revert_delay_pattern(out).clip(0, real-1)[:, 1:-1] + codes_qt = audio_codes.transpose(0, 1).contiguous().cpu() + codes_qt = _revert_delay_pattern(codes_qt) + codes_qt = codes_qt.clamp_(min=0, max=_NUM_REAL_CODES - 1) + if codes_qt.shape[-1] >= 3: + codes_qt = codes_qt[:, 1:-1] + # Code2Wav expects codebook-major flat: [Q * num_frames]. + codec_codes = codes_qt.reshape(-1).tolist() + + code2wav_inputs.append( + OmniTokensPrompt( + prompt_token_ids=codec_codes, + multi_modal_data=None, + mm_processor_kwargs=None, + additional_information=None, + ) + ) + return code2wav_inputs + + +def _extract_last_frame(pooling_output: OmniPayload) -> torch.Tensor | None: + audio_codes = pooling_output.get("codes", {}).get("audio") + if not isinstance(audio_codes, torch.Tensor) or audio_codes.numel() == 0: + return None + if audio_codes.ndim == 2: + frame = audio_codes[-1] + elif audio_codes.ndim == 1: + frame = audio_codes + else: + raise ValueError(f"unexpected audio_codes shape for higgs_audio_v2 async_chunk: {tuple(audio_codes.shape)}") + if frame.numel() == 0: + return None + frame = frame.to(torch.long).reshape(-1) + # Filter frames that still carry stream specials. + if int(frame.min().item()) < 0 or int(frame.max().item()) >= _NUM_REAL_CODES: + return None + return frame + + +def talker2code2wav_async_chunk( + transfer_manager: Any, + pooling_output: OmniPayload | None, + request: Any, + is_finished: bool = False, +) -> OmniPayloadStruct | None: + """Streaming adapter: buffer frames per request and flush a chunk at a time. + + Emits codes in codebook-major flat layout (``[Q * num_frames]``) plus a + ``MetaStruct(left_context_size, finished)`` so Stage 1 can stitch chunks + together with the conventional left-context overlap. + """ + request_id = request.external_req_id + finished = bool(is_finished or request.is_finished()) + + if isinstance(pooling_output, dict): + frame = _extract_last_frame(pooling_output) + if frame is not None: + transfer_manager.code_prompt_token_ids[request_id].append(frame.cpu().tolist()) + elif not finished: + return None + + connector = getattr(transfer_manager, "connector", None) + raw_cfg = getattr(connector, "config", {}) or {} + cfg = raw_cfg.get("extra", raw_cfg) if isinstance(raw_cfg, dict) else {} + chunk_size = int(cfg.get("codec_chunk_frames", _DEFAULT_CODEC_CHUNK_FRAMES)) + left_context_size_config = int(cfg.get("codec_left_context_frames", _DEFAULT_CODEC_LEFT_CONTEXT_FRAMES)) + + if chunk_size <= 0 or left_context_size_config < 0: + raise ValueError( + f"Invalid codec chunk config: codec_chunk_frames={chunk_size}, " + f"codec_left_context_frames={left_context_size_config}" + ) + + length = len(transfer_manager.code_prompt_token_ids[request_id]) + if length <= 0: + if finished: + return OmniPayloadStruct( + codes=CodesStruct(audio=torch.empty(0, dtype=torch.long)), + meta=MetaStruct(finished=torch.tensor(True, dtype=torch.bool)), + ) + return None + + chunk_remainder = length % chunk_size + if not finished and chunk_remainder != 0: + # Wait until we have a clean chunk boundary. + return None + + context_length = chunk_remainder if (finished and chunk_remainder > 0) else chunk_size + end_index = min(length, left_context_size_config + context_length) + left_context_size = max(0, end_index - context_length) + window_frames = transfer_manager.code_prompt_token_ids[request_id][-end_index:] + + if not window_frames: + # Defensive: should not happen because length > 0, but keeps the contract. + return OmniPayloadStruct( + codes=CodesStruct(audio=torch.empty(0, dtype=torch.long)), + meta=MetaStruct(finished=torch.tensor(finished, dtype=torch.bool)), + ) + + num_codebooks = len(window_frames[0]) + if num_codebooks != _NUM_CODEBOOKS: + raise ValueError(f"expected {_NUM_CODEBOOKS} codebooks per frame; got {num_codebooks}") + num_frames = len(window_frames) + flat = torch.tensor( + [window_frames[f][q] for q in range(num_codebooks) for f in range(num_frames)], + dtype=torch.long, + ) + + return OmniPayloadStruct( + codes=CodesStruct(audio=flat), + meta=MetaStruct( + left_context_size=left_context_size, + finished=torch.tensor(finished, dtype=torch.bool), + ), + ) From 52020c7af781850f6689c49fdf372251c2b66776 Mon Sep 17 00:00:00 2001 From: Canlin Guo Date: Tue, 26 May 2026 01:41:15 +0800 Subject: [PATCH 027/112] [UX] Rename default config to hunyuan_image_3_moe (#3835) Signed-off-by: gcanlin --- .../deploy/{hunyuan_image3.yaml => hunyuan_image_3_moe.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename vllm_omni/deploy/{hunyuan_image3.yaml => hunyuan_image_3_moe.yaml} (100%) diff --git a/vllm_omni/deploy/hunyuan_image3.yaml b/vllm_omni/deploy/hunyuan_image_3_moe.yaml similarity index 100% rename from vllm_omni/deploy/hunyuan_image3.yaml rename to vllm_omni/deploy/hunyuan_image_3_moe.yaml From 410a6a20b0fb695df99470fd4b8eec1ee9f13d0e Mon Sep 17 00:00:00 2001 From: Didan Deng <33117903+wtomin@users.noreply.github.com> Date: Tue, 26 May 2026 10:56:01 +0800 Subject: [PATCH 028/112] [Test] Qwen-Image Perf Test with High Concurrency (#2822) Signed-off-by: Didan Deng <33117903+wtomin@users.noreply.github.com> Signed-off-by: Gao Han Co-authored-by: Gao Han --- .../perf/scripts/run_diffusion_benchmark.py | 226 +++++++++++++++--- .../perf/tests/test_qwen_image_vllm_omni.json | 34 +++ 2 files changed, 227 insertions(+), 33 deletions(-) diff --git a/tests/dfx/perf/scripts/run_diffusion_benchmark.py b/tests/dfx/perf/scripts/run_diffusion_benchmark.py index 22e844427..d4ae73fa6 100644 --- a/tests/dfx/perf/scripts/run_diffusion_benchmark.py +++ b/tests/dfx/perf/scripts/run_diffusion_benchmark.py @@ -654,9 +654,47 @@ def run_benchmark( # --------------------------------------------------------------------------- -def assert_result(result: dict[str, Any], params: dict[str, Any], *, assert_baseline: bool) -> None: - """Assert request completion; optionally assert metrics against ``baseline`` in *params*.""" - num_prompts = params.get("num-prompts", 10) +def _resolve_baseline_value( + baseline_raw: Any, + *, + sweep_index: int | None, + max_concurrency: Any = None, + request_rate: Any = None, +) -> Any: + """Pick the baseline threshold for this sweep step.""" + if baseline_raw is None: + return 100000 + if isinstance(baseline_raw, dict): + if max_concurrency is not None: + for key in (max_concurrency, str(max_concurrency)): + if key in baseline_raw: + return baseline_raw[key] + if request_rate is not None: + for key in (request_rate, str(request_rate)): + if key in baseline_raw: + return baseline_raw[key] + raise KeyError( + f"baseline dict has no key for max_concurrency={max_concurrency!r} " + f"or request_rate={request_rate!r}; keys={list(baseline_raw.keys())!r}" + ) + if isinstance(baseline_raw, (list, tuple)): + if sweep_index is None: + raise ValueError("sweep_index is required when baseline is a list or tuple") + return baseline_raw[sweep_index] + return baseline_raw + + +def assert_result( + result: dict[str, Any], + params: dict[str, Any], + num_prompts: int, + *, + sweep_index: int | None = None, + max_concurrency: Any = None, + request_rate: Any = None, + assert_baseline: bool = True, +) -> None: + """Assert that benchmark metrics satisfy the configured baselines.""" completed = result.get("completed_requests", result.get("completed", 0)) assert completed == num_prompts, f"Expected {num_prompts} completed requests, got {completed}" @@ -666,9 +704,15 @@ def assert_result(result: dict[str, Any], params: dict[str, Any], *, assert_base print("Skipping performance assertions.") return - for metric, threshold in params.get("baseline", {}).items(): + for metric, baseline_raw in params.get("baseline", {}).items(): current = result.get(metric) assert current is not None, f"Metric '{metric}' not found in result: {list(result.keys())}" + threshold = _resolve_baseline_value( + baseline_raw, + sweep_index=sweep_index, + max_concurrency=max_concurrency, + request_rate=request_rate, + ) if "throughput" in metric: assert current >= threshold, f"{metric}: {current:.4f} < baseline {threshold}" else: @@ -692,6 +736,111 @@ def _resolve_benchmark_endpoint(server_cfg: dict[str, Any], params: dict[str, An return _default_benchmark_endpoint_for_task(cast(str, params.get("task", "t2i"))) +def _to_list(value: Any) -> list[Any]: + if value is None: + return [] + return [value] if not isinstance(value, (list, tuple)) else list(value) + + +def _build_run_params( + params: dict[str, Any], + *, + num_prompts: int, + sweep_index: int | None = None, + request_rate: Any | None = None, + max_concurrency: Any | None = None, +) -> dict[str, Any]: + run_params = { + key: value for key, value in params.items() if key not in {"request-rate", "max-concurrency", "num-prompts"} + } + run_params["num-prompts"] = num_prompts + if request_rate is not None: + run_params["request-rate"] = request_rate + if max_concurrency is not None: + run_params["max-concurrency"] = max_concurrency + if "baseline" in params: + run_params["baseline"] = { + metric: _resolve_baseline_value( + baseline_raw, + sweep_index=sweep_index, + max_concurrency=max_concurrency, + request_rate=request_rate, + ) + for metric, baseline_raw in params["baseline"].items() + } + return run_params + + +def _iter_sweep_runs(params: dict[str, Any]) -> list[dict[str, Any]]: + request_rate_list = _to_list(params.get("request-rate")) + num_prompt_list = _to_list(params.get("num-prompts", 10)) + max_concurrency_list = _to_list(params.get("max-concurrency")) + + max_len = max(len(request_rate_list), len(max_concurrency_list)) + if len(num_prompt_list) == 1 and max_len > 1: + num_prompt_list = num_prompt_list * max_len + elif max_len == 1 and len(num_prompt_list) > 1: + if len(request_rate_list) == 1: + request_rate_list = request_rate_list * len(num_prompt_list) + if len(max_concurrency_list) == 1: + max_concurrency_list = max_concurrency_list * len(num_prompt_list) + max_len = max(len(request_rate_list), len(max_concurrency_list)) + elif len(num_prompt_list) != max_len and max_len > 0: + raise ValueError("The number of prompts does not match the request-rate or max-concurrency") + + sweep_runs: list[dict[str, Any]] = [] + + for i, (request_rate, num_prompts) in enumerate(zip(request_rate_list, num_prompt_list)): + sweep_runs.append( + { + "params": _build_run_params( + params, + request_rate=request_rate, + num_prompts=num_prompts, + sweep_index=i, + ), + "num_prompts": num_prompts, + "sweep_index": i, + "request_rate": request_rate, + "max_concurrency": None, + } + ) + + for i, (max_concurrency, num_prompts) in enumerate(zip(max_concurrency_list, num_prompt_list)): + sweep_runs.append( + { + "params": _build_run_params( + params, + max_concurrency=max_concurrency, + num_prompts=num_prompts, + sweep_index=i, + request_rate="inf", + ), + "num_prompts": num_prompts, + "sweep_index": i, + "request_rate": None, + "max_concurrency": max_concurrency, + } + ) + + if not sweep_runs: + default_num_prompts = num_prompt_list[0] + sweep_runs.append( + { + "params": _build_run_params( + params, + num_prompts=default_num_prompts, + ), + "num_prompts": default_num_prompts, + "sweep_index": None, + "request_rate": None, + "max_concurrency": None, + } + ) + + return sweep_runs + + # --------------------------------------------------------------------------- # Test entry point # --------------------------------------------------------------------------- @@ -715,36 +864,47 @@ def test_diffusion_performance_benchmark(diffusion_server, benchmark_params, req test_name = benchmark_params["test_name"] params = benchmark_params["params"] server_cfg = getattr(diffusion_server, "server_cfg", {}) - endpoint = _resolve_benchmark_endpoint(server_cfg, params) + sweep_runs = _iter_sweep_runs(params) - result = run_benchmark( - host=diffusion_server.host, - port=diffusion_server.port, - model=diffusion_server.model, - params=params, - test_name=test_name, - endpoint=endpoint, - server_cfg=server_cfg, - source_file=cast(str, CONFIG_FILE_PATH), - ) + for sweep_run in sweep_runs: + endpoint = _resolve_benchmark_endpoint(server_cfg, sweep_run["params"]) + result = run_benchmark( + host=diffusion_server.host, + port=diffusion_server.port, + model=diffusion_server.model, + params=sweep_run["params"], + test_name=test_name, + endpoint=endpoint, + server_cfg=server_cfg, + source_file=cast(str, CONFIG_FILE_PATH), + ) - print(f"\n{'=' * 60}") - print(f"Results for {test_name} (server={diffusion_server.server_type}, endpoint={endpoint}):") - for key in ( - "throughput_qps", - "latency_mean", - "latency_median", - "latency_p50", - "latency_p99", - "peak_memory_mb_max", - "peak_memory_mb_mean", - "peak_memory_mb_median", - ): - if key in result: - print(f" {key}: {result[key]:.4f}") + print(f"\n{'=' * 60}") + print(f"Results for {test_name} (server={diffusion_server.server_type}, endpoint={endpoint}):") + for key in ( + "throughput_qps", + "latency_mean", + "latency_median", + "latency_p50", + "latency_p99", + "peak_memory_mb_max", + "peak_memory_mb_mean", + "peak_memory_mb_median", + ): + if key in result: + print(f" {key}: {result[key]:.4f}") - print(f"\n Aggregated results: {AGGREGATED_RESULT_FILE}") - print("=" * 60) + print(f"\n Aggregated results: {AGGREGATED_RESULT_FILE}") + print("=" * 60) - assert_baseline = request.config.getoption("--assert-baseline", default=False) - assert_result(result, params, assert_baseline=assert_baseline) + assert_baseline = request.config.getoption("--assert-baseline", default=False) + + assert_result( + result, + params, + sweep_run["num_prompts"], + sweep_index=sweep_run["sweep_index"], + max_concurrency=sweep_run["max_concurrency"], + request_rate=sweep_run["request_rate"], + assert_baseline=assert_baseline, + ) diff --git a/tests/dfx/perf/tests/test_qwen_image_vllm_omni.json b/tests/dfx/perf/tests/test_qwen_image_vllm_omni.json index e05f1b945..a6eacf88e 100644 --- a/tests/dfx/perf/tests/test_qwen_image_vllm_omni.json +++ b/tests/dfx/perf/tests/test_qwen_image_vllm_omni.json @@ -181,5 +181,39 @@ } } ] + }, + { + "test_name": "test_qwen_image_single_device_step_execution_high_concurrency", + "description": "Single-device Qwen-Image step-level execution benchmark at 512x512, 20 steps, concurrency sweep", + "server_type": "vllm-omni", + "server_params": { + "model": "Qwen/Qwen-Image", + "serve_args": { + "enable-diffusion-pipeline-profiler": true, + "step-execution": true, + "max-num-seqs": 8 + } + }, + "benchmark_params": [ + { + "name": "512x512_steps20_high_concurrency", + "dataset": "random", + "task": "t2i", + "width": 512, + "height": 512, + "num-inference-steps": 20, + "num-prompts": [20, 40, 80, 160], + "max-concurrency": [1, 2, 4, 8], + "warmup-requests": 8, + "warmup-concurrency": 8, + "warmup-num-inference-steps": 20, + "enable-negative-prompt": true, + "baseline": { + "throughput_qps": [0.30, 0.50, 0.60, 0.70], + "latency_mean": [3.50, 4.00, 7.00, 12.00], + "peak_memory_mb_mean": [67000, 67000, 67000, 67000] + } + } + ] } ] From 11a951b9626ba5e947fe30e44dc83cf1d7c70687 Mon Sep 17 00:00:00 2001 From: NumberWan Date: Tue, 26 May 2026 11:01:10 +0800 Subject: [PATCH 029/112] [BugFix]: CUDA `device-side assert` failures on single-stage BAGEL i2i requests (#3680) Signed-off-by: NumberWan --- .../bagel/test_forward_cache_update_vae.py | 169 ++++++++++++++++++ .../models/bagel/bagel_transformer.py | 14 +- 2 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 tests/diffusion/models/bagel/test_forward_cache_update_vae.py diff --git a/tests/diffusion/models/bagel/test_forward_cache_update_vae.py b/tests/diffusion/models/bagel/test_forward_cache_update_vae.py new file mode 100644 index 000000000..e49b408f4 --- /dev/null +++ b/tests/diffusion/models/bagel/test_forward_cache_update_vae.py @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""Unit tests for Bagel.forward_cache_update_vae packed-sequence assembly.""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock + +import numpy as np +import pytest +import torch +from PIL import Image +from transformers import PretrainedConfig +from vllm.transformers_utils.configs.bagel import BagelConfig + +from vllm_omni.diffusion.models.bagel.bagel_transformer import ( + Bagel, + BaseNavitOutputWithPast, + NaiveCache, +) + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + +HIDDEN_SIZE = 32 +NUM_LAYERS = 2 + + +def _make_bagel_config() -> BagelConfig: + llm_config = PretrainedConfig() + llm_config.hidden_size = HIDDEN_SIZE + llm_config.layer_module = "Qwen2MoTDecoderLayer" + llm_config.num_hidden_layers = NUM_LAYERS + llm_config.num_attention_heads = 4 + + vae_config = PretrainedConfig() + vae_config.downsample = 8 + vae_config.z_channels = 4 + + return BagelConfig( + llm_config=llm_config, + vae_config=vae_config, + visual_gen=True, + visual_und=False, + latent_patch_size=2, + max_latent_size=32, + ) + + +def _make_tracking_language_model() -> MagicMock: + """Return a mock LM that records forward() kwargs.""" + calls: list[dict] = [] + hidden_size = HIDDEN_SIZE + + def forward(*_args, **kwargs): + calls.append(dict(kwargs)) + if kwargs.get("return_embeddings_only"): + text_ids = kwargs["packed_text_ids"] + emb = torch.randn(text_ids.shape[0], hidden_size) + return BaseNavitOutputWithPast(packed_query_sequence=emb) + + packed_query_sequence = kwargs.get("packed_query_sequence") + if packed_query_sequence is None: + text_ids = kwargs["packed_text_ids"] + packed_query_sequence = torch.randn(text_ids.shape[0], hidden_size) + return BaseNavitOutputWithPast( + packed_query_sequence=packed_query_sequence, + past_key_values=kwargs.get("past_key_values"), + ) + + lm = MagicMock() + lm.forward = forward + lm._calls = calls + return lm + + +def _vae_transform(image: Image.Image) -> torch.Tensor: + arr = torch.from_numpy(np.array(image.convert("RGB"), dtype=np.float32)) + arr = arr / 127.5 - 1.0 + return arr.permute(2, 0, 1) + + +@pytest.fixture +def bagel_and_vae_input(): + language_model = _make_tracking_language_model() + bagel = Bagel(language_model=language_model, vit_model=None, config=_make_bagel_config()) + + image = Image.new("RGB", (64, 64), color=(128, 64, 32)) + new_token_ids = {"start_of_image": 100, "end_of_image": 101} + + gen_input, _newlens, _new_rope = bagel.prepare_vae_images( + curr_kvlens=[0], + curr_rope=[0], + images=[image], + transforms=_vae_transform, + new_token_ids=new_token_ids, + ) + + vae_model = MagicMock() + patch_shapes = gen_input["patchified_vae_latent_shapes"] + p = bagel.latent_patch_size + c = bagel.latent_channel + + def encode(padded_images: torch.Tensor) -> torch.Tensor: + # Match AutoEncoder.encode layout: [B, z_channels, h*p, w*p] per image. + batch = padded_images.shape[0] + assert batch == len(patch_shapes) + latents = [torch.randn(c, h * p, w * p) for h, w in patch_shapes] + return torch.stack(latents, dim=0) + + vae_model.encode = encode + + expected_seq_len = int(gen_input["packed_seqlens"].sum().item()) + num_text_tokens = int(gen_input["packed_text_ids"].numel()) + + return SimpleNamespace( + bagel=bagel, + language_model=language_model, + vae_model=vae_model, + gen_input=gen_input, + past_key_values=NaiveCache(NUM_LAYERS), + expected_seq_len=expected_seq_len, + num_text_tokens=num_text_tokens, + ) + + +class TestForwardCacheUpdateVae: + """Regression tests for single-stage img2img VAE cache prefill.""" + + def test_forwards_full_packed_query_sequence(self, bagel_and_vae_input): + """Update forward must use packed_query_sequence spanning text + VAE tokens.""" + ctx = bagel_and_vae_input + assert ctx.expected_seq_len > ctx.num_text_tokens, "test setup should include many VAE tokens" + + ctx.bagel.forward_cache_update_vae( + ctx.vae_model, + ctx.past_key_values, + **ctx.gen_input, + ) + + update_calls = [c for c in ctx.language_model._calls if not c.get("return_embeddings_only")] + assert len(update_calls) == 1 + + call = update_calls[0] + assert "packed_query_sequence" in call + assert "packed_text_ids" not in call + + packed = call["packed_query_sequence"] + assert packed.shape[0] == ctx.expected_seq_len + assert call.get("mode") == "gen" + + def test_packed_sequence_covers_text_and_vae_index_ranges(self, bagel_and_vae_input): + """Text and VAE index ranges must lie within packed_query_sequence.""" + ctx = bagel_and_vae_input + ctx.bagel.forward_cache_update_vae( + ctx.vae_model, + ctx.past_key_values, + **ctx.gen_input, + ) + + update_call = next(c for c in ctx.language_model._calls if not c.get("return_embeddings_only")) + packed = update_call["packed_query_sequence"] + + text_idx = ctx.gen_input["packed_text_indexes"] + vae_idx = ctx.gen_input["packed_vae_token_indexes"] + assert int(text_idx.max()) < packed.shape[0] + assert int(vae_idx.max()) < packed.shape[0] + assert int(text_idx.min()) >= 0 + assert int(vae_idx.min()) >= 0 diff --git a/vllm_omni/diffusion/models/bagel/bagel_transformer.py b/vllm_omni/diffusion/models/bagel/bagel_transformer.py index d33b26f60..ecaba8e08 100644 --- a/vllm_omni/diffusion/models/bagel/bagel_transformer.py +++ b/vllm_omni/diffusion/models/bagel/bagel_transformer.py @@ -1389,6 +1389,18 @@ class Bagel(nn.Module): packed_timestep_embeds = self.time_embedder(packed_timesteps) packed_latent = self.vae2llm(packed_latent) + packed_timestep_embeds + packed_pos_embed + # Mirror forward_cache_update_vit: build a full packed_sequence so MoE gen-mode + # indexes (packed_text_indexes / packed_vae_token_indexes) match tensor length. + packed_text_embedding = self.language_model.forward( + packed_text_ids=packed_text_ids, + return_embeddings_only=True, + ).packed_query_sequence + packed_sequence = packed_text_embedding.new_zeros((sum(packed_seqlens), self.hidden_size)) + packed_sequence[packed_text_indexes] = packed_text_embedding + if packed_latent.dtype != packed_sequence.dtype: + packed_latent = packed_latent.to(packed_sequence.dtype) + packed_sequence[packed_vae_token_indexes] = packed_latent + extra_inputs = {} if self.use_moe: extra_inputs = { @@ -1398,7 +1410,7 @@ class Bagel(nn.Module): } output = self.language_model.forward( - packed_text_ids=packed_text_ids, + packed_query_sequence=packed_sequence, query_lens=packed_seqlens, packed_query_position_ids=packed_position_ids, packed_query_indexes=packed_indexes, From cad25fa20c27299d58cc4b4bc46358887532670d Mon Sep 17 00:00:00 2001 From: Zheng Wengang Date: Tue, 26 May 2026 11:16:43 +0800 Subject: [PATCH 030/112] [CI] Add nightly-ci for multi-stage deployment (#3610) Signed-off-by: ZhengWG Co-authored-by: Hongsheng Liu --- .buildkite/test-nightly.yml | 47 +++++++ .../tests/test_qwen3_omni_multi_replicas.json | 115 ++++++++++++++++++ vllm_omni/config/stage_config.py | 4 +- .../deploy/qwen3_omni_moe_multi_replicas.yaml | 26 ++-- 4 files changed, 181 insertions(+), 11 deletions(-) create mode 100644 tests/dfx/perf/tests/test_qwen3_omni_multi_replicas.json diff --git a/.buildkite/test-nightly.yml b/.buildkite/test-nightly.yml index 1e25cf2e0..88cb8dbb5 100644 --- a/.buildkite/test-nightly.yml +++ b/.buildkite/test-nightly.yml @@ -305,6 +305,51 @@ steps: path: /mnt/hf-cache type: DirectoryOrCreate + - label: ":full_moon: Omni · Perf Test · Multi-Replica" + key: nightly-omni-performance-multi-replicas + timeout_in_minutes: 180 + commands: + - export BENCHMARK_DIR=tests/dfx/perf/results + - | + set +e + pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen3_omni_multi_replicas.json + EXIT=$$? + buildkite-agent artifact upload "tests/dfx/perf/results/*.json" + exit $$EXIT + agents: + queue: "mithril-h100-pool" + plugins: + - kubernetes: + podSpec: + containers: + - image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT + resources: + limits: + nvidia.com/gpu: 3 + volumeMounts: + - name: devshm + mountPath: /dev/shm + - name: hf-cache + mountPath: /root/.cache/huggingface + env: + - name: HF_HOME + value: /root/.cache/huggingface + - name: HF_TOKEN + valueFrom: + secretKeyRef: + name: hf-token-secret + key: token + nodeSelector: + node.kubernetes.io/instance-type: gpu-h100-sxm + volumes: + - name: devshm + emptyDir: + medium: Memory + - name: hf-cache + hostPath: + path: /mnt/hf-cache + type: DirectoryOrCreate + - label: ":full_moon: Omni · Multi-Replica Startup Test with 4x H100" timeout_in_minutes: 45 @@ -1188,6 +1233,7 @@ steps: - nightly-omni-performance-no-async-chunk - nightly-omni-performance-async-chunk - nightly-omni-performance-vllm-text + - nightly-omni-performance-multi-replicas - nightly-tts-performance - nightly-diffusion-x2iat-performance-qwen-image - nightly-diffusion-x2iat-performance-qwen-image-edit @@ -1204,6 +1250,7 @@ steps: - buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-omni-performance-no-async-chunk - buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-omni-performance-async-chunk - buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-omni-performance-vllm-text + - buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-omni-performance-multi-replicas - buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-qwen-image - buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-qwen-image-edit - buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-qwen-image-edit-2509 diff --git a/tests/dfx/perf/tests/test_qwen3_omni_multi_replicas.json b/tests/dfx/perf/tests/test_qwen3_omni_multi_replicas.json new file mode 100644 index 000000000..0e7cb63f7 --- /dev/null +++ b/tests/dfx/perf/tests/test_qwen3_omni_multi_replicas.json @@ -0,0 +1,115 @@ +[ + { + "test_name": "test_qwen3_omni_3gpu_replica2", + "server_params": { + "model": "Qwen/Qwen3-Omni-30B-A3B-Instruct", + "stage_overrides": { + "1": {"num_replicas": 2, "devices": "1,2"}, + "2": {"num_replicas": 2, "devices": "1,2"} + }, + "extra_cli_args": [ + "--async-chunk" + ] + }, + "benchmark_params": [ + { + "dataset_name": "random", + "backend": "openai-chat-omni", + "endpoint": "/v1/chat/completions", + "num_prompts": [128, 128, 128, 128], + "max_concurrency": [8, 16, 24, 32], + "random_input_len": 2500, + "random_output_len": 900, + "ignore_eos": true, + "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration", + "baseline": { + "mean_ttft_ms": [1000, 2000, 3000, 4000], + "mean_tpot_ms": [200, 200, 200, 200], + "mean_audio_ttfp_ms": [10000, 15000, 20000, 25000], + "mean_audio_rtf": [1.5, 1.5, 1.5, 1.5] + } + }, + { + "dataset_name": "random-mm", + "backend": "openai-chat-omni", + "endpoint": "/v1/chat/completions", + "num_prompts": [10], + "request_rate": [0.1], + "random_input_len": 100, + "random_output_len": 100, + "random_range_ratio": 0.0, + "ignore_eos": true, + "random_mm_base_items_per_request": 1, + "random_mm_num_mm_items_range_ratio": 0.5, + "random_mm_limit_mm_per_prompt": { + "audio": 1 + }, + "random_mm_bucket_config": { + "(0, 60, 3)": 1.0 + }, + "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration", + "baseline": { + "mean_ttft_ms": [2000], + "mean_audio_ttfp_ms": [2000], + "mean_audio_rtf": [0.25] + } + }, + { + "dataset_name": "random-mm", + "backend": "openai-chat-omni", + "endpoint": "/v1/chat/completions", + "num_prompts": [40], + "request_rate": [0.5], + "random_input_len": 100, + "random_output_len": 100, + "random_range_ratio": 0.0, + "ignore_eos": true, + "random_mm_base_items_per_request": 2, + "random_mm_num_mm_items_range_ratio": 0.5, + "random_mm_limit_mm_per_prompt": { + "image": 1, + "video": 1 + }, + "random_mm_bucket_config": { + "(256, 256, 1)": 0.5, + "(720, 1280, 2)": 0.5 + }, + "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration", + "baseline": { + "mean_ttft_ms": [6000], + "mean_audio_ttfp_ms": [6000], + "mean_audio_rtf": [0.7] + } + }, + { + "dataset_name": "random-mm", + "backend": "openai-chat-omni", + "endpoint": "/v1/chat/completions", + "num_prompts": [100], + "request_rate": [1.0], + "random_input_len": 100, + "random_output_len": 100, + "random_range_ratio": 0.0, + "ignore_eos": true, + "random_mm_base_items_per_request": 3, + "random_mm_num_mm_items_range_ratio": 0.5, + "random_mm_limit_mm_per_prompt": { + "image": 1, + "video": 1, + "audio": 1 + }, + "random_mm_bucket_config": { + "(256, 256, 1)": 0.34, + "(720, 1280, 2)": 0.33, + "(0, 60, 3)": 0.33 + }, + "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration", + "baseline": { + "mean_ttft_ms": [12000], + "mean_audio_ttfp_ms": [12000], + "mean_audio_rtf": [1.0] + } + } + ] + } +] diff --git a/vllm_omni/config/stage_config.py b/vllm_omni/config/stage_config.py index c459ecabe..7c17cf7ce 100644 --- a/vllm_omni/config/stage_config.py +++ b/vllm_omni/config/stage_config.py @@ -942,7 +942,7 @@ class StageConfig: # CLI overrides take precedence over YAML defaults for key, value in self.runtime_overrides.items(): - if value is not None and key not in ("devices", "max_batch_size"): + if value is not None and key not in ("devices", "max_batch_size", "num_replicas"): engine_args[key] = value # Build runtime config from YAML defaults + CLI overrides @@ -950,6 +950,8 @@ class StageConfig: runtime.setdefault("process", True) if self.runtime_overrides.get("devices") is not None: runtime["devices"] = self.runtime_overrides["devices"] + if self.runtime_overrides.get("num_replicas") is not None: + runtime["num_replicas"] = self.runtime_overrides["num_replicas"] # Legacy compat: migrate runtime.max_batch_size → engine_args.max_num_seqs legacy_mbs = runtime.pop("max_batch_size", None) diff --git a/vllm_omni/deploy/qwen3_omni_moe_multi_replicas.yaml b/vllm_omni/deploy/qwen3_omni_moe_multi_replicas.yaml index 791e29a8d..900c9e4bb 100644 --- a/vllm_omni/deploy/qwen3_omni_moe_multi_replicas.yaml +++ b/vllm_omni/deploy/qwen3_omni_moe_multi_replicas.yaml @@ -23,7 +23,11 @@ connectors: stages: - stage_id: 0 + max_num_batched_tokens: 32768 + max_num_seqs: 64 gpu_memory_utilization: 0.9 + trust_remote_code: true + enable_prefix_caching: false devices: "0" default_sampling_params: temperature: 0.4 @@ -34,7 +38,11 @@ stages: repetition_penalty: 1.05 - stage_id: 1 + max_num_batched_tokens: 32768 + max_num_seqs: 64 gpu_memory_utilization: 0.6 + trust_remote_code: true + enable_prefix_caching: false devices: "1,2" num_replicas: 2 input_connectors: @@ -47,11 +55,13 @@ stages: repetition_penalty: 1.05 - stage_id: 2 - gpu_memory_utilization: 0.1 - max_num_seqs: 1 - enforce_eager: true - async_scheduling: false max_num_batched_tokens: 51200 + max_num_seqs: 64 + gpu_memory_utilization: 0.1 + enforce_eager: true + trust_remote_code: true + enable_prefix_caching: false + async_scheduling: false devices: "1,2" num_replicas: 2 input_connectors: @@ -70,20 +80,16 @@ platforms: - stage_id: 0 gpu_memory_utilization: 0.6 tensor_parallel_size: 2 + max_num_batched_tokens: 8192 devices: "0,1" - stage_id: 1 gpu_memory_utilization: 0.6 - enforce_eager: true + max_num_batched_tokens: 8192 devices: "2" - stage_id: 2 gpu_memory_utilization: 0.3 devices: "2" - rocm: - stages: - - stage_id: 0 - enforce_eager: true - xpu: stages: - stage_id: 0 From ec94e83a25fb6ff00a4a1e7a89939bf681883eed Mon Sep 17 00:00:00 2001 From: bjf-frz Date: Tue, 26 May 2026 16:23:28 +0800 Subject: [PATCH 031/112] [CI][Bugfix]Fix Wan2.2 I2V reference image upload (#3869) Signed-off-by: bjf-frz --- .../wan22_i2v/test_wan22_i2v_video_similarity.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py b/tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py index 17e4fc0f2..5300f1c6e 100644 --- a/tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py +++ b/tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py @@ -359,9 +359,18 @@ def _validate_image_source(source: str) -> None: def _build_online_image_reference(source: str) -> str: - if _is_remote_image_source(source) or source.startswith("data:image"): + if source.startswith("data:image"): return source + if _is_remote_image_source(source): + response = requests.get(source, timeout=60) + response.raise_for_status() + mime_type = response.headers.get("Content-Type", "").split(";", 1)[0].strip() + if not mime_type.startswith("image/"): + mime_type = guess_type(urlparse(source).path)[0] or "image/png" + encoded = b64encode(response.content).decode("ascii") + return f"data:{mime_type};base64,{encoded}" + image_path = Path(source) mime_type = guess_type(image_path.name)[0] or "image/png" encoded = b64encode(image_path.read_bytes()).decode("ascii") @@ -479,7 +488,10 @@ def _send_video_request_with_timeout( headers={"Accept": "application/json"}, timeout=60, ) - response.raise_for_status() + try: + response.raise_for_status() + except requests.HTTPError as exc: + raise requests.HTTPError(f"{exc}; response body: {response.text}", response=response) from exc job_data = response.json() video_id = job_data["id"] openai_client._wait_until_video_completed(video_id, timeout_seconds=timeout_seconds) From 36eb8afba5e066c599c25f041750ae90523a1442 Mon Sep 17 00:00:00 2001 From: dengyunyang <584797741@qq.com> Date: Tue, 26 May 2026 16:45:05 +0800 Subject: [PATCH 032/112] [HunyuanImage][End2End Performance CI] Add hunyuan end2end test (#3849) Signed-off-by: dengyunyang <584797741@qq.com> --- .../diffusion/diffusion_benchmark_serving.py | 140 +++++++++++++++++- .../perf/scripts/run_diffusion_benchmark.py | 71 ++++++++- .../perf/tests/test_hunyuan_image3_it2i.json | 131 ++++++++++++++++ 3 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 tests/dfx/perf/tests/test_hunyuan_image3_it2i.json diff --git a/benchmarks/diffusion/diffusion_benchmark_serving.py b/benchmarks/diffusion/diffusion_benchmark_serving.py index 7e8ea97e1..2f385b538 100644 --- a/benchmarks/diffusion/diffusion_benchmark_serving.py +++ b/benchmarks/diffusion/diffusion_benchmark_serving.py @@ -47,6 +47,13 @@ Usage: --endpoint /v1/images/edits --dataset random --task ti2i --num-prompts 10 \ --bot-task think + python benchmarks/diffusion/diffusion_benchmark_serving.py \ + --endpoint /v1/images/edits \ + --dataset custom \ + --dataset-path custom_requests.jsonl \ + --task ti2i \ + --bot-task think + i2i (chat-based models such as Qwen-Image-Edit): python3 benchmarks/diffusion/diffusion_benchmark_serving.py \ --endpoint /v1/chat/completions --dataset vbench --task i2i --num-prompts 10 @@ -566,6 +573,135 @@ class TraceDataset(BaseDataset): return [self[i] for i in range(len(self))] +class CustomDataset(BaseDataset): + """ + Custom dataset that loads requests from a JSONL file. + + Each line in the JSONL file should be a JSON object with the following fields: + - prompt (required): The text prompt for the request + - width (optional): Image/video width + - height (optional): Image height + - num_inference_steps (optional): Number of diffusion steps + - seed (optional): Random seed + - image_paths (optional): List of input image paths for i2i/i2v/ti2i tasks + - image_urls (optional): List of input image URLs (alternative to image_paths) + + Example JSONL for ti2i: + {"prompt": "Add sunset lighting", "width": 1024, "height": 1024, "image_urls": ["https://example.com/image.jpg"]} + """ + + def __init__(self, args, api_url: str, model: str): + super().__init__(args, api_url, model) + self.dataset_path = args.dataset_path + if not self.dataset_path: + raise ValueError("--dataset-path must be provided when using 'custom' dataset") + self.load_data() + + def load_data(self) -> None: + """Load data from JSONL file.""" + import pandas as pd + + if not self.dataset_path.endswith(".jsonl"): + raise ValueError("Custom dataset must be a JSONL file") + + try: + df = pd.read_json(path_or_buf=self.dataset_path, lines=True) + except Exception as e: + raise ValueError(f"Failed to load JSONL file {self.dataset_path}: {e}") + + if "prompt" not in df.columns: + raise ValueError("JSONL file must contain a 'prompt' column") + + self.data = df.to_dict("records") + print(f"Loaded {len(self.data)} requests from {self.dataset_path}") + + def _resolve_image_paths(self, item: dict) -> list[str] | None: + # Handle local file paths + if "image_paths" in item and item["image_paths"]: + paths = item["image_paths"] + if isinstance(paths, str): + paths = [paths] + + # Verify all paths exist + valid_paths = [] + for path in paths: + if os.path.exists(path): + valid_paths.append(path) + else: + raise ValueError(f"Image file not found: {path}") + + return valid_paths if valid_paths else None + + # Handle URLs - download to temp files + if "image_urls" in item and item["image_urls"]: + urls = item["image_urls"] + if isinstance(urls, str): + urls = [urls] + + downloaded_paths = [] + for url in urls: + try: + response = requests.get(url, timeout=30) + response.raise_for_status() + + # Create temp file with appropriate extension + suffix = os.path.splitext(url)[1] or ".png" + with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: + tmp.write(response.content) + downloaded_paths.append(tmp.name) + except Exception as e: + raise ValueError(f"Failed to download image from {url}: {e}") + + return downloaded_paths if downloaded_paths else None + + return None + + def __len__(self) -> int: + return len(self.data) + + def __getitem__(self, idx: int) -> RequestFuncInput: + item = self.data[idx] + + # Extract fields with fallback to args defaults + prompt = item["prompt"] + width = item.get("width", self.args.width) + height = item.get("height", self.args.height) + num_inference_steps = item.get("num_inference_steps", self.args.num_inference_steps) + seed = item.get("seed", self.args.seed) + reserved_keys = {"prompt", "width", "height", "num_inference_steps", "seed", "image_paths", "image_urls"} + extra_body = {k: v for k, v in item.items() if k not in reserved_keys} + + # Handle image paths/URLs + image_paths = self._resolve_image_paths(item) + + return RequestFuncInput( + prompt=prompt, + api_url=self.api_url, + model=self.model, + seed=seed, + image_paths=image_paths, + extra_body=extra_body, + width=width, + height=height, + num_inference_steps=num_inference_steps, + ) + + def get_requests(self) -> list[RequestFuncInput]: + """Get all requests, cycling through data if num_prompts > dataset size.""" + num_requests = getattr(self.args, "num_prompts", len(self.data)) + + if num_requests <= len(self.data): + return [self[i] for i in range(num_requests)] + + # Cycle through the dataset to reach num_prompts + requests = [] + for i in range(num_requests): + idx = i % len(self.data) + requests.append(self[idx]) + + return requests + + class RandomDataset(BaseDataset): def __init__(self, args, api_url: str, model: str, enable_negative_prompt: bool = False): super().__init__(args, api_url, model) @@ -950,6 +1086,8 @@ async def benchmark(args): dataset = TraceDataset(args, api_url, args.model) elif args.dataset == "random": dataset = RandomDataset(args, api_url, args.model, args.enable_negative_prompt) + elif args.dataset == "custom": + dataset = CustomDataset(args, api_url, args.model) else: raise ValueError(f"Unknown dataset: {args.dataset}") @@ -1097,7 +1235,7 @@ if __name__ == "__main__": "--dataset", type=str, default="vbench", - choices=["vbench", "trace", "random"], + choices=["vbench", "trace", "random", "custom"], help="Dataset to use.", ) parser.add_argument( diff --git a/tests/dfx/perf/scripts/run_diffusion_benchmark.py b/tests/dfx/perf/scripts/run_diffusion_benchmark.py index d4ae73fa6..92ce2740d 100644 --- a/tests/dfx/perf/scripts/run_diffusion_benchmark.py +++ b/tests/dfx/perf/scripts/run_diffusion_benchmark.py @@ -41,6 +41,72 @@ pytestmark = [pytest.mark.diffusion, pytest.mark.full_model] os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" os.environ.setdefault("DIFFUSION_ATTENTION_BACKEND", "FLASH_ATTN") + +# --------------------------------------------------------------------------- +# Inline field processing +# --------------------------------------------------------------------------- +def _process_inline_fields(obj: Any, parent_key: str = "") -> None: + """Recursively process '*-inline' fields into temp files.""" + + if isinstance(obj, list): + for item in obj: + _process_inline_fields(item, parent_key) + return + + if not isinstance(obj, dict): + return + + import atexit + + import yaml + + for key in list(obj.keys()): + value = obj[key] + + if not key.endswith("-inline"): + _process_inline_fields(value, key) + continue + + base_key = key[:-7] + full_key = f"{parent_key}.{key}" if parent_key else key + + try: + if not isinstance(value, dict): + raise ValueError("must be a dict") + + file_type = value.get("type") + content = value.get("content") + + if file_type not in {"yaml", "jsonl"}: + raise ValueError(f"invalid type: {file_type}") + + fd, path = tempfile.mkstemp( + suffix=f".{file_type}", + prefix=f"{base_key}_", + ) + + atexit.register(Path(path).unlink, missing_ok=True) + + with os.fdopen(fd, "w", encoding="utf-8") as f: + if file_type == "jsonl": + items = content if isinstance(content, list) else [content] + f.writelines(json.dumps(x, ensure_ascii=False) + "\n" for x in items) + else: + yaml.dump( + content, + f, + allow_unicode=True, + sort_keys=False, + indent=2, + ) + + obj[base_key] = path + del obj[key] + + except Exception as e: + print(f"Warning: failed processing '{full_key}': {e}") + + # --------------------------------------------------------------------------- # Paths # --------------------------------------------------------------------------- @@ -102,11 +168,14 @@ def _resolve_refs(configs: list[dict[str, Any]], config_dir: Path) -> list[dict[ def load_configs(config_path: str) -> list[dict[str, Any]]: + """Load benchmark configs from JSON file and process inline fields.""" try: abs_path = Path(config_path).resolve() with open(abs_path, encoding="utf-8") as f: configs = json.load(f) - return _resolve_refs(configs, abs_path.parent) + configs = _resolve_refs(configs, abs_path.parent) + _process_inline_fields(configs) + return configs except json.JSONDecodeError as e: raise ValueError(f"JSON parsing error: {str(e)}") except FileNotFoundError: diff --git a/tests/dfx/perf/tests/test_hunyuan_image3_it2i.json b/tests/dfx/perf/tests/test_hunyuan_image3_it2i.json new file mode 100644 index 000000000..661f6d263 --- /dev/null +++ b/tests/dfx/perf/tests/test_hunyuan_image3_it2i.json @@ -0,0 +1,131 @@ +[ + { + "test_name": "test_hunyuan_image3_ti2i_tp4_tp4_inline", + "description": "AR (TP=4) + DiT (TP=4) - using inline dataset and deploy-config", + "server_type": "vllm-omni", + "benchmark_endpoint": "/v1/images/edits", + "server_params": { + "model": "tencent/HunyuanImage-3.0-Instruct", + "serve_args": { + "deploy-config-inline": { + "type": "yaml", + "content": { + "pipeline": "hunyuan_image_3_moe", + "async_chunk": false, + "trust_remote_code": true, + "connectors": { + "rdma_connector": { + "name": "MooncakeTransferEngineConnector", + "extra": { + "host": "auto", + "zmq_port": 50051, + "protocol": "rdma", + "device_name": "", + "memory_pool_size": 4294967296, + "memory_pool_device": "cpu" + } + } + }, + "stages": [ + { + "stage_id": 0, + "is_comprehension": true, + "final_output": true, + "final_output_type": "text", + "max_num_seqs": 4, + "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, + "hf_overrides": { + "rope_parameters": { + "mrope_section": [0, 32, 32], + "rope_type": "default" + } + }, + "omni_kv_config": { + "need_send_cache": true + }, + "output_connectors": { + "to_stage_1": "rdma_connector" + }, + "default_sampling_params": { + "temperature": 0.0, + "top_p": 1, + "top_k": -1, + "max_tokens": 8192, + "detokenize": true, + "skip_special_tokens": false, + "stop_token_ids": [128025] + } + }, + { + "stage_id": 1, + "max_num_seqs": 1, + "enforce_eager": true, + "devices": "4,5,6,7", + "distributed_executor_backend": "mp", + "omni_kv_config": { + "need_recv_cache": true + }, + "parallel_config": { + "tensor_parallel_size": 4, + "enable_expert_parallel": true + }, + "input_connectors": { + "from_stage_0": "rdma_connector" + }, + "default_sampling_params": { + "num_inference_steps": 50, + "guidance_scale": 0 + } + } + ], + "edges": [ + { + "from": 0, + "to": 1, + "window_size": -1, + "max_inflight": 1 + } + ] + } + }, + "enable-diffusion-pipeline-profiler": true + } + }, + "benchmark_params": [ + { + "name": "inline_dataset_5_requests", + "dataset": "custom", + "dataset-path-inline": { + "type": "jsonl", + "content": [ + { + "prompt": "新年宠物海报,Q版圆润的可爱标题“新年快乐汪”,副标题“HAPPY NEW YEAR”。 鱼眼镜头,背景是房间门口,近景,上传的主体歪头笑,围着红色围巾,戴着红色毛线帽,高清,绒毛细节,面部特写。 宝丽莱相纸,超现实主义,写实主义,胶片摄影,打印颗粒感肌理。肌理,超写实,复古感。", + "image_urls": ["https://raw.githubusercontent.com/Tencent-Hunyuan/HunyuanImage-3.0/main/assets/demo_instruct_imgs/input_0_0.png"], + "num_inference_steps": 8, + "seed": 42, + "guidance_scale": 1.0 + } + ] + }, + "task": "ti2i", + "bot-task": "think_recaption", + "max-concurrency": 4, + "num-prompts": 16, + "skip-performance-assertion": true, + "baseline": { + "throughput_qps": 0.1, + "latency_p99": 44.46, + "latency_mean": 35.57, + "latency_std_dev": 0.5, + "peak_memory_mb_max": 70000, + "peak_memory_mb_mean": 70000 + } + } + ] + } +] From 385df0359e25d7197b4171e352a7ae100498e98f Mon Sep 17 00:00:00 2001 From: mglyn <39453182+mglyn@users.noreply.github.com> Date: Tue, 26 May 2026 16:45:38 +0800 Subject: [PATCH 033/112] [BugFix] Fix LTX-2.3 audio latent padding for sequence parallelism (#3854) Signed-off-by: mglyn <1203789601@qq.com> --- .../models/ltx2/test_ltx2_3_pipeline.py | 99 +++++++++++++++++++ .../diffusion/models/ltx2/pipeline_ltx2_3.py | 44 ++++++++- 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/tests/diffusion/models/ltx2/test_ltx2_3_pipeline.py b/tests/diffusion/models/ltx2/test_ltx2_3_pipeline.py index 665126df7..afe92698d 100644 --- a/tests/diffusion/models/ltx2/test_ltx2_3_pipeline.py +++ b/tests/diffusion/models/ltx2/test_ltx2_3_pipeline.py @@ -15,12 +15,29 @@ These tests verify: import json import os import tempfile +from types import SimpleNamespace import pytest +import torch pytestmark = [pytest.mark.core_model, pytest.mark.cpu] +def _make_ltx23_pipeline(sequence_parallel_size: int = 1): + from vllm_omni.diffusion.models.ltx2.pipeline_ltx2_3 import LTX23Pipeline + + pipeline = object.__new__(LTX23Pipeline) + torch.nn.Module.__init__(pipeline) + pipeline.audio_vae_temporal_compression_ratio = 4 + pipeline.audio_vae_mel_compression_ratio = 4 + pipeline.od_config = SimpleNamespace(parallel_config=SimpleNamespace(sequence_parallel_size=sequence_parallel_size)) + pipeline.audio_vae = SimpleNamespace( + latents_mean=torch.tensor(0.0), + latents_std=torch.tensor(1.0), + ) + return pipeline + + class TestPipelineIndependence: """Verify LTX23Pipeline is fully independent from LTX2Pipeline.""" @@ -228,3 +245,85 @@ class TestInitExports: for name in expected_classes: assert hasattr(ltx2, name), f"{name} not exported from ltx2 package" assert name in ltx2.__all__, f"{name} not in ltx2.__all__" + + +class TestAudioLatentSPPadding: + def test_prepare_audio_latents_pads_generated_dummy_length_for_sp(self): + pipeline = _make_ltx23_pipeline(sequence_parallel_size=2) + + latents, original_num_frames, padded_num_frames = pipeline.prepare_audio_latents( + batch_size=1, + num_channels_latents=8, + num_mel_bins=64, + audio_latent_length=1, + dtype=torch.float32, + device=torch.device("cpu"), + ) + + assert original_num_frames == 1 + assert padded_num_frames == 2 + assert latents.shape == (1, 2, 128) + + def test_prepare_audio_latents_pads_provided_packed_sequence_dim_for_sp(self): + pipeline = _make_ltx23_pipeline(sequence_parallel_size=4) + latents = torch.arange(40, dtype=torch.float32).view(1, 10, 4) + + padded, original_num_frames, padded_num_frames = pipeline.prepare_audio_latents( + batch_size=1, + num_channels_latents=2, + num_mel_bins=8, + audio_latent_length=10, + dtype=torch.float32, + device=torch.device("cpu"), + latents=latents, + ) + + assert original_num_frames == 10 + assert padded_num_frames == 12 + assert padded.shape == (1, 12, 4) + torch.testing.assert_close(padded[:, :10], latents) + torch.testing.assert_close(padded[:, 10:], torch.zeros(1, 2, 4)) + + def test_prepare_audio_latents_accepts_already_padded_4d_latents_for_sp(self): + pipeline = _make_ltx23_pipeline(sequence_parallel_size=4) + latents = torch.arange(96, dtype=torch.float32).view(1, 2, 12, 4) + + audio_latent_length = pipeline._resolve_audio_latent_length(10, latents) + padded, original_num_frames, padded_num_frames = pipeline.prepare_audio_latents( + batch_size=1, + num_channels_latents=2, + num_mel_bins=16, + audio_latent_length=audio_latent_length, + dtype=torch.float32, + device=torch.device("cpu"), + latents=latents, + ) + + assert audio_latent_length == 10 + assert original_num_frames == 10 + assert padded_num_frames == 12 + assert padded.shape == (1, 12, 8) + torch.testing.assert_close(padded, pipeline._pack_audio_latents(latents)) + + def test_resolve_audio_latent_length_preserves_legacy_4d_shape_inference(self): + pipeline = _make_ltx23_pipeline(sequence_parallel_size=4) + latents = torch.zeros(1, 2, 13, 4) + + audio_latent_length = pipeline._resolve_audio_latent_length(10, latents) + + assert audio_latent_length == 13 + + def test_prepare_audio_latents_rejects_incompatible_provided_length(self): + pipeline = _make_ltx23_pipeline(sequence_parallel_size=4) + latents = torch.zeros(1, 11, 4) + + with pytest.raises(ValueError, match="incompatible audio frame count"): + pipeline.prepare_audio_latents( + batch_size=1, + num_channels_latents=2, + num_mel_bins=8, + audio_latent_length=10, + dtype=torch.float32, + device=torch.device("cpu"), + latents=latents, + ) diff --git a/vllm_omni/diffusion/models/ltx2/pipeline_ltx2_3.py b/vllm_omni/diffusion/models/ltx2/pipeline_ltx2_3.py index ec3b495f5..f3bc7c928 100644 --- a/vllm_omni/diffusion/models/ltx2/pipeline_ltx2_3.py +++ b/vllm_omni/diffusion/models/ltx2/pipeline_ltx2_3.py @@ -445,6 +445,26 @@ class LTX23Pipeline(nn.Module, ProgressBarMixin): def _unpad_audio_latents(latents: torch.Tensor, num_frames: int) -> torch.Tensor: return latents[:, :num_frames] + @staticmethod + def _get_sp_padded_audio_latent_length(audio_latent_length: int, sp_size: int) -> int: + if sp_size > 1: + audio_latent_length += (sp_size - (audio_latent_length % sp_size)) % sp_size + return audio_latent_length + + def _resolve_audio_latent_length(self, audio_latent_length: int, audio_latents: torch.Tensor | None) -> int: + if audio_latents is None or audio_latents.ndim != 4: + return audio_latent_length + + provided_latent_length = audio_latents.shape[2] + sp_size = getattr(self.od_config.parallel_config, "sequence_parallel_size", 1) or 1 + padded_latent_length = self._get_sp_padded_audio_latent_length(audio_latent_length, int(sp_size)) + + # Keep requested duration semantics when callers pass 4D latents that + # are already padded for SP; other 4D lengths retain shape inference. + if provided_latent_length in {audio_latent_length, padded_latent_length}: + return audio_latent_length + return provided_latent_length + # ------------------------------------------------------------------ # Latent preparation # ------------------------------------------------------------------ @@ -497,9 +517,11 @@ class LTX23Pipeline(nn.Module, ProgressBarMixin): latents: torch.Tensor | None = None, ) -> tuple[torch.Tensor, int, int]: original_latent_length = audio_latent_length - padded_latent_length = original_latent_length latent_mel_bins = num_mel_bins // self.audio_vae_mel_compression_ratio + sp_size = getattr(self.od_config.parallel_config, "sequence_parallel_size", 1) or 1 + padded_latent_length = self._get_sp_padded_audio_latent_length(original_latent_length, int(sp_size)) + if latents is not None: if latents.ndim == 4: latents = self._pack_audio_latents(latents) @@ -508,6 +530,23 @@ class LTX23Pipeline(nn.Module, ProgressBarMixin): latents = self._normalize_audio_latents(latents, self.audio_vae.latents_mean, self.audio_vae.latents_std) noise = randn_tensor(latents.shape, generator=generator, device=latents.device, dtype=latents.dtype) latents = noise_scale * noise + (1 - noise_scale) * latents + + if latents.shape[1] not in {original_latent_length, padded_latent_length}: + raise ValueError( + "Provided `audio_latents` has incompatible audio frame count " + f"{latents.shape[1]}; expected {original_latent_length} or {padded_latent_length}." + ) + + if latents.shape[1] == original_latent_length and padded_latent_length > original_latent_length: + padding = torch.zeros( + latents.shape[0], + padded_latent_length - original_latent_length, + latents.shape[2], + dtype=latents.dtype, + device=latents.device, + ) + latents = torch.cat([latents, padding], dim=1) + return latents.to(device=device, dtype=dtype), original_latent_length, padded_latent_length shape = (batch_size, num_channels_latents, padded_latent_length, latent_mel_bins) @@ -783,8 +822,7 @@ class LTX23Pipeline(nn.Module, ProgressBarMixin): self.audio_sampling_rate / self.audio_hop_length / float(self.audio_vae_temporal_compression_ratio) ) audio_num_frames = round(duration_s * audio_latents_per_second) - if audio_latents is not None and audio_latents.ndim == 4: - _, _, audio_num_frames, _ = audio_latents.shape + audio_num_frames = self._resolve_audio_latent_length(audio_num_frames, audio_latents) num_mel_bins = self.audio_vae.config.mel_bins if self.audio_vae is not None else 64 latent_mel_bins = num_mel_bins // self.audio_vae_mel_compression_ratio From abc31731ab7f1cb2981fd312c1b8889430281933 Mon Sep 17 00:00:00 2001 From: Hongsheng Liu Date: Tue, 26 May 2026 21:00:06 +0800 Subject: [PATCH 034/112] Update CUDA Docker base image to vLLM v0.21.0 (#3859) Signed-off-by: hsliuustc0106 --- docker/Dockerfile.cuda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile.cuda b/docker/Dockerfile.cuda index 78f64f6a5..6c94689e6 100644 --- a/docker/Dockerfile.cuda +++ b/docker/Dockerfile.cuda @@ -1,4 +1,4 @@ -ARG BASE_IMAGE=vllm/vllm-openai:v0.20.0 +ARG BASE_IMAGE=vllm/vllm-openai:v0.21.0 FROM ${BASE_IMAGE} ARG COMMON_WORKDIR=/app From 9322cc6176410009add63358eaa35b4c953e0691 Mon Sep 17 00:00:00 2001 From: WeiQing Chen <40507679+david6666666@users.noreply.github.com> Date: Tue, 26 May 2026 22:18:03 +0800 Subject: [PATCH 035/112] [Docs] Strengthen diffusion perf optimization quality gate (#3851) Signed-off-by: david6666666 <530634352@qq.com> --- .claude/skills/diffusion-perf-opt/SKILL.md | 30 +++++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.claude/skills/diffusion-perf-opt/SKILL.md b/.claude/skills/diffusion-perf-opt/SKILL.md index 559134935..0d1b714fd 100644 --- a/.claude/skills/diffusion-perf-opt/SKILL.md +++ b/.claude/skills/diffusion-perf-opt/SKILL.md @@ -21,6 +21,9 @@ Before proposing changes, ask for the optimization scene if it is not already kn - Current enabled strategies: USP/SP, CFG parallel, HSDP/FSDP, VAE patch parallel, torch.compile, profiler options. - Optimization target: latency, throughput, memory, cost, or quality-preserving speed. - Precision/quality tolerance: bf16/fp8/quantization/sparsity/approximate attention allowed or not. +- Quality validation method: exact-output comparison when feasible, or metrics + such as SSIM, PSNR, LPIPS, cosine similarity, MAE, MSE, temporal flicker + checks, baseline self-run variance, and reviewable output artifacts. ## Workflow @@ -79,7 +82,19 @@ Before proposing changes, ask for the optimization scene if it is not already kn - Record latency, stage timings, memory, output quality, and logs. - Report comparison tables in ms. Include at least end-to-end client time, server `inference_time`, server stage generation time if available, `vae.encode`, `diffuse`, `vae.decode`, and peak memory. -6. **Collect diagnostic trace only after narrowing hypotheses.** +6. **Enforce the quality and precision gate for every optimization.** + - Do not mark an optimization ready until quality is checked on the same + workload: model, prompt/media, seed, shape, steps, scheduler, dtype, + backend, parallelism, and output encoding. + - For math-preserving changes, require exact/near-exact agreement or show + that differences are within baseline self-run variance. + - For precision, quantization, approximate attention, sparse/custom kernels, + or backend changes, include reviewable artifacts and metrics such as SSIM, + PSNR, LPIPS, cosine similarity, MAE, MSE, and temporal flicker checks. + - If quality tolerance is not stated, default to quality-preserving behavior. + Failed or inconclusive quality validation blocks a ready-to-merge claim. + +7. **Collect diagnostic trace only after narrowing hypotheses.** - Use torch profiler for a small number of requests. - Run two separate diagnostic traces instead of mixing concerns: - **Operator/shape trace**: enable `torch_profiler_record_shapes=True` and keep stack collection disabled. Use this to rank CUDA kernels, NCCL collectives, attention/MLP/norm/RoPE work, and shape-specific hot operators. @@ -91,15 +106,16 @@ Before proposing changes, ask for the optimization scene if it is not already kn - Diagnostic reports must be written to disk and preserve: server command, profiler config, warmup/request/polling commands, trace artifact paths, rank analyzed, analyzer output or summary, and the decision about whether additional ranks are necessary. - Analyze both rank-level balance and device-level free bubbles when additional ranks are opened. -7. **Analyze host, communication, and operators.** +8. **Analyze host, communication, and operators.** - Find GPU idle/free intervals and map each large gap to the enclosing CPU/Python code. - Separate real GPU idle from profiler overhead such as CUPTI `Command Buffer Full`. - Compare NCCL kernel time to user annotations; annotations can overcount nested intervals. - Rank operator work by total CUDA time and by repeated small-kernel launch count. -8. **Produce an optimization plan.** +9. **Produce an optimization plan.** - Classify candidates as P0/P1/P2. - For each candidate, state necessity, expected benefit, implementation path, validation plan, and quality risk. + - Include a concrete quality/precision validation gate for each candidate. - Do not implement high-risk operator rewrites before proving the operator is a bottleneck for the target shapes. - End the plan with a user-facing candidate selection table. The assistant should not automatically choose a risky optimization just because it is @@ -124,7 +140,13 @@ Before proposing changes, ask for the optimization scene if it is not already kn - **P1:** meaningful code changes with contained risk. Examples: cross-attention KV caching, VAE gather/broadcast reduction, AdaLayerNorm/RMSNorm/RoPE fusion after trace evidence. - **P2:** high implementation or quality risk. Examples: FA to LA replacement, custom Triton/CUDA fused kernels, FP8/quantization, sparsity/Rainfusion-style acceleration. -Every implemented optimization needs A/B validation and quality regression. A/B means same workload and hardware before/after. Quality regression means checking generated image/video stability, artifacts, temporal flicker, and seed behavior when precision or approximate kernels change. +Every implemented optimization needs A/B validation and a passing +quality/precision gate, including math-preserving changes such as padding trim, +layout cleanup, cache reuse, or host/runtime cleanup. Use objective metrics +when available, keep reviewable artifacts, and compare against baseline +self-run variance for precision, quantization, approximate kernels, or backend +changes. If quality validation fails or is inconclusive, do not present the +optimization as ready to merge. ## Optimization Layers From e459fdb30add4f87b3e5132dc56bb5a8d66e4f88 Mon Sep 17 00:00:00 2001 From: Zeng Chuang Date: Tue, 26 May 2026 23:15:21 +0800 Subject: [PATCH 036/112] [bugfix] fix default deploy config in hunyuan_image offline example (#3879) Signed-off-by: zengchuang --- examples/offline_inference/hunyuan_image3/end2end.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/offline_inference/hunyuan_image3/end2end.py b/examples/offline_inference/hunyuan_image3/end2end.py index aed06bcb9..537379b89 100644 --- a/examples/offline_inference/hunyuan_image3/end2end.py +++ b/examples/offline_inference/hunyuan_image3/end2end.py @@ -17,7 +17,7 @@ from vllm_omni.entrypoints.omni import Omni from vllm_omni.inputs.data import OmniPromptType _REPO_ROOT = Path(__file__).resolve().parents[3] -_DEFAULT_DEPLOY_CONFIG = str(_REPO_ROOT / "vllm_omni" / "deploy" / "hunyuan_image3.yaml") +_DEFAULT_DEPLOY_CONFIG = str(_REPO_ROOT / "vllm_omni" / "deploy" / "hunyuan_image_3_moe.yaml") _DEFAULT_AR_DEPLOY_CONFIG = str(_REPO_ROOT / "vllm_omni" / "deploy" / "hunyuan_image3_ar.yaml") _MODALITY_TASK_MAP: dict[str, tuple[str, str | None]] = { From 1f1c82ecdd768828f697457837ab6d62b37df1f8 Mon Sep 17 00:00:00 2001 From: Dan <416947747@qq.com> Date: Wed, 27 May 2026 00:24:43 +0800 Subject: [PATCH 037/112] [BugFix] Fix Qwen3-TTS Code2Wav compatibility with transformers >= 5.9.0 (#3880) Signed-off-by: Dan250124 <416947747@qq.com> --- .../tokenizer_12hz/modeling_qwen3_tts_tokenizer_v2.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/vllm_omni/model_executor/models/qwen3_tts/tokenizer_12hz/modeling_qwen3_tts_tokenizer_v2.py b/vllm_omni/model_executor/models/qwen3_tts/tokenizer_12hz/modeling_qwen3_tts_tokenizer_v2.py index 87acb6002..99b26ea1e 100644 --- a/vllm_omni/model_executor/models/qwen3_tts/tokenizer_12hz/modeling_qwen3_tts_tokenizer_v2.py +++ b/vllm_omni/model_executor/models/qwen3_tts/tokenizer_12hz/modeling_qwen3_tts_tokenizer_v2.py @@ -13,6 +13,7 @@ # limitations under the License. """PyTorch Qwen3TTSTokenizerV2 model.""" +import inspect import math from collections.abc import Callable from dataclasses import dataclass @@ -565,12 +566,17 @@ class Qwen3TTSTokenizerV2DecoderTransformerModel(Qwen3TTSTokenizerV2DecoderPreTr # Prepare mask arguments mask_kwargs = { "config": self.config, - "input_embeds": inputs_embeds, "attention_mask": attention_mask, - "cache_position": cache_position, "past_key_values": past_key_values, "position_ids": position_ids, } + # Handle API changes across transformers versions + sig = inspect.signature(create_causal_mask) + if "input_embeds" in sig.parameters: + mask_kwargs["input_embeds"] = inputs_embeds + mask_kwargs["cache_position"] = cache_position + else: + mask_kwargs["inputs_embeds"] = inputs_embeds # Create the masks causal_mask_mapping = { "full_attention": create_causal_mask(**mask_kwargs), From 283873411a0f806e94114f0ba08952cdc07a5838 Mon Sep 17 00:00:00 2001 From: lyj-jjj Date: Wed, 27 May 2026 08:18:47 +0800 Subject: [PATCH 038/112] glm-image: fix(npu)per-stage runtime env for HCCL ports + GLM-Image NPU stage config (#3235) Signed-off-by: lyj-jjj Co-authored-by: Cursor Co-authored-by: Hongsheng Liu --- .../test_async_omni_engine_stage_init.py | 19 +- tests/engine/test_single_stage_mode.py | 8 +- vllm_omni/config/stage_config.py | 43 +++- vllm_omni/deploy/glm_image.yaml | 37 ++++ vllm_omni/engine/async_omni_engine.py | 206 +++++++++--------- vllm_omni/engine/stage_init_utils.py | 61 +++++- 6 files changed, 254 insertions(+), 120 deletions(-) diff --git a/tests/engine/test_async_omni_engine_stage_init.py b/tests/engine/test_async_omni_engine_stage_init.py index b423aa407..71952e6cf 100644 --- a/tests/engine/test_async_omni_engine_stage_init.py +++ b/tests/engine/test_async_omni_engine_stage_init.py @@ -3,6 +3,7 @@ import os import threading import time import types +from contextlib import contextmanager import pytest @@ -189,10 +190,12 @@ def test_initialize_diffusion_replica_restores_device_visibility_after_local_ini old_env = os.environ.get(env_var) os.environ[env_var] = "0,1" - def _fake_setup_stage_devices(_stage_id, _runtime_cfg): + @contextmanager + def _fake_stage_runtime_setup(_stage_id, _runtime_cfg): current_omni_platform.set_device_control_env_var("1") + yield - monkeypatch.setattr(engine_mod, "setup_stage_devices", _fake_setup_stage_devices) + monkeypatch.setattr(engine_mod, "stage_runtime_setup", _fake_stage_runtime_setup) monkeypatch.setattr(engine_mod, "inject_kv_stage_info", lambda *_: None) monkeypatch.setattr(engine_mod, "initialize_diffusion_stage", lambda *_, **__: types.SimpleNamespace()) @@ -221,7 +224,11 @@ def test_initialize_diffusion_replica_passes_stage_init_timeout_and_inline_flag( captured: dict[str, object] = {} - monkeypatch.setattr(engine_mod, "setup_stage_devices", lambda *_: None) + @contextmanager + def _noop_stage_runtime_setup(*_): + yield + + monkeypatch.setattr(engine_mod, "stage_runtime_setup", _noop_stage_runtime_setup) monkeypatch.setattr(engine_mod, "inject_kv_stage_info", lambda *_: None) def _capture_initialize_diffusion_stage( @@ -527,7 +534,11 @@ def test_initialize_llm_replica_passes_stage_init_timeout_to_complete_stage_hand prev_device_env = os.environ.get(device_env_var) os.environ[device_env_var] = "0" - monkeypatch.setattr(engine_mod, "setup_stage_devices", lambda *_: None) + @contextmanager + def _noop_stage_runtime_setup(*_): + yield + + monkeypatch.setattr(engine_mod, "stage_runtime_setup", _noop_stage_runtime_setup) monkeypatch.setattr(engine_mod, "build_engine_args_dict", lambda *_, **__: {}) monkeypatch.setattr(engine_mod, "acquire_device_locks", lambda *_: []) monkeypatch.setattr(engine_mod, "spawn_stage_core", lambda **_: (fake_addresses, fake_proc, "ipc://handshake")) diff --git a/tests/engine/test_single_stage_mode.py b/tests/engine/test_single_stage_mode.py index 52187c341..04ddbe22d 100644 --- a/tests/engine/test_single_stage_mode.py +++ b/tests/engine/test_single_stage_mode.py @@ -4,7 +4,7 @@ from __future__ import annotations import os import threading -from contextlib import contextmanager +from contextlib import contextmanager, nullcontext from types import SimpleNamespace from typing import Any @@ -835,7 +835,7 @@ class TestSingleStageReplicaInitialization: prev_device_env = os.environ.get(device_env_var) os.environ[device_env_var] = "0" - mocker.patch.object(engine_mod, "setup_stage_devices") + mocker.patch.object(engine_mod, "stage_runtime_setup", return_value=nullcontext()) mocker.patch.object(engine_mod, "build_engine_args_dict", return_value={}) mocker.patch.object(engine_mod, "acquire_device_locks", return_value=[]) mocker.patch.object(engine_mod, "release_device_locks") @@ -909,7 +909,7 @@ class TestSingleStageReplicaInitialization: prev_device_env = os.environ.get(device_env_var) os.environ[device_env_var] = "0" - mocker.patch.object(engine_mod, "setup_stage_devices") + mocker.patch.object(engine_mod, "stage_runtime_setup", return_value=nullcontext()) mocker.patch.object(engine_mod, "inject_kv_stage_info") mocker.patch.object(engine_mod, "build_diffusion_config", return_value="diffusion-config") mock_register = mocker.patch.object( @@ -988,7 +988,7 @@ class TestSingleStageReplicaInitialization: prev_device_env = os.environ.get(device_env_var) os.environ[device_env_var] = "0" - mocker.patch.object(engine_mod, "setup_stage_devices") + mocker.patch.object(engine_mod, "stage_runtime_setup", return_value=nullcontext()) mocker.patch.object(engine_mod, "inject_kv_stage_info") mocker.patch.object(engine_mod, "build_diffusion_config", return_value="diffusion-config") mocker.patch.object( diff --git a/vllm_omni/config/stage_config.py b/vllm_omni/config/stage_config.py index 7c17cf7ce..17cb5cf20 100644 --- a/vllm_omni/config/stage_config.py +++ b/vllm_omni/config/stage_config.py @@ -10,7 +10,7 @@ import warnings from dataclasses import asdict, dataclass, field, fields from enum import Enum from pathlib import Path -from typing import Any +from typing import Any, NamedTuple from vllm.logger import init_logger from vllm.v1.core.sched.scheduler import Scheduler as VLLMScheduler @@ -408,6 +408,7 @@ class StageDeployConfig: stage_id: int devices: str | None = None num_replicas: int = 1 + env: dict[str, Any] | None = None # Inter-stage connector wiring and request defaults. output_connectors: dict[str, str] | None = None @@ -478,6 +479,7 @@ _STAGE_RESERVED_KEYS = frozenset( "stage_id", "devices", "num_replicas", + "env", "output_connectors", "input_connectors", "default_sampling_params", @@ -498,6 +500,7 @@ def _parse_stage_deploy(stage_data: dict[str, Any]) -> StageDeployConfig: runtime_cfg = dict(stage_data.get("runtime", {})) devices = runtime_cfg.get("devices", stage_data.get("devices")) num_replicas = runtime_cfg.get("num_replicas", stage_data.get("num_replicas", 1)) + env = runtime_cfg.get("env", stage_data.get("env")) if "engine_args" in stage_data: for k, v in stage_data["engine_args"].items(): @@ -512,6 +515,7 @@ def _parse_stage_deploy(stage_data: dict[str, Any]) -> StageDeployConfig: "stage_id": stage_data["stage_id"], "devices": devices, "num_replicas": int(num_replicas), + "env": env, } for name, f in _STAGE_DEPLOY_FIELDS.items(): if name in flat_args: @@ -651,8 +655,14 @@ def load_deploy_config(path: str | Path) -> DeployConfig: return DeployConfig(**kwargs) -def _extract_platform_overrides(ps: dict[str, Any]) -> tuple[dict[str, Any], str | None]: - """Return ``(overrides, devices)`` from a platform stage entry. +class PlatformOverrides(NamedTuple): + overrides: dict[str, Any] + devices: str | None + env: dict[str, Any] | None + + +def _extract_platform_overrides(ps: dict[str, Any]) -> PlatformOverrides: + """Return overrides, devices, and env from a platform stage entry. Handles both the nested layout (``engine_args:`` / ``runtime.devices``) and the flat layout. ``devices`` is ``None`` when no override is set. @@ -662,9 +672,9 @@ def _extract_platform_overrides(ps: dict[str, Any]) -> tuple[dict[str, Any], str runtime_cfg = ps.get("runtime", {}) if "num_replicas" in runtime_cfg: overrides["num_replicas"] = runtime_cfg["num_replicas"] - return overrides, runtime_cfg.get("devices") - overrides = {k: v for k, v in ps.items() if k not in ("stage_id", "devices")} - return overrides, ps.get("devices") + return PlatformOverrides(overrides, runtime_cfg.get("devices"), runtime_cfg.get("env")) + overrides = {k: v for k, v in ps.items() if k not in ("stage_id", "devices", "env")} + return PlatformOverrides(overrides, ps.get("devices"), ps.get("env")) def _apply_platform_overrides( @@ -689,10 +699,21 @@ def _apply_platform_overrides( base = base_by_id.get(ps["stage_id"]) if base is None: continue - overrides, devices = _extract_platform_overrides(ps) - if devices is not None: - base.devices = devices - for key, val in overrides.items(): + po = _extract_platform_overrides(ps) + if po.devices is not None: + base.devices = po.devices + if po.env is not None: + if isinstance(base.env, dict) and isinstance(po.env, dict): + base.env = {**base.env, **po.env} + else: + logger.warning( + "Stage %s env override replaces base env entirely (base type=%s, override type=%s)", + ps["stage_id"], + type(base.env).__name__, + type(po.env).__name__, + ) + base.env = po.env + for key, val in po.overrides.items(): if hasattr(base, key): # Deep-merge dict-valued fields listed in _DEEP_MERGE_KEYS so # platform overlays don't silently clobber sibling keys (e.g. @@ -880,6 +901,8 @@ def merge_pipeline_deploy( if ds.devices is not None: runtime["devices"] = ds.devices runtime["num_replicas"] = ds.num_replicas + if ds.env is not None: + runtime["env"] = ds.env runtime["requires_multimodal_data"] = ps.requires_multimodal_data result.append( diff --git a/vllm_omni/deploy/glm_image.yaml b/vllm_omni/deploy/glm_image.yaml index 099df1b15..470bf2cd4 100644 --- a/vllm_omni/deploy/glm_image.yaml +++ b/vllm_omni/deploy/glm_image.yaml @@ -46,3 +46,40 @@ stages: guidance_scale: 1.5 height: 1024 width: 1024 + +platforms: + npu: + stages: + - stage_id: 0 + devices: "0,1,2,3" + env: + # When multiple business processes share a single NPU simultaneously, + # port conflict issues may occur. You can refer to the link below to set different parameters for different processes, + # replacing the default parameter configuration. + # https://www.hiascend.com/document/detail/zh/canncommercial/850/maintenref/envvar/envref_07_0143.html + HCCL_IF_BASE_PORT: 23000 + HCCL_HOST_SOCKET_PORT_RANGE: "23000-23199" + HCCL_NPU_SOCKET_PORT_RANGE: "23200-23399" + async_scheduling: true + enforce_eager: false + distributed_executor_backend: "mp" + enable_prefix_caching: true + tensor_parallel_size: 4 + sequence_parallel_size: 1 + compilation_config: + cudagraph_capture_sizes: [1, 2] + cudagraph_mode: "FULL_DECODE_ONLY" + worker_cls: vllm_omni.platforms.npu.worker.npu_ar_worker.NPUARWorker + scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler + - stage_id: 1 + devices: "0,1,2,3" + env: + HCCL_IF_BASE_PORT: 24000 + HCCL_HOST_SOCKET_PORT_RANGE: "24000-24199" + HCCL_NPU_SOCKET_PORT_RANGE: "24200-24399" + distributed_executor_backend: "mp" + parallel_config: + tensor_parallel_size: 1 + sequence_parallel_size: 4 + ulysses_degree: 4 + ring_degree: 1 diff --git a/vllm_omni/engine/async_omni_engine.py b/vllm_omni/engine/async_omni_engine.py index 23ef9b855..2396f433a 100644 --- a/vllm_omni/engine/async_omni_engine.py +++ b/vllm_omni/engine/async_omni_engine.py @@ -100,7 +100,7 @@ from vllm_omni.engine.stage_init_utils import ( load_omni_transfer_config_for_model, prepare_engine_environment, release_device_locks, - setup_stage_devices, + stage_runtime_setup, terminate_alive_proc, ) from vllm_omni.engine.stage_pool import StagePool, StagePoolClient @@ -875,50 +875,53 @@ class AsyncOmniEngine: with llm_stage_launch_lock: previous_visible_devices = os.environ.get(device_control_env) try: - setup_stage_devices(plan.metadata.stage_id, plan.metadata.runtime_cfg) - vllm_config = plan.stage_vllm_config - executor_class = plan.executor_class - assert vllm_config is not None - assert executor_class is not None - engine_args_dict = build_engine_args_dict( - stage_cfg, - self.model, - stage_connector_spec=plan.stage_connector_spec, - cli_tokenizer=getattr(self, "tokenizer", None), - ) - lock_fds = acquire_device_locks( + with stage_runtime_setup( plan.metadata.stage_id, - engine_args_dict, - stage_init_timeout, - ) - if self.single_stage_mode and self._omni_master_server is not None: - coord_router_addr: str | None = ( - self._coordinator_runtime.router_address - if self._coordinator_runtime is not None - else None + plan.metadata.runtime_cfg, + ): + vllm_config = plan.stage_vllm_config + executor_class = plan.executor_class + assert vllm_config is not None + assert executor_class is not None + engine_args_dict = build_engine_args_dict( + stage_cfg, + self.model, + stage_connector_spec=plan.stage_connector_spec, + cli_tokenizer=getattr(self, "tokenizer", None), ) - engine_manager, coordinator, addresses = launch_stack.enter_context( - launch_omni_core_engines( + lock_fds = acquire_device_locks( + plan.metadata.stage_id, + engine_args_dict, + stage_init_timeout, + ) + if self.single_stage_mode and self._omni_master_server is not None: + coord_router_addr: str | None = ( + self._coordinator_runtime.router_address + if self._coordinator_runtime is not None + else None + ) + engine_manager, coordinator, addresses = launch_stack.enter_context( + launch_omni_core_engines( + vllm_config=vllm_config, + executor_class=executor_class, + log_stats=False, + omni_master_server=self._omni_master_server, + stage_id=plan.metadata.stage_id, + stage_config=stage_cfg, + replica_id=plan.replica_id, + omni_coordinator_address=coord_router_addr, + ) + ) + else: + addresses, proc, handshake_address = spawn_stage_core( vllm_config=vllm_config, executor_class=executor_class, log_stats=False, - omni_master_server=self._omni_master_server, - stage_id=plan.metadata.stage_id, - stage_config=stage_cfg, - replica_id=plan.replica_id, - omni_coordinator_address=coord_router_addr, ) + logger.info( + "[AsyncOmniEngine] Stage %s engine launch started", + plan.metadata.stage_id, ) - else: - addresses, proc, handshake_address = spawn_stage_core( - vllm_config=vllm_config, - executor_class=executor_class, - log_stats=False, - ) - logger.info( - "[AsyncOmniEngine] Stage %s engine launch started", - plan.metadata.stage_id, - ) finally: if previous_visible_devices is None: current_omni_platform.unset_device_control_env_var() @@ -1017,68 +1020,71 @@ class AsyncOmniEngine: with stage_launch_lock: previous_visible_devices = os.environ.get(device_control_env) try: - setup_stage_devices(plan.metadata.stage_id, plan.metadata.runtime_cfg) - omni_conn_cfg, omni_from, omni_to = plan.omni_kv_connector - if omni_conn_cfg: - inject_omni_kv_config(plan.stage_cfg, omni_conn_cfg, omni_from, omni_to) - inject_kv_stage_info(plan.stage_cfg, plan.metadata.stage_id, self.stage_configs) - if self.single_stage_mode: - assert self._omni_master_server is not None - od_config = build_diffusion_config(self.model, plan.stage_cfg, plan.metadata) - lock_fds = acquire_diffusion_device_locks( - plan.metadata.stage_id, - od_config, - stage_init_timeout, - ) - handshake_address, request_address, response_address = register_stage_with_omni_master( - omni_master_address=self._omni_master_server.address, - omni_master_port=self._omni_master_server.port, - omni_stage_id=plan.metadata.stage_id, - omni_stage_config=plan.stage_cfg, - return_addresses=True, - replica_id=plan.replica_id, - ) - logger.info( - "[AsyncOmniEngine] Stage %s diffusion registration completed", - plan.metadata.stage_id, - ) - coord_router_addr: str | None = ( - self._coordinator_runtime.router_address - if self._coordinator_runtime is not None - else None - ) - proc, _, _, _ = spawn_diffusion_proc( - self.model, - od_config, - handshake_address=handshake_address, - request_address=request_address, - response_address=response_address, - omni_coordinator_address=coord_router_addr, - omni_stage_id=plan.metadata.stage_id, - omni_replica_id=plan.replica_id, - ) - complete_diffusion_handshake(proc, handshake_address, stage_init_timeout) - logger.info( - "[AsyncOmniEngine] Stage %s diffusion startup completed", - plan.metadata.stage_id, - ) - client = StageDiffusionClient.from_addresses( - plan.metadata, - request_address=request_address, - response_address=response_address, - proc=proc, - batch_size=self.diffusion_batch_size, - ) - else: - client = initialize_diffusion_stage( - plan.metadata.stage_id, - self.model, - plan.stage_cfg, - plan.metadata, - stage_init_timeout=stage_init_timeout, - batch_size=self.diffusion_batch_size, - use_inline=self.num_stages == 1 and plan.num_replicas == 1, - ) + with stage_runtime_setup( + plan.metadata.stage_id, + plan.metadata.runtime_cfg, + ): + omni_conn_cfg, omni_from, omni_to = plan.omni_kv_connector + if omni_conn_cfg: + inject_omni_kv_config(plan.stage_cfg, omni_conn_cfg, omni_from, omni_to) + inject_kv_stage_info(plan.stage_cfg, plan.metadata.stage_id, self.stage_configs) + if self.single_stage_mode: + assert self._omni_master_server is not None + od_config = build_diffusion_config(self.model, plan.stage_cfg, plan.metadata) + lock_fds = acquire_diffusion_device_locks( + plan.metadata.stage_id, + od_config, + stage_init_timeout, + ) + handshake_address, request_address, response_address = register_stage_with_omni_master( + omni_master_address=self._omni_master_server.address, + omni_master_port=self._omni_master_server.port, + omni_stage_id=plan.metadata.stage_id, + omni_stage_config=plan.stage_cfg, + return_addresses=True, + replica_id=plan.replica_id, + ) + logger.info( + "[AsyncOmniEngine] Stage %s diffusion registration completed", + plan.metadata.stage_id, + ) + coord_router_addr: str | None = ( + self._coordinator_runtime.router_address + if self._coordinator_runtime is not None + else None + ) + proc, _, _, _ = spawn_diffusion_proc( + self.model, + od_config, + handshake_address=handshake_address, + request_address=request_address, + response_address=response_address, + omni_coordinator_address=coord_router_addr, + omni_stage_id=plan.metadata.stage_id, + omni_replica_id=plan.replica_id, + ) + complete_diffusion_handshake(proc, handshake_address, stage_init_timeout) + logger.info( + "[AsyncOmniEngine] Stage %s diffusion startup completed", + plan.metadata.stage_id, + ) + client = StageDiffusionClient.from_addresses( + plan.metadata, + request_address=request_address, + response_address=response_address, + proc=proc, + batch_size=self.diffusion_batch_size, + ) + else: + client = initialize_diffusion_stage( + plan.metadata.stage_id, + self.model, + plan.stage_cfg, + plan.metadata, + stage_init_timeout=stage_init_timeout, + batch_size=self.diffusion_batch_size, + use_inline=self.num_stages == 1 and plan.num_replicas == 1, + ) finally: if previous_visible_devices is None: current_omni_platform.unset_device_control_env_var() diff --git a/vllm_omni/engine/stage_init_utils.py b/vllm_omni/engine/stage_init_utils.py index 1a9f98fe5..bd727b930 100644 --- a/vllm_omni/engine/stage_init_utils.py +++ b/vllm_omni/engine/stage_init_utils.py @@ -13,9 +13,10 @@ import importlib import multiprocessing as mp import os import time -from collections.abc import Callable, Sequence +from collections.abc import Callable, Generator, Sequence +from contextlib import contextmanager from dataclasses import dataclass, replace -from typing import Any, Literal +from typing import Any, Literal, cast from vllm.logger import init_logger from vllm.sampling_params import SamplingParams @@ -635,6 +636,62 @@ def setup_stage_devices(stage_id: int, runtime_cfg: Any) -> None: ) +@contextmanager +def stage_runtime_setup(stage_id: int, runtime_cfg: Any) -> Generator[None, None, None]: + """Apply per-stage ``runtime.env`` and ``runtime.devices`` for the context. + + Restores ``runtime.env`` on exit. Device visibility restore remains the + caller's responsibility (e.g. ``AsyncOmniEngine`` saves/restores the + platform device-control env var around this block). + """ + with stage_runtime_env(stage_id, runtime_cfg): + setup_stage_devices(stage_id, runtime_cfg) + yield + + +@contextmanager +def stage_runtime_env(stage_id: int, runtime_cfg: Any) -> Generator[None, None, None]: + """Apply per-stage ``runtime.env`` for the duration of the context.""" + if runtime_cfg is None: + runtime_cfg = {} + elif not isinstance(runtime_cfg, dict): + runtime_cfg = cast(dict[str, Any], _to_dict(runtime_cfg)) + + raw_env = runtime_cfg.get("env") + if raw_env is None: + yield + return + if isinstance(raw_env, dict): + runtime_env = cast(dict[str, Any], raw_env) + else: + runtime_env = cast(dict[str, Any], _to_dict(raw_env)) + if not runtime_env: + logger.warning( + "[stage_init] Stage-%s ignored runtime.env with unsupported type %s", + stage_id, + type(raw_env).__name__, + ) + yield + return + + previous_env: dict[str, str | None] = {} + for key, value in runtime_env.items(): + env_key = str(key) + previous_env[env_key] = os.environ.get(env_key) + os.environ[env_key] = str(value) + + if previous_env: + logger.info("[stage_init] Stage-%s applied runtime env keys: %s", stage_id, sorted(previous_env)) + try: + yield + finally: + for key, old_value in previous_env.items(): + if old_value is None: + os.environ.pop(key, None) + else: + os.environ[key] = old_value + + def build_engine_args_dict( stage_config: Any, model: str, From 3c48257f8d2aa001a6676fc6e39602a8e6c900be Mon Sep 17 00:00:00 2001 From: TaffyOfficial <2587297563@qq.com> Date: Wed, 27 May 2026 08:27:11 +0800 Subject: [PATCH 039/112] [Feat][HunyuanImage3] Stream AR text for IT2I image edits (#3723) Signed-off-by: TaffyOfficial <2324465096@qq.com> Co-authored-by: TaffyOfficial <2324465096@qq.com> --- docs/serving/image_edit_api.md | 73 +++++- .../openai_api/test_image_server.py | 237 ++++++++++++++++++ vllm_omni/entrypoints/openai/api_server.py | 63 ++--- .../entrypoints/openai/image_api_utils.py | 44 ++++ .../entrypoints/openai/protocol/__init__.py | 8 + .../entrypoints/openai/protocol/images.py | 37 ++- vllm_omni/entrypoints/openai/serving_chat.py | 156 +++++++++++- 7 files changed, 556 insertions(+), 62 deletions(-) diff --git a/docs/serving/image_edit_api.md b/docs/serving/image_edit_api.md index 79303e1a6..4fc13a62b 100644 --- a/docs/serving/image_edit_api.md +++ b/docs/serving/image_edit_api.md @@ -13,7 +13,7 @@ For example... ```bash # Qwen-Image vllm serve Qwen/Qwen-Image-Edit-2511 --omni --port 8000 - +``` ### Generate Images @@ -92,7 +92,8 @@ Content-Type: multipart/form-data | `user` | string | null | User identifier for tracking | | `output_format` | string | "png" | The format in which the generated images are returned. Must be one of "png", "jpg", "jpeg", "webp". | | `output_compression` | integer | 100 | The compression level (0-100%) for the generated images. | -| `background` | string or null | "auto" | Allows to set transparency for the background of the generated image(s). +| `background` | string or null | "auto" | Allows to set transparency for the background of the generated image(s). | +| `stream` | boolean | false | Return Server-Sent Events instead of a single JSON response. Currently supported only by multi-stage image edit pipelines that produce AR text before the final image, such as HunyuanImage3 IT2I AR+DiT. | #### vllm-omni Extension Parameters @@ -109,6 +110,8 @@ Content-Type: multipart/form-data ### Response Format +When `stream=false` or omitted, the endpoint returns the standard image edit response: + ```json { "created": 1701234567, @@ -120,10 +123,56 @@ Content-Type: multipart/form-data } ], "output_format": null, - "size": null, + "size": null } ``` +### Streaming Response Format + +Set `stream=true` when you want to receive the HunyuanImage3 IT2I AR +recaption text before the final edited image is ready. Streaming is only +available for multi-stage image edit pipelines where stage 0 produces AR text +and a later diffusion stage produces the image. Single-stage diffusion image +edit pipelines reject `stream=true` with HTTP 400. + +The response uses Server-Sent Events with `Content-Type: text/event-stream`. +Each event is sent as one `data:` line. The stream order is: + +1. One or more AR text delta chunks. +2. One final image chunk. +3. `data: [DONE]`. + +AR text chunks expose only the generated text delta and the AR completion +`index`. They do not expose token ids, ratio tokens, KV cache data, or internal +prompt token ids. + +Example AR delta event: + +```text +data: {"object":"image.edit.chunk","type":"ar_delta","delta":"A close-up product photo...","index":0,"created":1701234567,"model":"tencent/HunyuanImage-3.0-Instruct"} +``` + +Example final image event: + +```text +data: {"object":"image.edit.chunk","type":"image","data":[{"b64_json":"","url":null,"revised_prompt":null}],"output_format":"png","size":"1024x1024","created":1701234567,"model":"tencent/HunyuanImage-3.0-Instruct"} +``` + +Terminal event: + +```text +data: [DONE] +``` + +If the engine fails after the SSE response has started, the stream emits an +error chunk followed by `[DONE]`: + +```text +data: {"object":"error","created":1701234567,"model":"tencent/HunyuanImage-3.0-Instruct","error":{"message":"","type":"server_error","code":500}} + +data: [DONE] +``` + ## Examples ### Multiple Images input @@ -134,12 +183,28 @@ curl -s -D >(grep -i x-request-id >&2) \ -X POST "http://localhost:8000/v1/images/edits" \ -F "model=xxx" \ -F "image=@xx.png" \ - -F "image=@xx.png" + -F "image=@xx.png" \ -F "prompt='this bear is wearing sportwear. holding a basketball, and bending one leg.'" \ -F "size=1024x1024" \ -F "output_format=png" ``` +### Streaming HunyuanImage3 IT2I AR Text + +```bash +curl -N -X POST "http://localhost:8000/v1/images/edits" \ + -F "model=tencent/HunyuanImage-3.0-Instruct" \ + -F "image=@./input.png" \ + -F "prompt=Turn the product into a clean studio advertisement." \ + -F "size=1024x1024" \ + -F "output_format=png" \ + -F "stream=true" +``` + +Use the `ar_delta` events for progressive display of the AR-generated +recaption. Decode the `b64_json` field from the final `image` event to get the +edited image. + ## Parameter Handling diff --git a/tests/entrypoints/openai_api/test_image_server.py b/tests/entrypoints/openai_api/test_image_server.py index 40adb7a91..2556ac71d 100644 --- a/tests/entrypoints/openai_api/test_image_server.py +++ b/tests/entrypoints/openai_api/test_image_server.py @@ -9,6 +9,7 @@ OpenAI-compatible async text-to-image generation API endpoints in api_server.py. import base64 import io +import json from argparse import Namespace from types import SimpleNamespace @@ -19,6 +20,7 @@ from PIL import Image from pytest_mock import MockerFixture from vllm import SamplingParams from vllm.entrypoints.openai.models.protocol import BaseModelPath +from vllm.sampling_params import RequestOutputKind from vllm_omni.entrypoints.async_omni import AsyncOmni from vllm_omni.entrypoints.openai.api_server import _DiffusionServingModels, router @@ -120,6 +122,27 @@ class MockGenerationResult: self.peak_memory_mb = 0.0 +class MockStageResult: + """Mock multi-stage output for streaming image edit tests.""" + + def __init__(self, *, stage_id, final_output_type, text="", texts=None, images=None): + self.stage_id = stage_id + self.final_output_type = final_output_type + self.images = images or [] + if texts is not None: + outputs = [SimpleNamespace(text=item, index=index) for index, item in enumerate(texts)] + elif text: + outputs = [SimpleNamespace(text=text, index=0)] + else: + outputs = [] + self.request_output = SimpleNamespace( + outputs=outputs, + images=self.images, + ) + self.stage_durations = {} + self.peak_memory_mb = 0.0 + + class FakeAsyncOmni: """Fake AsyncOmni that yields a single diffusion output.""" @@ -391,6 +414,72 @@ def async_omni_stage_configs_only_client(): return TestClient(app) +@pytest.fixture +def streaming_image_edit_client(): + """Create a multi-stage client whose engine yields AR text before image output.""" + from fastapi import FastAPI + + from vllm_omni.entrypoints.async_omni import AsyncOmni + from vllm_omni.entrypoints.openai.api_server import router + from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat + + class FakeAsyncOmniClass(AsyncOmni): + def __init__(self): + stage_configs = [ + SimpleNamespace(stage_type="llm", is_comprehension=True), + SimpleNamespace(stage_type="diffusion", is_comprehension=False), + ] + default_sampling_params_list = [ + SamplingParams(temperature=0.1), + OmniDiffusionSamplingParams(), + ] + self.engine = SimpleNamespace( + stage_configs=stage_configs, + default_sampling_params_list=default_sampling_params_list, + ) + self.default_sampling_params_list = default_sampling_params_list + self.captured_sampling_params_list = None + 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): + 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 + yield MockStageResult(stage_id=0, final_output_type="text", text="recap") + yield MockStageResult(stage_id=0, final_output_type="text", text=" done") + yield MockStageResult( + stage_id=1, + final_output_type="image", + images=[Image.new("RGB", (32, 24), color="purple")], + ) + + def __class_getitem__(cls, item): + return cls + + def get_diffusion_od_config(self): + return self.od_config + + app = FastAPI() + app.include_router(router) + + engine = FakeAsyncOmniClass() + chat_handler = object.__new__(OmniOpenAIServingChat) + chat_handler.engine_client = engine + chat_handler._diffusion_engine = None + app.state.openai_serving_chat = chat_handler + app.state.engine_client = engine + app.state.stage_configs = [ + SimpleNamespace(stage_type="llm"), + SimpleNamespace(stage_type="diffusion"), + ] + app.state.args = Namespace( + default_sampling_params='{"1": {"num_inference_steps":4, "guidance_scale":7.5, "generator_device":"cpu"}}', + max_generated_image_size=1024 * 1792, + ) + return TestClient(app) + + def test_health_endpoint(test_client): """Test health check endpoint for diffusion mode""" response = test_client.get("/health") @@ -680,6 +769,154 @@ def test_image_edits_async_omni_stage_configs_only(async_omni_stage_configs_only assert len(captured) == 2 +def _parse_sse_payloads(body: str): + payloads = [] + for line in body.splitlines(): + if not line.startswith("data: "): + continue + data = line[len("data: ") :] + payloads.append(data if data == "[DONE]" else json.loads(data)) + return payloads + + +def test_image_edits_streaming_returns_ar_delta_then_image(streaming_image_edit_client): + img_bytes = make_test_image_bytes((16, 16)) + response = streaming_image_edit_client.post( + "/v1/images/edits", + files=[("image", img_bytes)], + data={ + "prompt": "edit me", + "size": "auto", + "stream": "true", + "output_format": "png", + }, + ) + + assert response.status_code == 200 + assert response.headers["content-type"].startswith("text/event-stream") + + payloads = _parse_sse_payloads(response.text) + assert [p["type"] if isinstance(p, dict) and "type" in p else p for p in payloads] == [ + "ar_delta", + "ar_delta", + "image", + "[DONE]", + ] + assert payloads[0]["delta"] == "recap" + assert payloads[0]["index"] == 0 + assert payloads[1]["delta"] == " done" + assert payloads[2]["output_format"] == "png" + assert payloads[2]["size"] == "16x16" + + image_payload = payloads[2]["data"][0] + img = Image.open(io.BytesIO(base64.b64decode(image_payload["b64_json"]))) + assert img.size == (32, 24) + + +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): + yield MockStageResult(stage_id=0, final_output_type="text", texts=["first", "second"]) + yield MockStageResult( + stage_id=1, + final_output_type="image", + images=[Image.new("RGB", (32, 24), color="purple")], + ) + + streaming_image_edit_client.app.state.engine_client.generate = generate_multi_output_delta + img_bytes = make_test_image_bytes((16, 16)) + response = streaming_image_edit_client.post( + "/v1/images/edits", + files=[("image", img_bytes)], + data={ + "prompt": "edit me", + "stream": "true", + }, + ) + + assert response.status_code == 200 + payloads = _parse_sse_payloads(response.text) + assert [(payloads[0]["index"], payloads[0]["delta"]), (payloads[1]["index"], payloads[1]["delta"])] == [ + (0, "first"), + (1, "second"), + ] + + +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): + yield MockStageResult(stage_id=0, final_output_type="text", text="recap") + + streaming_image_edit_client.app.state.engine_client.generate = generate_without_image + img_bytes = make_test_image_bytes((16, 16)) + response = streaming_image_edit_client.post( + "/v1/images/edits", + files=[("image", img_bytes)], + data={ + "prompt": "edit me", + "stream": "true", + }, + ) + + assert response.status_code == 200 + payloads = _parse_sse_payloads(response.text) + assert payloads[0]["type"] == "ar_delta" + assert payloads[1]["object"] == "error" + assert "without a final image" in payloads[1]["error"]["message"] + assert payloads[2] == "[DONE]" + + +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): + yield MockStageResult(stage_id=0, final_output_type="text", text="recap") + yield MockStageResult(stage_id=1, final_output_type="image", images=[]) + + streaming_image_edit_client.app.state.engine_client.generate = generate_empty_image + img_bytes = make_test_image_bytes((16, 16)) + response = streaming_image_edit_client.post( + "/v1/images/edits", + files=[("image", img_bytes)], + data={ + "prompt": "edit me", + "stream": "true", + }, + ) + + assert response.status_code == 200 + payloads = _parse_sse_payloads(response.text) + assert payloads[0]["type"] == "ar_delta" + assert payloads[1]["object"] == "error" + assert "empty final image" in payloads[1]["error"]["message"] + assert payloads[2] == "[DONE]" + + +def test_image_edits_streaming_rejects_single_stage(test_client): + img_bytes = make_test_image_bytes((16, 16)) + response = test_client.post( + "/v1/images/edits", + files=[("image", img_bytes)], + data={ + "prompt": "edit me", + "stream": "true", + }, + ) + + assert response.status_code == 400 + assert "multi-stage" in response.json()["detail"] + + +def test_image_edits_streaming_rejects_single_stage_before_loading_url(test_client): + response = test_client.post( + "/v1/images/edits", + data={ + "prompt": "edit me", + "url": "https://example.invalid/not-fetched.png", + "stream": "true", + }, + ) + + assert response.status_code == 400 + assert "multi-stage" in response.json()["detail"] + + def test_generate_images_max_size_rejected(async_omni_test_client): """Test that a size exceeding max_generated_image_size returns 400.""" response = async_omni_test_client.post( diff --git a/vllm_omni/entrypoints/openai/api_server.py b/vllm_omni/entrypoints/openai/api_server.py index 69a6dd603..d8d59285d 100644 --- a/vllm_omni/entrypoints/openai/api_server.py +++ b/vllm_omni/entrypoints/openai/api_server.py @@ -92,6 +92,7 @@ from vllm_omni.entrypoints.openai.errors import InvalidInputReferenceError from vllm_omni.entrypoints.openai.image_api_utils import ( SUPPORTED_LAYERED_RESOLUTIONS, encode_image_base64, + encode_image_base64_with_compression, parse_size, validate_layered_layers, ) @@ -1644,7 +1645,7 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request) # Encode images to base64 with the specified format image_data = [ - ImageData(b64_json=_encode_image_base64_with_compression(img, format=output_format), revised_prompt=None) + ImageData(b64_json=encode_image_base64_with_compression(img, format=output_format), revised_prompt=None) for img in images ] @@ -1694,6 +1695,7 @@ async def edit_images( output_format: str | None = Form("png"), background: str | None = Form("auto"), output_compression: Annotated[int, Form(ge=0, le=100)] = 100, + stream: bool = Form(False), user: str | None = Form(None), # unused now # vllm-omni extensions for image editing mask_image: str | UploadFile | None = None, @@ -1734,6 +1736,11 @@ async def edit_images( status_code=HTTPStatus.BAD_REQUEST.value, detail="Only response_format 'b64_json' is supported now.", ) + if stream and len(stage_configs) <= 1: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST.value, + detail="stream=true is only supported for multi-stage image editing pipelines.", + ) try: # 2. Build prompt & images params cot_output = None @@ -1941,7 +1948,17 @@ async def edit_images( extra_body=extra_body, reference_images=ref_b64_list, request_id=request_id, + stream=stream, + model=model_name, + output_format=output_format, + output_compression=output_compression, + size=size_str, ) + if stream and not isinstance(generation_result, ErrorResponse): + return StreamingResponse( + content=generation_result, + media_type="text/event-stream", + ) if isinstance(generation_result, ErrorResponse): raise HTTPException( status_code=generation_result.error.code if generation_result.error else 400, @@ -1964,7 +1981,7 @@ async def edit_images( # Encode images to base64 image_data = [ ImageData( - b64_json=_encode_image_base64_with_compression( + b64_json=encode_image_base64_with_compression( img, format=output_format, output_compression=output_compression ), revised_prompt=None, @@ -2289,48 +2306,6 @@ def _choose_output_format(output_format: str | None, background: str | None) -> return "jpeg" -def _prepare_image_for_output_format(image: Image.Image, format: str) -> Image.Image: - fmt = format.lower() - if fmt not in {"jpg", "jpeg"}: - return image - - if image.mode == "RGB": - return image - - if image.mode in {"RGBA", "LA"} or (image.mode == "P" and "transparency" in image.info): - alpha_image = image.convert("RGBA") - flattened = Image.new("RGB", alpha_image.size, (255, 255, 255)) - flattened.paste(alpha_image, mask=alpha_image.getchannel("A")) - return flattened - - return image.convert("RGB") - - -def _encode_image_base64_with_compression( - image: Image.Image, format: str = "png", output_compression: int = 100 -) -> str: - """Encode PIL Image to a base64 image string. - - Args: - image: PIL Image object - format: Output image format (e.g., "PNG", "JPEG", "WEBP") - output_compression: Compression level (0-100%), 100 for best quality - Returns: - Base64-encoded image as string - """ - buffer = io.BytesIO() - image = _prepare_image_for_output_format(image, format) - save_kwargs = {} - if format in ("jpg", "jpeg", "webp"): - save_kwargs["quality"] = output_compression - elif format == "png": - save_kwargs["compress_level"] = max(0, min(9, 9 - output_compression // 11)) # Map 0-100 to 9-0 - - image.save(buffer, format=format, **save_kwargs) - buffer.seek(0) - return base64.b64encode(buffer.read()).decode("utf-8") - - def apply_stage_default_sampling_params( default_params_json: str | None, sampling_params: Any, diff --git a/vllm_omni/entrypoints/openai/image_api_utils.py b/vllm_omni/entrypoints/openai/image_api_utils.py index 97847ad11..b958d786a 100644 --- a/vllm_omni/entrypoints/openai/image_api_utils.py +++ b/vllm_omni/entrypoints/openai/image_api_utils.py @@ -68,6 +68,50 @@ def encode_image_base64(image: PIL.Image.Image) -> str: return base64.b64encode(buffer.read()).decode("utf-8") +def prepare_image_for_output_format(image: PIL.Image.Image, format: str) -> PIL.Image.Image: + fmt = format.lower() + if fmt not in {"jpg", "jpeg"}: + return image + + if image.mode == "RGB": + return image + + if image.mode in {"RGBA", "LA"} or (image.mode == "P" and "transparency" in image.info): + alpha_image = image.convert("RGBA") + flattened = PIL.Image.new("RGB", alpha_image.size, (255, 255, 255)) + flattened.paste(alpha_image, mask=alpha_image.getchannel("A")) + return flattened + + return image.convert("RGB") + + +def encode_image_base64_with_compression( + image: PIL.Image.Image, format: str = "png", output_compression: int = 100 +) -> str: + """Encode a PIL image to a base64 image string. + + Args: + image: PIL Image object. + format: Output image format, such as "png", "jpeg", or "webp". + output_compression: Compression level (0-100), where 100 keeps the + best quality for lossy formats and uses the lowest PNG compression. + + Returns: + Base64-encoded image string. + """ + buffer = io.BytesIO() + image = prepare_image_for_output_format(image, format) + save_kwargs = {} + if format in ("jpg", "jpeg", "webp"): + save_kwargs["quality"] = output_compression + elif format == "png": + save_kwargs["compress_level"] = max(0, min(9, 9 - output_compression // 11)) + + image.save(buffer, format=format, **save_kwargs) + buffer.seek(0) + return base64.b64encode(buffer.read()).decode("utf-8") + + def validate_layered_layers(layers: int | None) -> int | None: """Validate the Qwen-Image-Layered ``layers`` parameter.""" if layers is None: diff --git a/vllm_omni/entrypoints/openai/protocol/__init__.py b/vllm_omni/entrypoints/openai/protocol/__init__.py index c73203cc4..c68f6f598 100644 --- a/vllm_omni/entrypoints/openai/protocol/__init__.py +++ b/vllm_omni/entrypoints/openai/protocol/__init__.py @@ -4,6 +4,10 @@ from vllm_omni.entrypoints.openai.protocol.chat_completion import OmniChatCompletionStreamResponse from vllm_omni.entrypoints.openai.protocol.images import ( ImageData, + ImageEditARDeltaChunk, + ImageEditImageChunk, + ImageEditStreamError, + ImageEditStreamResponse, ImageGenerationRequest, ImageGenerationResponse, ResponseFormat, @@ -16,6 +20,10 @@ from vllm_omni.entrypoints.openai.protocol.videos import ( __all__ = [ "ImageData", + "ImageEditARDeltaChunk", + "ImageEditImageChunk", + "ImageEditStreamError", + "ImageEditStreamResponse", "ImageGenerationRequest", "ImageGenerationResponse", "ResponseFormat", diff --git a/vllm_omni/entrypoints/openai/protocol/images.py b/vllm_omni/entrypoints/openai/protocol/images.py index dbf4c24b3..34fee760b 100644 --- a/vllm_omni/entrypoints/openai/protocol/images.py +++ b/vllm_omni/entrypoints/openai/protocol/images.py @@ -8,7 +8,7 @@ for text-to-image generation, with vllm-omni specific extensions. """ from enum import Enum -from typing import Any +from typing import Any, Literal from pydantic import BaseModel, Field, field_validator @@ -175,3 +175,38 @@ class ImageGenerationResponse(BaseModel): description="Chain-of-thought text output from the AR stage. " "Only present for image editing (IT2I) with CoT-enabled models.", ) + + +class ImageEditARDeltaChunk(BaseModel): + """Streaming chunk carrying a text delta from the image-edit AR stage.""" + + object: Literal["image.edit.chunk"] = "image.edit.chunk" + type: Literal["ar_delta"] = "ar_delta" + delta: str = Field(..., description="Text delta generated by the AR stage") + 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") + + +class ImageEditImageChunk(BaseModel): + """Streaming chunk carrying the final image-edit result.""" + + object: Literal["image.edit.chunk"] = "image.edit.chunk" + type: Literal["image"] = "image" + data: list[ImageData] = Field(..., description="Array of generated images") + output_format: str = Field(..., description="The output format of the image generation") + 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") + + +class ImageEditStreamError(BaseModel): + """Streaming error chunk emitted before the terminal [DONE] event.""" + + object: Literal["error"] = "error" + created: int = Field(..., description="Unix timestamp of when the stream was created") + model: str = Field(..., description="Model used for the image edit request") + error: dict[str, Any] = Field(..., description="OpenAI-compatible streaming error payload") + + +ImageEditStreamResponse = ImageEditARDeltaChunk | ImageEditImageChunk | ImageEditStreamError diff --git a/vllm_omni/entrypoints/openai/serving_chat.py b/vllm_omni/entrypoints/openai/serving_chat.py index d3bf29d89..ec10606a1 100644 --- a/vllm_omni/entrypoints/openai/serving_chat.py +++ b/vllm_omni/entrypoints/openai/serving_chat.py @@ -86,9 +86,15 @@ from vllm.utils.collection_utils import as_list from vllm.v1.engine.exceptions import EngineDeadError from vllm_omni.entrypoints.openai.audio_utils_mixin import AudioMixin -from vllm_omni.entrypoints.openai.image_api_utils import validate_layered_layers +from vllm_omni.entrypoints.openai.image_api_utils import encode_image_base64_with_compression, validate_layered_layers from vllm_omni.entrypoints.openai.protocol import OmniChatCompletionStreamResponse from vllm_omni.entrypoints.openai.protocol.audio import AudioResponse, CreateAudio +from vllm_omni.entrypoints.openai.protocol.images import ( + ImageData, + ImageEditARDeltaChunk, + ImageEditImageChunk, + ImageEditStreamError, +) from vllm_omni.entrypoints.openai.stage_params import ( build_stage_sampling_params_list, get_default_sampling_params_list, @@ -2414,17 +2420,13 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): return engine_prompt, sampling_params_list - async def generate_diffusion_images( + def _prepare_diffusion_image_request( self, *, prompt: str, extra_body: dict[str, Any] | None = None, reference_images: list[str] | None = None, - request_id: str | None = None, - ) -> tuple[list[Image.Image], dict[str, Any], float] | ErrorResponse: - """Generate diffusion images and return raw images plus generation stats.""" - if request_id is None: - request_id = f"chatcmpl-{uuid.uuid4().hex[:16]}" + ) -> tuple[Any, OmniTextPrompt, OmniDiffusionSamplingParams, list[Image.Image]] | ErrorResponse: if extra_body is None: extra_body = {} if reference_images is None: @@ -2499,9 +2501,44 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): status_code=400, ) + return engine, gen_prompt, gen_params, pil_images + + async def generate_diffusion_images( + self, + *, + prompt: str, + extra_body: dict[str, Any] | None = None, + reference_images: list[str] | None = None, + request_id: str | None = None, + stream: bool = False, + model: str | None = None, + output_format: str = "png", + output_compression: int = 100, + size: str = "auto", + ) -> tuple[list[Image.Image], dict[str, Any], float, str | None] | ErrorResponse | AsyncIterator[str]: + """Generate diffusion images and return raw images plus generation stats.""" + if request_id is None: + request_id = f"chatcmpl-{uuid.uuid4().hex[:16]}" + + prepared = self._prepare_diffusion_image_request( + prompt=prompt, + extra_body=extra_body, + reference_images=reference_images, + ) + if isinstance(prepared, ErrorResponse): + return prepared + engine, gen_prompt, gen_params, pil_images = prepared + if extra_body is None: + extra_body = {} + if isinstance(engine, AsyncOmni): diffusion_engine = cast(AsyncOmni, engine) stage_configs = getattr(diffusion_engine, "stage_configs", None) or [] + if stream and len(stage_configs) <= 1: + return self._create_error_response( + "stream=true is only supported for multi-stage image editing pipelines.", + status_code=400, + ) if len(stage_configs) > 1: # Pull tokenizer from the comprehension (AR) stage so we can # build HF byte-for-byte prompt_token_ids in the helper. If @@ -2526,15 +2563,31 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): engine_prompt = gen_prompt sampling_params_list = [gen_params] - result = None - async for output in diffusion_engine.generate( + sampling_params_list = coerce_param_message_types(sampling_params_list, stream) + result_generator = diffusion_engine.generate( prompt=engine_prompt, sampling_params_list=sampling_params_list, request_id=request_id, - ): + ) + if stream: + return self._stream_diffusion_image_chunks( + result_generator, + model=model or self._diffusion_model_name, + output_format=output_format, + output_compression=output_compression, + size=size, + ) + + result = None + async for output in result_generator: result = output if result is None: return self._create_error_response("No output generated from AsyncOmni", status_code=500) + elif stream: + return self._create_error_response( + "Streaming image edits require a multi-stage AsyncOmni engine.", + status_code=400, + ) else: result = await engine.generate( prompt=gen_prompt, @@ -2557,14 +2610,91 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): if isinstance(ar_text, str) and ar_text.strip(): cot_output = ar_text + return self._flatten_diffusion_images(images), stage_durations, peak_memory_mb, cot_output + + async def _stream_diffusion_image_chunks( + self, + result_generator: AsyncIterator[OmniRequestOutput], + *, + model: str, + output_format: str, + output_compression: int, + size: str, + ) -> AsyncIterator[str]: + """Yield image edit SSE chunks from multi-stage diffusion outputs.""" + created = int(time.time()) + emitted_image = False + try: + async for output in result_generator: + final_output_type = getattr(output, "final_output_type", None) + stage_id = getattr(output, "stage_id", None) + if final_output_type == "text" and stage_id == 0: + request_output = output.request_output + for completion in request_output.outputs: + text = completion.text or "" + if not text: + continue + chunk = ImageEditARDeltaChunk( + delta=text, + index=completion.index, + created=created, + model=model, + ) + yield f"data: {chunk.model_dump_json()}\n\n" + elif final_output_type == "image": + images = self._flatten_diffusion_images(getattr(output.request_output, "images", [])) + if not images: + raise RuntimeError("Streaming image edit produced an empty final image output.") + image_data = [ + ImageData( + b64_json=encode_image_base64_with_compression( + img, + format=output_format, + output_compression=output_compression, + ), + revised_prompt=None, + ) + for img in images + ] + chunk = ImageEditImageChunk( + data=image_data, + output_format=output_format, + size=size, + created=created, + model=model, + ) + yield f"data: {chunk.model_dump_json()}\n\n" + emitted_image = True + if not emitted_image: + raise RuntimeError("Streaming image edit completed without a final image output.") + except EngineDeadError as exc: + logger.error("EngineDeadError during streaming image edit: %s", exc) + data = self.create_streaming_error_response(exc) + yield f"data: {data}\n\n" + terminate_if_errored(engine=self.engine_client) + except Exception as exc: + logger.exception("Streaming image edit failed: %s", exc) + chunk = ImageEditStreamError( + created=created, + model=model, + error={ + "message": str(exc), + "type": "server_error", + "code": 500, + }, + ) + yield f"data: {chunk.model_dump_json()}\n\n" + yield "data: [DONE]\n\n" + + @staticmethod + def _flatten_diffusion_images(images: Any) -> list[Image.Image]: flat_images: list[Image.Image] = [] - for item in images: + for item in images or []: if isinstance(item, list): flat_images.extend(item) else: flat_images.append(item) - - return flat_images, stage_durations, peak_memory_mb, cot_output + return flat_images async def _create_diffusion_chat_completion( self, From a3802d1e5c9d504229df6572c4beec23b3815782 Mon Sep 17 00:00:00 2001 From: Dnoob Date: Wed, 27 May 2026 10:13:31 +0800 Subject: [PATCH 040/112] [Doc][Benchmark] Rewrite benchmarks/README.md as repo-wide index (#1939) Signed-off-by: root Signed-off-by: Dnoob --- benchmarks/README.md | 86 ++++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/benchmarks/README.md b/benchmarks/README.md index 68ffd40ef..37a6264a9 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -1,44 +1,60 @@ -# Benchmarks Overview and Architecture +# Benchmarks -This document explains the benchmark architecture across all benchmark assets in this repo. It describes what we measure, and where to find or plug in new scenarios. Per-task details remain in subfolder READMEs (e.g., `benchmarks//README.md`). +This directory contains benchmark suites for evaluating different model families and infrastructure components in vLLM-Omni. Each subfolder targets a different benchmark family with its own scripts, configs, and metrics. See the per-subfolder READMEs for detailed usage. -## Scope and goals -- Establish repeatable latency/throughput measurements for multimodal LLM pipelines. -- Provide both HF Transformers (offline) and vLLM-Omni (multi-stage/pipeline) baselines. -- Make it easy to plug in new datasets and models with minimal changes to the runner scripts. +## Benchmark families -## Dataset and inputs -- Default example: SeedTTS top-100 prompts (`benchmarks/build_dataset/top100.txt`) via `benchmarks/build_dataset/`. -- Extensible: drop in new prompt files or modality-aligned payloads; keep the expected format for the consuming scripts (e.g., one prompt per line). -- If you add a new dataset, document it under `benchmarks//README.md` and point scripts to your data path. +### [TTS](tts/README.md) — Text-to-Speech -## Directory layout -- `benchmarks/build_dataset/` — dataset prep utilities (e.g., SeedTTS top100). -- `benchmarks//vllm_omni/` — vLLM-Omni pipeline benchmarks, logs, outputs. -- `benchmarks/accuracy/` — accuracy benchmark integrations that adapt external - benchmark suites to vLLM-Omni serving and evaluation flows. -- Add new tasks under `benchmarks//...` with the same pattern: `transformers/`, `vllm_omni/`, task-specific README, and (optionally) dataset prep notes. +Model-agnostic serving benchmarks for TTS models, including Qwen3-TTS and VoxCPM2. -## Reference workflows -- **HF Transformers (offline, single process)** - Script (example): `benchmarks//transformers/eval_qwen3_moe_omni_transformers.sh` - Outputs: `benchmark_results/perf_stats.json`, `benchmark_results/results.json`, `benchmark_results/audio/` (if audio is produced). +- **Layout**: `tts/bench_tts.py` (serving benchmark driver), `tts/model_configs.yaml` (model registry), `tts/plot_results.py` (result plotting) +- **Dataset**: Seed-TTS full or text-only datasets, plus bundled smoke/design prompts under `build_dataset/` +- **Key metrics**: TTFP (time to first audio packet), E2E latency, RTF (real-time factor), throughput (audio seconds / wall-clock second) -- **vLLM-Omni end-to-end pipeline** - Script (example): `benchmarks//vllm_omni/eval_qwen3_moe_omni.sh` - Outputs: `vllm_omni/logs/*.stats.jsonl` (per-stage/overall latency & TPS), `vllm_omni/logs/stage*.log`, `vllm_omni/outputs/` (text/audio artifacts). +### [Diffusion](diffusion/README.md) — Image and Video Generation -- **Adding a new task/model** - 1) Create `benchmarks//transformers/` and/or `benchmarks//vllm_omni/` with scripts referencing your model and dataset. - 2) Add a task README describing dataset, configs, and expected outputs. - 3) Keep the output/log structure similar for easy comparison (perf_stats/results/audio or text outputs; stats.jsonl/logs for pipeline). +Online-serving benchmark for diffusion image/video models, sending requests to the configured vLLM serving endpoint (`/v1/chat/completions`, `/v1/images/generations`, or `/v1/videos`, depending on backend/task). -## Metrics to watch -- **Throughput**: `overall_tps`, `*_tps_avg` per stage. -- **Latency distribution**: look for long tails in `*.stats.jsonl`. -- **Quality/completeness**: missing outputs or errors in stage logs indicate pipeline failures or misconfigurations. +- **Tasks**: text-to-image, text-to-video, image-to-image, image-to-video, text+image-to-image, text+image-to-video +- **Datasets**: `vbench`, `trace`, `random` +- **Key metrics**: request throughput, latency percentiles, optional SLO attainment -## Troubleshooting -- Verify GPU/driver/FlashAttention2 requirements for your chosen model/config. -- Ensure network access for dataset/model downloads (Google Drive, Hugging Face, etc.). -- If outputs are missing or slow, inspect per-stage logs and `*.stats.jsonl` for errors, stragglers, or contention. +### [GLM-Image](glm_image/README.md) — Text-to-Image and Image-to-Image + +Benchmarks for GLM-Image performance across HuggingFace baseline, vLLM-Omni offline inference, and vLLM-Omni online serving. + +- **Layout**: `glm_image/huggingface/` (HF baseline), `glm_image/vllm-omni/` (offline inference), `glm_image/benchmark_glm_image.py` (online serving) +- **Tasks**: text-to-image and image-to-image +- **Key metrics**: request/image throughput, latency percentiles, optional per-stage pipeline timings + +### [Distributed](distributed/omni_connectors/README.md) — RDMA Connector Testing + +RDMA environment setup and transfer tests for `MooncakeTransferEngineConnector`, including pytest-based single-node checks and manual cross-node benchmarks. + +- **Transfer modes**: `copy`, `zerocopy`, `gpu` (GPUDirect) +- **Supports**: single-node pytest suites and manual multi-node/cross-node transfer testing + +### [Accuracy](accuracy/README.md) — Image Generation and Editing Quality + +Accuracy benchmarks for image generation/editing models, adapting external suites to vLLM-Omni serving and local judge-evaluation flows. + +- **Layout**: `accuracy/text_to_image/` (GEBench), `accuracy/image_to_image/` (GEdit-Bench) +- **Method**: generation and judge scoring both run through local `vllm-omni serve` endpoints + +### Common serving metrics framework + +`vllm_omni/benchmarks/` extends `vllm bench serve --omni` with Omni-specific datasets, backends, and multimodal metrics. Key metrics include: + +- **Text output**: TTFT (time to first token), TPOT (time per output token), ITL (inter-token latency) +- **Audio output**: TTFP (time to first audio packet), E2E latency, RTF (real-time factor) +- **Throughput**: request throughput, output token throughput, total token throughput, audio throughput + +See `vllm_omni/benchmarks/serve.py` for the `vllm bench serve --omni` runner wrapper and `vllm_omni/benchmarks/metrics/` for Omni metric definitions. + +## Adding a new benchmark + +1. Create a subfolder under `benchmarks//` with scripts, configs if needed, and a `README.md`. +2. If comparing against another runtime, use clear backend subfolders where applicable, such as `huggingface/` and `vllm-omni/`, or follow the shared TTS serving benchmark pattern in `tts/`. +3. Add reusable dataset or prompt-building utilities to `build_dataset/` if applicable. +4. Update this README with a link to the new benchmark family. From 753508fe025c3f897a729cd7cef3817e12f06a5f Mon Sep 17 00:00:00 2001 From: Jason <72191212+JasonJ2021@users.noreply.github.com> Date: Wed, 27 May 2026 11:06:18 +0800 Subject: [PATCH 041/112] [Bugfix] Fix Qwen-Image-Edit-2511 TeaCache zero_cond_t handling (#3219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: JasonJ2021 Signed-off-by: Jason <72191212+JasonJ2021@users.noreply.github.com> Co-authored-by: Zeyu Huang | 黃澤宇 <11222265+fhfuih@users.noreply.github.com> --- ...json => test_qwen_image_edit_2511_vllm_omni.json} | 12 ++++++------ tests/e2e/accuracy/test_qwen_image_edit.py | 2 +- .../online_serving/test_qwen_image_edit_expansion.py | 8 ++++---- vllm_omni/diffusion/cache/teacache/extractors.py | 6 ++++-- 4 files changed, 15 insertions(+), 13 deletions(-) rename tests/dfx/perf/tests/{test_qwen_image_edit_2509_vllm_omni.json => test_qwen_image_edit_2511_vllm_omni.json} (94%) diff --git a/tests/dfx/perf/tests/test_qwen_image_edit_2509_vllm_omni.json b/tests/dfx/perf/tests/test_qwen_image_edit_2511_vllm_omni.json similarity index 94% rename from tests/dfx/perf/tests/test_qwen_image_edit_2509_vllm_omni.json rename to tests/dfx/perf/tests/test_qwen_image_edit_2511_vllm_omni.json index d13f37053..82c2ca27d 100644 --- a/tests/dfx/perf/tests/test_qwen_image_edit_2509_vllm_omni.json +++ b/tests/dfx/perf/tests/test_qwen_image_edit_2511_vllm_omni.json @@ -1,10 +1,10 @@ [ { - "test_name": "test_qwen_image_edit_2509_single_device", + "test_name": "test_qwen_image_edit_2511_single_device", "description": "Single-device baseline (two input images)", "server_type": "vllm-omni", "server_params": { - "model": "Qwen/Qwen-Image-Edit-2509", + "model": "Qwen/Qwen-Image-Edit-2511", "serve_args": { "enable-diffusion-pipeline-profiler": true } @@ -49,11 +49,11 @@ ] }, { - "test_name": "test_qwen_image_edit_2509_ulysses2_cfg2_vae_patch4", + "test_name": "test_qwen_image_edit_2511_ulysses2_cfg2_vae_patch4", "description": "Ulysses SP=2 + CFG=2 + VAE patch parallel=4", "server_type": "vllm-omni", "server_params": { - "model": "Qwen/Qwen-Image-Edit-2509", + "model": "Qwen/Qwen-Image-Edit-2511", "serve_args": { "ulysses-degree": 2, "cfg-parallel-size": 2, @@ -102,11 +102,11 @@ ] }, { - "test_name": "test_qwen_image_edit_2509_ulysses2_cfg2_cache_dit", + "test_name": "test_qwen_image_edit_2511_ulysses2_cfg2_cache_dit", "description": "Ulysses SP=2 + CFG=2 + CacheDiT", "server_type": "vllm-omni", "server_params": { - "model": "Qwen/Qwen-Image-Edit-2509", + "model": "Qwen/Qwen-Image-Edit-2511", "serve_args": { "ulysses-degree": 2, "cfg-parallel-size": 2, diff --git a/tests/e2e/accuracy/test_qwen_image_edit.py b/tests/e2e/accuracy/test_qwen_image_edit.py index ea4a9ff1b..540444d42 100644 --- a/tests/e2e/accuracy/test_qwen_image_edit.py +++ b/tests/e2e/accuracy/test_qwen_image_edit.py @@ -20,7 +20,7 @@ pytestmark = [pytest.mark.full_model, pytest.mark.diffusion] SINGLE_MODEL = "Qwen/Qwen-Image-Edit" -MULTIPLE_MODEL = "Qwen/Qwen-Image-Edit-2509" +MULTIPLE_MODEL = "Qwen/Qwen-Image-Edit-2511" WIDTH = 512 HEIGHT = 512 NUM_INFERENCE_STEPS = 20 diff --git a/tests/e2e/online_serving/test_qwen_image_edit_expansion.py b/tests/e2e/online_serving/test_qwen_image_edit_expansion.py index 9b30ef4eb..632f75231 100644 --- a/tests/e2e/online_serving/test_qwen_image_edit_expansion.py +++ b/tests/e2e/online_serving/test_qwen_image_edit_expansion.py @@ -141,10 +141,10 @@ def test_qwen_image_edit(omni_server: OmniServer, openai_client: OpenAIClientHan @pytest.mark.parametrize( "omni_server", - _get_diffusion_feature_cases("Qwen/Qwen-Image-Edit-2509"), + _get_diffusion_feature_cases("Qwen/Qwen-Image-Edit-2511"), indirect=True, ) -def test_qwen_image_edit_2509_single_image(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_qwen_image_edit_2511_single_image(omni_server: OmniServer, openai_client: OpenAIClientHandler): """Test Qwen-Image-Edit-2509 with a single image input. Regression: with tea_cache enabled and zero_cond_t=True, the TeaCache @@ -176,10 +176,10 @@ def test_qwen_image_edit_2509_single_image(omni_server: OmniServer, openai_clien @pytest.mark.parametrize( "omni_server", - _get_diffusion_feature_cases("Qwen/Qwen-Image-Edit-2509"), + _get_diffusion_feature_cases("Qwen/Qwen-Image-Edit-2511"), indirect=True, ) -def test_qwen_image_edit_2509_two_images(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_qwen_image_edit_2511_two_images(omni_server: OmniServer, openai_client: OpenAIClientHandler): """Test Qwen-Image-Edit-2509 with two image inputs.""" image_data_url_1 = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" image_data_url_2 = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" diff --git a/vllm_omni/diffusion/cache/teacache/extractors.py b/vllm_omni/diffusion/cache/teacache/extractors.py index 5b7ab14e5..91259bc8e 100644 --- a/vllm_omni/diffusion/cache/teacache/extractors.py +++ b/vllm_omni/diffusion/cache/teacache/extractors.py @@ -277,8 +277,10 @@ def extract_qwen_context( def postprocess(h): """Apply Qwen-specific output postprocessing.""" - t = temb.chunk(2, dim=0)[0] if module.zero_cond_t else temb - h = module.norm_out(h, t) + if getattr(module, "zero_cond_t", False): + h = module.norm_out(h, temb.chunk(2, dim=0)[0]) + else: + h = module.norm_out(h, temb) output = module.proj_out(h) if not return_dict: return (output,) From 41cb6378a20becad583d222c155ac485b1c12ddc Mon Sep 17 00:00:00 2001 From: WeiQing Chen <40507679+david6666666@users.noreply.github.com> Date: Wed, 27 May 2026 14:17:10 +0800 Subject: [PATCH 042/112] [Perf] Trim HunyuanVideo encoder padding tokens (#3844) Signed-off-by: david6666666 <530634352@qq.com> Co-authored-by: SYLAR <125541396+lishunyang12@users.noreply.github.com> --- .../models/hunyuan_video/hunyuan_video_15_transformer.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vllm_omni/diffusion/models/hunyuan_video/hunyuan_video_15_transformer.py b/vllm_omni/diffusion/models/hunyuan_video/hunyuan_video_15_transformer.py index 30ba62184..1ef618003 100644 --- a/vllm_omni/diffusion/models/hunyuan_video/hunyuan_video_15_transformer.py +++ b/vllm_omni/diffusion/models/hunyuan_video/hunyuan_video_15_transformer.py @@ -764,6 +764,13 @@ class HunyuanVideo15Transformer3DModel(nn.Module): # Create explicit attn_mask for image tokens when SP auto_pad is active. ctx = get_forward_context() + if not ctx.sp_active: + max_valid_encoder_tokens = int(encoder_attention_mask.sum(dim=1).max().item()) + if max_valid_encoder_tokens < encoder_attention_mask.shape[1]: + encoder_hidden_states = encoder_hidden_states[:, :max_valid_encoder_tokens] + encoder_attention_mask = encoder_attention_mask[:, :max_valid_encoder_tokens] + if encoder_attention_mask.all(): + encoder_attention_mask = None hidden_states_mask = None if ctx.sp_original_seq_len is not None and ctx.sp_padding_size > 0: padded_seq_len = ctx.sp_original_seq_len + ctx.sp_padding_size From c349f62839ebf3fafc13c7a06a9be6670363103c Mon Sep 17 00:00:00 2001 From: "rongfu.leng" Date: Wed, 27 May 2026 17:02:17 +0800 Subject: [PATCH 043/112] [Feat] opt qwen image model load use ColumnParallelLinear replace ReplicatedLinear (#3875) Signed-off-by: rongfu.leng Co-authored-by: Peter Pan --- .../models/qwen_image/qwen_image_transformer.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py b/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py index dac724162..5e4d526d8 100644 --- a/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py +++ b/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py @@ -716,15 +716,17 @@ class QwenImageTransformerBlock(nn.Module): self.attention_head_dim = attention_head_dim # Image processing modules. - # Modulation linear is kept full precision (quant_config=None) — it + # Modulation linear is kept unquantized (quant_config=None) — it # produces shift/scale/gate values that are precision-sensitive - # (see #2728). + # (see #2728). Use column TP with gather_output=True so weights are + # sharded while downstream modulation still receives full [B, 6 * dim]. self.img_mod = nn.Sequential( nn.SiLU(), - ReplicatedLinear( + ColumnParallelLinear( dim, 6 * dim, bias=True, + gather_output=True, return_bias=False, quant_config=None, prefix=_join_prefix(prefix, "img_mod.1"), @@ -751,10 +753,11 @@ class QwenImageTransformerBlock(nn.Module): # Text processing modules. self.txt_mod = nn.Sequential( nn.SiLU(), - ReplicatedLinear( + ColumnParallelLinear( dim, 6 * dim, bias=True, + gather_output=True, return_bias=False, quant_config=None, prefix=_join_prefix(prefix, "txt_mod.1"), From c33ef34654053f629a8b81a50e5ec09e1c1b7db4 Mon Sep 17 00:00:00 2001 From: bjf-frz Date: Wed, 27 May 2026 19:18:01 +0800 Subject: [PATCH 044/112] [Bugfix]Fix Hunyuan Image3 denoise flow alignment (#3857) Signed-off-by: bjf-frz Co-authored-by: Hongsheng Liu --- .../test_image_kv_cache_manager.py | 202 ++++++++++++------ tests/e2e/accuracy/test_hunyuan_image3.py | 6 +- .../hunyuan_image3_transformer.py | 126 ++++++----- .../hunyuan_image3/pipeline_hunyuan_image3.py | 41 +++- .../models/hunyuan_image3/prompt_utils.py | 4 +- 5 files changed, 246 insertions(+), 133 deletions(-) diff --git a/tests/diffusion/models/hunyuan_image3/test_image_kv_cache_manager.py b/tests/diffusion/models/hunyuan_image3/test_image_kv_cache_manager.py index 342103350..8a35d8aaa 100644 --- a/tests/diffusion/models/hunyuan_image3/test_image_kv_cache_manager.py +++ b/tests/diffusion/models/hunyuan_image3/test_image_kv_cache_manager.py @@ -23,6 +23,7 @@ NUM_HEADS = 4 NUM_KV_HEADS = 2 HEAD_DIM = 16 IMAGE_TOKEN_LEN = 8 +SUFFIX_TOKEN_LEN = 3 SCALING = 1.0 / math.sqrt(HEAD_DIM) @@ -79,6 +80,10 @@ def _make_known_kv(num_tokens, base=0.0): return k, v +def _gen_timestep_index(bs: int, current_start: int) -> torch.Tensor: + return torch.full((bs, 1), current_start, dtype=torch.long) + + def _call_mgr( mgr, bs, @@ -90,6 +95,8 @@ def _call_mgr( uncond_cfg_prefill=False, num_image_tokens=IMAGE_TOKEN_LEN, shard_image_size=None, + gen_timestep_scatter_index=None, + position_ids=None, ): query = torch.randn(bs * q_len, NUM_HEADS, HEAD_DIM) attn_mask = torch.zeros(bs, 1, seq_len, seq_len) @@ -104,6 +111,8 @@ def _call_mgr( uncond_cfg_prefill=uncond_cfg_prefill, num_image_tokens=num_image_tokens, shard_image_size=shard_image_size, + gen_timestep_scatter_index=gen_timestep_scatter_index, + position_ids=position_ids, ) @@ -117,54 +126,65 @@ def test_no_ar_kv(bs): """ No AR KV injected. Tests the basic first_step cache → update reuse path. - Sequence layout per batch on first_step (q_len=12, IMAGE_TOKEN_LEN=8): - [prompt(3) | image(8) | eoi(1)] - image_size = IMAGE_TOKEN_LEN + 1 = 9 - cached_prompt_len = seq_len - image_size = 12 - 9 = 3 + Sequence layout per batch on first_step (q_len=14, IMAGE_TOKEN_LEN=8): + [prompt(3) | current timestep/image(8) | suffix(3)] + gen_timestep_scatter_index = 3 + cached_prompt_len = 3 After first_step: - image_kv_cache_map stores prompt tokens [0:3] for each batch (total 3*bs tokens flat). + image_kv_cache_map stores prompt tokens [0:3] for each batch. - Update step (q_len=IMAGE_TOKEN_LEN=8, seq_len = cached_prompt(3) + eoi(1) + image(8) = 12): - _reuse_prompt_kv produces [cached_prompt(3) | new_image(8) | zero_eoi(1)] per batch. + Update step (q_len=IMAGE_TOKEN_LEN=8, seq_len = cached_prompt(3) + current(8) = 11): + _reuse_prompt_kv produces [cached_prompt(3) | new_current(8)] per batch. """ mgr = _make_cache_mgr() assert mgr.image_kv_cache_map is None # --- first_step --- - q_len = 12 + prompt_len = 3 + q_len = prompt_len + IMAGE_TOKEN_LEN + SUFFIX_TOKEN_LEN k_flat, v_flat = _make_known_kv(bs * q_len, base=1.0) - _call_mgr(mgr, bs, q_len=q_len, seq_len=q_len, key_flat=k_flat, value_flat=v_flat, first_step=True) + _call_mgr( + mgr, + bs, + q_len=q_len, + seq_len=q_len, + key_flat=k_flat, + value_flat=v_flat, + first_step=True, + gen_timestep_scatter_index=_gen_timestep_index(bs, prompt_len), + ) cached_key, cached_value = mgr.image_kv_cache_map # 3 prompt tokens cached per batch - assert cached_key.shape[0] == 3 * bs + assert cached_key.shape == (bs, prompt_len, NUM_KV_HEADS, HEAD_DIM) + assert torch.equal(mgr.image_kv_cache_lens, torch.full((bs,), prompt_len)) for b in range(bs): flat_offset = b * q_len - cache_offset = b * 3 - assert torch.allclose(cached_key[cache_offset : cache_offset + 3], k_flat[flat_offset : flat_offset + 3]) - assert torch.allclose(cached_value[cache_offset : cache_offset + 3], v_flat[flat_offset : flat_offset + 3]) + assert torch.allclose(cached_key[b, :prompt_len], k_flat[flat_offset : flat_offset + prompt_len]) + assert torch.allclose(cached_value[b, :prompt_len], v_flat[flat_offset : flat_offset + prompt_len]) # --- update step --- img_q_len = IMAGE_TOKEN_LEN - update_seq_len = 3 + 1 + img_q_len # cached_prompt(3) + eoi(1) + image(8) = 12 + update_seq_len = prompt_len + img_q_len # cached_prompt(3) + current(8) = 11 new_img_k, new_img_v = _make_known_kv(bs * img_q_len, base=50.0) key_input = new_img_k.reshape(bs, img_q_len, NUM_KV_HEADS, HEAD_DIM) val_input = new_img_v.reshape(bs, img_q_len, NUM_KV_HEADS, HEAD_DIM) - result_k, result_v = mgr._reuse_prompt_kv(key_input, val_input, update_seq_len, bs=bs) + position_ids = torch.arange(prompt_len, prompt_len + img_q_len).repeat(bs, 1) + result_k, result_v = mgr._reuse_prompt_kv(key_input, val_input, update_seq_len, bs=bs, position_ids=position_ids) assert result_k.shape == (bs, update_seq_len, NUM_KV_HEADS, HEAD_DIM) for b in range(bs): - cache_offset = b * 3 img_offset = b * img_q_len # Cached prompt preserved - assert torch.allclose(result_k[b, :3], cached_key[cache_offset : cache_offset + 3]) + assert torch.allclose(result_k[b, :prompt_len], cached_key[b, :prompt_len]) # New image tokens - assert torch.allclose(result_k[b, 3 : 3 + img_q_len], new_img_k[img_offset : img_offset + img_q_len]) - # Zero eoi at the end - assert torch.allclose(result_k[b, -1], torch.zeros(NUM_KV_HEADS, HEAD_DIM)) + assert torch.allclose( + result_k[b, prompt_len : prompt_len + img_q_len], + new_img_k[img_offset : img_offset + img_q_len], + ) # ============================================================ @@ -178,16 +198,17 @@ def test_ar_kv_no_cfg(sp_size): AR KV injected, bs=1, no CFG. Tests AR prefix prepend + cache + reuse. sp_size=1: - first_step input: q_len=12, ar_len=5, seq_len=17 - Sequence layout: [ar(5) | prompt(3) | image(8) | eoi(1)] - image_size = IMAGE_TOKEN_LEN + 1 = 9 - cached_prompt_len = seq_len - image_size = 17 - 9 = 8 (= ar(5) + prompt(3)) + first_step input: q_len=14, ar_len=5, seq_len=19 + Sequence layout: [ar(5) | prompt(3) | current(8) | suffix(3)] + gen_timestep_scatter_index = 3 + cached_prompt_len = ar(5) + prompt(3) = 8 - Update step: q_len=8, seq_len = cached_prompt(8) + eoi(1) + image(8) = 17 - Result: [cached(8) | new_image(8) | zero_eoi(1)] + Update step: q_len=8, seq_len = cached_prompt(8) + current(8) = 16 + Result: [cached(8) | new_current(8)] sp_size=2: shard_image_size = 4 (passed externally, simulating SP sharding) + gen_timestep_scatter_index = 8 cached_prompt_len = seq_len - shard_image_size = 17 - 4 = 13 (= ar(5) + prompt(8)) Note: with SP, more of the sequence is treated as "prompt" because image tokens are sharded across ranks. @@ -204,15 +225,23 @@ def test_ar_kv_no_cfg(sp_size): mgr._injected_ar_kv = [(ar_k.clone(), ar_v.clone())] # --- first_step: call _cache_prompt_kv directly --- - bs, q_len = 1, 12 - seq_len = q_len + ar_len # 17 + bs = 1 + q_len = 12 if sp_size > 1 else 14 + seq_len = q_len + ar_len k_raw, v_raw = _make_known_kv(q_len, base=1.0) key_4d = k_raw.reshape(1, q_len, NUM_KV_HEADS, HEAD_DIM) val_4d = v_raw.reshape(1, q_len, NUM_KV_HEADS, HEAD_DIM) shard_image_size = 4 if sp_size > 1 else None + current_start = 8 if sp_size > 1 else 3 mgr.image_kv_cache_map = None - mgr._cache_prompt_kv(key_4d, val_4d, seq_len, shard_image_size) + mgr._cache_prompt_kv( + key_4d, + val_4d, + seq_len, + shard_image_size, + gen_timestep_scatter_index=_gen_timestep_index(bs, current_start), + ) cached_key, cached_value = mgr.image_kv_cache_map if sp_size == 1: @@ -222,25 +251,32 @@ def test_ar_kv_no_cfg(sp_size): # cached = seq_len - shard_image_size = 17 - 4 = 13 expected_cached_len = seq_len - shard_image_size - assert cached_key.shape[0] == expected_cached_len + assert cached_key.shape == (bs, expected_cached_len, NUM_KV_HEADS, HEAD_DIM) # AR KV always at the front - assert torch.allclose(cached_key[:ar_len], ar_k) - assert torch.allclose(cached_value[:ar_len], ar_v) + assert torch.allclose(cached_key[0, :ar_len], ar_k) + assert torch.allclose(cached_value[0, :ar_len], ar_v) # Prompt tokens follow AR prompt_cached = expected_cached_len - ar_len - assert torch.allclose(cached_key[ar_len : ar_len + prompt_cached], k_raw[:prompt_cached]) + assert torch.allclose(cached_key[0, ar_len : ar_len + prompt_cached], k_raw[:prompt_cached]) # AR KV consumed assert mgr._injected_ar_kv is None # --- update step: call _reuse_prompt_kv directly --- if sp_size == 1: img_q_len = IMAGE_TOKEN_LEN - update_seq_len = expected_cached_len + 1 + img_q_len # 8 + 1 + 8 = 17 + update_seq_len = expected_cached_len + img_q_len # 8 + 8 = 16 new_img_k, new_img_v = _make_known_kv(img_q_len, base=50.0) key_input = new_img_k.reshape(1, img_q_len, NUM_KV_HEADS, HEAD_DIM) val_input = new_img_v.reshape(1, img_q_len, NUM_KV_HEADS, HEAD_DIM) - result_k, result_v = mgr._reuse_prompt_kv(key_input, val_input, update_seq_len, bs=1) + position_ids = torch.arange(expected_cached_len, expected_cached_len + img_q_len).reshape(1, img_q_len) + result_k, result_v = mgr._reuse_prompt_kv( + key_input, + val_input, + update_seq_len, + bs=1, + position_ids=position_ids, + ) assert result_k.shape == (1, update_seq_len, NUM_KV_HEADS, HEAD_DIM) # AR + prompt preserved @@ -248,8 +284,6 @@ def test_ar_kv_no_cfg(sp_size): assert torch.allclose(result_k[0, ar_len : ar_len + prompt_cached], k_raw[:prompt_cached]) # New image tokens assert torch.allclose(result_k[0, expected_cached_len : expected_cached_len + img_q_len], new_img_k) - # Zero eoi - assert torch.allclose(result_k[0, -1], torch.zeros(NUM_KV_HEADS, HEAD_DIM)) else: # SP path: _reuse_prompt_kv returns only cached prompt (no image concat) img_q_len = shard_image_size # 4 @@ -292,13 +326,12 @@ def test_ar_kv_with_cfg(cfg_parallel, bs): → _injected_ar_kv becomes [(pos_ar(10), pos_av(10)), (neg_k(10), neg_v(10))] first_step (bs=2, q_len=12, seq_len=22): - Batch 0 (pos): [pos_ar(10) | prompt(3) | image(8) | eoi(1)] - Batch 1 (neg): [neg_ar(10) | prompt(3) | image(8) | eoi(1)] - cached_prompt_len per batch = 22 - 9 = 13 - Total cached = 13 * 2 = 26 + Batch 0 (pos): [pos_ar(10) | prompt(3) | current(8) | suffix(3)] + Batch 1 (neg): [neg_ar(10) | prompt(3) | current(8) | suffix(3)] + cached_prompt_len per batch = 10 + 3 = 13 - Update (bs=2, seq_len = 13 + 1 + 8 = 22): - Result per batch: [cached(13) | new_image(8) | zero_eoi(1)] + Update (bs=2, seq_len = 13 + 8 = 21): + Result per batch: [cached(13) | new_current(8)] CFG Parallel (cfg_parallel=True, bs=1): This rank handles only the negative branch. @@ -308,11 +341,11 @@ def test_ar_kv_with_cfg(cfg_parallel, bs): → _injected_ar_kv = [(neg_k(10), neg_v(10))] first_step (bs=1, q_len=12, seq_len=22): - [neg_ar(10) | prompt(3) | image(8) | eoi(1)] - cached_prompt_len = 22 - 9 = 13 + [neg_ar(10) | prompt(3) | current(8) | suffix(3)] + cached_prompt_len = 13 - Update (bs=1, seq_len = 13 + 1 + 8 = 22): - Result: [cached(13) | new_image(8) | zero_eoi(1)] + Update (bs=1, seq_len = 13 + 8 = 21): + Result: [cached(13) | new_current(8)] """ positive_reuse_len = 10 negative_reuse_len = 6 @@ -351,46 +384,62 @@ def test_ar_kv_with_cfg(cfg_parallel, bs): mgr._injected_ar_kv = [mgr._injected_ar_kv[1]] # --- first_step --- - q_len = 12 + prompt_len = 3 + q_len = prompt_len + IMAGE_TOKEN_LEN + SUFFIX_TOKEN_LEN seq_len = q_len + positive_reuse_len # 22 k_flat, v_flat = _make_known_kv(bs * q_len, base=1.0) - _call_mgr(mgr, bs, q_len=q_len, seq_len=seq_len, key_flat=k_flat, value_flat=v_flat, first_step=True) + _call_mgr( + mgr, + bs, + q_len=q_len, + seq_len=seq_len, + key_flat=k_flat, + value_flat=v_flat, + first_step=True, + gen_timestep_scatter_index=_gen_timestep_index(bs, prompt_len), + ) cached_key, cached_value = mgr.image_kv_cache_map - cached_prompt_len_per_batch = seq_len - (IMAGE_TOKEN_LEN + 1) # 22 - 9 = 13 - assert cached_key.shape[0] == cached_prompt_len_per_batch * bs + cached_prompt_len_per_batch = positive_reuse_len + prompt_len + assert cached_key.shape == (bs, cached_prompt_len_per_batch, NUM_KV_HEADS, HEAD_DIM) assert mgr._injected_ar_kv is None if not cfg_parallel: # bs=2: batch 0 = pos, batch 1 = neg # Batch 0: pos_ar(10) + prompt(3) - assert torch.allclose(cached_key[:positive_reuse_len], pos_ar_k) - assert torch.allclose(cached_key[positive_reuse_len:13], k_flat[:3]) + assert torch.allclose(cached_key[0, :positive_reuse_len], pos_ar_k) + assert torch.allclose(cached_key[0, positive_reuse_len:13], k_flat[:3]) # Batch 1: neg_ar(10) + prompt(3) - assert torch.allclose(cached_key[13:23], neg_ar_k) - assert torch.allclose(cached_key[23:26], k_flat[q_len : q_len + 3]) + assert torch.allclose(cached_key[1, :positive_reuse_len], neg_ar_k) + assert torch.allclose(cached_key[1, positive_reuse_len:13], k_flat[q_len : q_len + 3]) else: # bs=1: only neg branch - assert torch.allclose(cached_key[:positive_reuse_len], neg_ar_k) - assert torch.allclose(cached_key[positive_reuse_len:13], k_flat[:3]) + assert torch.allclose(cached_key[0, :positive_reuse_len], neg_ar_k) + assert torch.allclose(cached_key[0, positive_reuse_len:13], k_flat[:3]) # --- update step --- img_q_len = IMAGE_TOKEN_LEN - update_seq_len = cached_prompt_len_per_batch + 1 + img_q_len # 13 + 1 + 8 = 22 + update_seq_len = cached_prompt_len_per_batch + img_q_len # 13 + 8 = 21 new_img_k, new_img_v = _make_known_kv(bs * img_q_len, base=50.0) key_input = new_img_k.reshape(bs, img_q_len, NUM_KV_HEADS, HEAD_DIM) val_input = new_img_v.reshape(bs, img_q_len, NUM_KV_HEADS, HEAD_DIM) - result_k, result_v = mgr._reuse_prompt_kv(key_input, val_input, update_seq_len, bs=bs) + position_ids = torch.arange(cached_prompt_len_per_batch, cached_prompt_len_per_batch + img_q_len).repeat(bs, 1) + result_k, result_v = mgr._reuse_prompt_kv( + key_input, + val_input, + update_seq_len, + bs=bs, + position_ids=position_ids, + ) assert result_k.shape == (bs, update_seq_len, NUM_KV_HEADS, HEAD_DIM) for b in range(bs): - cache_offset = b * cached_prompt_len_per_batch # Cached prompt preserved assert torch.allclose( result_k[b, :cached_prompt_len_per_batch], - cached_key[cache_offset : cache_offset + cached_prompt_len_per_batch], + cached_key[b, :cached_prompt_len_per_batch], ) # New image tokens img_offset = b * img_q_len @@ -398,8 +447,6 @@ def test_ar_kv_with_cfg(cfg_parallel, bs): result_k[b, cached_prompt_len_per_batch : cached_prompt_len_per_batch + img_q_len], new_img_k[img_offset : img_offset + img_q_len], ) - # Zero eoi - assert torch.allclose(result_k[b, -1], torch.zeros(NUM_KV_HEADS, HEAD_DIM)) # ============================================================ @@ -412,7 +459,7 @@ def test_cross_request_isolation(): Verify leftover image_kv_cache_map from a previous request is NOT treated as AR KV. Setup: mgr has stale cache from a prior request (9 tokens, base=999). - New request: first_step with q_len=12, no AR KV. + New request: first_step with q_len=14, no AR KV. Expected: stale cache is overwritten. New cache = prompt tokens from current request. The stale values (999.x) must NOT appear in the new cache. @@ -421,20 +468,33 @@ def test_cross_request_isolation(): # Simulate leftover from previous request leftover_k, leftover_v = _make_known_kv(9, base=999.0) - mgr.image_kv_cache_map = (leftover_k, leftover_v) + mgr.image_kv_cache_map = ( + leftover_k.reshape(1, 9, NUM_KV_HEADS, HEAD_DIM), + leftover_v.reshape(1, 9, NUM_KV_HEADS, HEAD_DIM), + ) assert mgr._injected_ar_kv is None # New request first_step - bs, seq_len = 1, 12 + bs = 1 + prompt_len = 3 + seq_len = prompt_len + IMAGE_TOKEN_LEN + SUFFIX_TOKEN_LEN k_flat, v_flat = _make_known_kv(bs * seq_len, base=1.0) - _call_mgr(mgr, bs, q_len=seq_len, seq_len=seq_len, key_flat=k_flat, value_flat=v_flat, first_step=True) + _call_mgr( + mgr, + bs, + q_len=seq_len, + seq_len=seq_len, + key_flat=k_flat, + value_flat=v_flat, + first_step=True, + gen_timestep_scatter_index=_gen_timestep_index(bs, prompt_len), + ) cached_key, cached_value = mgr.image_kv_cache_map - # cached_prompt_len = 12 - 9 = 3 - assert cached_key.shape[0] == 3 + assert cached_key.shape == (bs, prompt_len, NUM_KV_HEADS, HEAD_DIM) # Must be from current request, not stale - assert torch.allclose(cached_key[:3], k_flat[:3]) - assert torch.allclose(cached_value[:3], v_flat[:3]) + assert torch.allclose(cached_key[0, :prompt_len], k_flat[:prompt_len]) + assert torch.allclose(cached_value[0, :prompt_len], v_flat[:prompt_len]) # Stale values must not be present assert not torch.any(cached_key >= 999.0) diff --git a/tests/e2e/accuracy/test_hunyuan_image3.py b/tests/e2e/accuracy/test_hunyuan_image3.py index 81394297d..7ed3c85a0 100644 --- a/tests/e2e/accuracy/test_hunyuan_image3.py +++ b/tests/e2e/accuracy/test_hunyuan_image3.py @@ -77,9 +77,9 @@ THRESHOLDS = { "text_prefix_match": 10, # First 10 characters must match exactly "cot_semantic_sim": 0.9, # Full CoT semantic similarity # Image comparison - "clip_score": 85, # CLIP image semantic similarity - "ssim": 0.20, # Structural similarity - "psnr": 11.0, # Peak signal-to-noise ratio (dB) + "clip_score": 90, # CLIP image semantic similarity + "ssim": 0.26, # Structural similarity + "psnr": 12.5, # Peak signal-to-noise ratio (dB) } QUANT_PROMPT = "A brown and white dog is running on the grass." diff --git a/vllm_omni/diffusion/models/hunyuan_image3/hunyuan_image3_transformer.py b/vllm_omni/diffusion/models/hunyuan_image3/hunyuan_image3_transformer.py index eaf0c2afe..80bb1d7b6 100644 --- a/vllm_omni/diffusion/models/hunyuan_image3/hunyuan_image3_transformer.py +++ b/vllm_omni/diffusion/models/hunyuan_image3/hunyuan_image3_transformer.py @@ -891,6 +891,7 @@ class ImageKVCacheManager: # cache related self.image_token_len: int = image_token_len self.image_kv_cache_map: tuple[torch.Tensor, torch.Tensor] | None = None + self.image_kv_cache_lens: torch.Tensor | None = None self._injected_ar_kv: list[tuple[torch.Tensor, torch.Tensor]] | None = None self.sp_size = get_sequence_parallel_world_size() @@ -904,12 +905,23 @@ class ImageKVCacheManager: prefix=f"{prefix}.attn" if prefix else "", ) + @staticmethod + def _get_current_starts( + gen_timestep_scatter_index: torch.Tensor | None, + ar_kv_len: int = 0, + ) -> torch.Tensor: + assert gen_timestep_scatter_index is not None, ( + "`gen_timestep_scatter_index` is required to locate the generated image timestep token for image KV reuse." + ) + return gen_timestep_scatter_index[:, -1].to(dtype=torch.long) + ar_kv_len + def _cache_prompt_kv( self, key: torch.Tensor, value: torch.Tensor, seq_len: int, shard_image_size: int | None = None, + gen_timestep_scatter_index: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: """Cache prompt KV on first_step. Consumes _injected_ar_kv. @@ -939,12 +951,26 @@ class ImageKVCacheManager: key = torch.cat(new_keys, dim=0) value = torch.cat(new_values, dim=0) - image_size = shard_image_size if self.sp_size > 1 else (self.image_token_len + 1) - cached_prompt_len = seq_len - image_size + cached_prompt_lens = self._get_current_starts(gen_timestep_scatter_index, ar_kv_len) + assert torch.all(cached_prompt_lens <= key.shape[1]), ( + f"cached_prompt_lens({cached_prompt_lens.tolist()}) must be <= key length({key.shape[1]})" + ) + if shard_image_size is not None: + expected_prompt_len = seq_len - shard_image_size + assert torch.all(cached_prompt_lens == expected_prompt_len), ( + f"cached_prompt_lens({cached_prompt_lens.tolist()}) != " + f"seq_len({seq_len}) - shard_image_size({shard_image_size})" + ) - cached_key = key[:, :cached_prompt_len].reshape(-1, num_kv_heads, head_dim) - cached_value = value[:, :cached_prompt_len].reshape(-1, num_kv_heads, head_dim) + max_cached_prompt_len = int(cached_prompt_lens.max().item()) + cached_key = key.new_zeros(bs, max_cached_prompt_len, num_kv_heads, head_dim) + cached_value = value.new_zeros(bs, max_cached_prompt_len, num_kv_heads, head_dim) + for b in range(bs): + cached_prompt_len = int(cached_prompt_lens[b].item()) + cached_key[b, :cached_prompt_len] = key[b, :cached_prompt_len] + cached_value[b, :cached_prompt_len] = value[b, :cached_prompt_len] self.image_kv_cache_map = (cached_key, cached_value) + self.image_kv_cache_lens = cached_prompt_lens return key, value def _reuse_prompt_kv( @@ -954,42 +980,36 @@ class ImageKVCacheManager: seq_len: int, bs: int, shard_image_size: int | None = None, + position_ids: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: """ Reuse cached prompt KV in subsequent denoising steps. - Non-SP: returns [cached_prompt, new_image, zero_eoi] shaped [bs, seq_len, ...]. - SP: returns cached prompt only shaped [bs, cached_prompt_len, ...] (used as joint_text). + Non-SP: concatenates cached prompt KV with current image KV. + SP: returns cached prompt KV only (used as joint_text). """ cached_key, cached_value = self.image_kv_cache_map - _, q_len, num_kv_heads, head_dim = key.shape - cached_prompt_len = cached_key.shape[0] // bs + _, q_len, _, _ = key.shape + assert cached_key.dim() == 4 and cached_key.shape[0] == bs + max_cached_prompt_len = cached_key.shape[1] if shard_image_size is not None: - assert cached_prompt_len == seq_len - q_len - result_k = cached_key.reshape(bs, cached_prompt_len, num_kv_heads, head_dim) - result_v = cached_value.reshape(bs, cached_prompt_len, num_kv_heads, head_dim) - return result_k.contiguous(), result_v.contiguous() + assert max_cached_prompt_len + q_len == seq_len, ( + f"max_cached_prompt_len({max_cached_prompt_len}) + q_len({q_len}) != seq_len({seq_len})" + ) + return cached_key.contiguous(), cached_value.contiguous() - assert cached_prompt_len + 1 == seq_len - q_len, f"{cached_prompt_len + 1} != {seq_len - q_len}" - - key = key.reshape(-1, num_kv_heads, head_dim) - value = value.reshape(-1, num_kv_heads, head_dim) - zero_eoi = torch.zeros(1, num_kv_heads, head_dim, device=key.device, dtype=key.dtype) - - new_key, new_value = [], [] - for b in range(bs): - cache_base = b * cached_prompt_len - img_base = b * q_len - new_key.append(cached_key[cache_base : cache_base + cached_prompt_len]) - new_key.append(key[img_base : img_base + q_len]) - new_key.append(zero_eoi) - new_value.append(cached_value[cache_base : cache_base + cached_prompt_len]) - new_value.append(value[img_base : img_base + q_len]) - new_value.append(zero_eoi) - - new_key = torch.cat(new_key).reshape(bs, seq_len, num_kv_heads, head_dim) - new_value = torch.cat(new_value).reshape(bs, seq_len, num_kv_heads, head_dim) + assert max_cached_prompt_len + q_len == seq_len, ( + f"max_cached_prompt_len({max_cached_prompt_len}) + q_len({q_len}) != seq_len({seq_len})" + ) + assert self.image_kv_cache_lens is not None + if position_ids is not None: + assert position_ids.shape == (bs, q_len) + assert torch.all(position_ids[:, 0] == self.image_kv_cache_lens), ( + "The first current position must immediately follow each sample's cached prompt KV." + ) + new_key = torch.cat([cached_key, key], dim=1) + new_value = torch.cat([cached_value, value], dim=1) return new_key.contiguous(), new_value.contiguous() def _build_neg_ar_kv( @@ -1032,7 +1052,7 @@ class ImageKVCacheManager: query_lens = kwargs.get("query_lens") seq_lens = kwargs.get("seq_lens") - shard_image_size = kwargs.get("shard_image_size") + shard_image_size = kwargs.get("shard_image_size") if self.sp_size > 1 else None bs = len(query_lens) q_len = query_lens[0] seq_len = seq_lens[0] @@ -1058,7 +1078,14 @@ class ImageKVCacheManager: value = value[:, :0, :, :] elif first_step: self.image_kv_cache_map = None # reset first - key, value = self._cache_prompt_kv(key, value, seq_len, shard_image_size) + self.image_kv_cache_lens = None + key, value = self._cache_prompt_kv( + key, + value, + seq_len, + shard_image_size, + kwargs.get("gen_timestep_scatter_index"), + ) if self.sp_size > 1: local_prompt_len = seq_len - shard_image_size join_query_len = query.shape[1] - shard_image_size @@ -1070,7 +1097,7 @@ class ImageKVCacheManager: value = value[:, local_prompt_len:, :, :] else: if self.sp_size <= 1: - key, value = self._reuse_prompt_kv(key, value, seq_len, bs) + key, value = self._reuse_prompt_kv(key, value, seq_len, bs, position_ids=kwargs.get("position_ids")) else: joint_text_query = query[:, :0, :, :] joint_text_key, joint_text_value = self._reuse_prompt_kv(key, value, seq_len, bs, shard_image_size) @@ -1814,6 +1841,7 @@ class HunyuanImage3DecoderLayer(nn.Module): hidden_states, self_attn_weights, present_key_value = self.self_attn( hidden_states=hidden_states, attention_mask=attention_mask, + position_ids=position_ids, custom_pos_emb=custom_pos_emb, past_key_value=past_key_value, output_attentions=output_attentions, @@ -1878,12 +1906,21 @@ class HunyuanImagePreprocessor(nn.Module): image_token_len: int, is_first_step: bool = False, uncond_cfg_prefill: bool = False, + gen_timestep_scatter_index: torch.Tensor | None = None, ): # ---------- compute prompt length ---------- if uncond_cfg_prefill: prompt_len = hidden_states.shape[1] elif is_first_step: - prompt_len = hidden_states.shape[1] - image_token_len - 1 + assert gen_timestep_scatter_index is not None, ( + "`gen_timestep_scatter_index` is required to split prompt and " + "generated image tokens for sequence parallel image generation." + ) + prompt_lens = gen_timestep_scatter_index[:, -1] + assert torch.all(prompt_lens == prompt_lens[0]), ( + "Sequence parallel image generation requires the generated timestep position to match across the batch." + ) + prompt_len = int(prompt_lens[0].item()) else: prompt_len = 0 @@ -2327,6 +2364,7 @@ class HunyuanImage3Model(nn.Module): num_image_tokens, first_step, uncond_cfg_prefill=uncond_cfg_prefill, + gen_timestep_scatter_index=gen_timestep_scatter_index, ) assert len(set(query_lens)) == 1 and len(set(seq_lens)) == 1, ( "query_lens and seq_lens must be the same for sequence parallel" @@ -2334,15 +2372,15 @@ class HunyuanImage3Model(nn.Module): prompt_size = text_hidden_states.shape[1] shard_image_size = image_hidden_states.shape[1] - if first_step: # [image tokens, last token] - shard_padding_size = shard_image_size * sp_world_size - (num_image_tokens + 1) - else: # [image tokens, last token] + if first_step: + shard_padding_size = shard_image_size * sp_world_size - (origin_query_len - prompt_size) + else: shard_padding_size = shard_image_size * sp_world_size - num_image_tokens if first_step: seq_lens = [prompt_size + shard_image_size + ar_kv_reuse_len for _ in seq_lens] else: seq_lens = [x - y for x, y in zip(seq_lens, query_lens)] - seq_lens = [seq_len + shard_image_size - 1 for seq_len in seq_lens] + seq_lens = [seq_len + shard_image_size for seq_len in seq_lens] query_lens = [prompt_size + shard_image_size for _ in query_lens] if prompt_size > 0: @@ -2357,10 +2395,6 @@ class HunyuanImage3Model(nn.Module): position_ids = image_position_ids custom_pos_emb = (image_custom_pos_emb_sin, image_custom_pos_emb_cos) - if not first_step: - assert torch.all(~attention_mask[..., -1]), "The last token should not be attended to" - attention_mask = attention_mask[..., :-1] # we attention without last token - if shard_padding_size > 0: B, H, Q, K = attention_mask.shape pad = shard_padding_size @@ -3091,15 +3125,11 @@ class HunyuanImage3Text2ImagePipeline(DiffusionPipeline): # Scheduler step (all ranks compute locally in CFG parallel) latents = self.scheduler.step(pred, t, latents, **_scheduler_step_extra_kwargs, return_dict=False)[0] if i != len(timesteps) - 1 and should_compute: - offset = model_kwargs.get("ar_kv_reuse_offset", 0) model_kwargs = self.model._update_model_kwargs_for_generation( # noqa model_output, model_kwargs, ) - if input_ids.shape[1] != model_kwargs["position_ids"].shape[1]: - # Select using relative positions - index = model_kwargs["position_ids"] - offset - input_ids = torch.gather(input_ids, 1, index=index) + input_ids = None attention_mask = model_kwargs.get("attention_mask") b, _, q_len1, seq_len = attention_mask.shape query_lens = [q_len1] * b diff --git a/vllm_omni/diffusion/models/hunyuan_image3/pipeline_hunyuan_image3.py b/vllm_omni/diffusion/models/hunyuan_image3/pipeline_hunyuan_image3.py index 677d67796..b4a588df9 100644 --- a/vllm_omni/diffusion/models/hunyuan_image3/pipeline_hunyuan_image3.py +++ b/vllm_omni/diffusion/models/hunyuan_image3/pipeline_hunyuan_image3.py @@ -1001,7 +1001,7 @@ class HunyuanImage3Pipeline( if inputs_embeds is not None and past_key_values is None: model_inputs = {"inputs_embeds": inputs_embeds} else: - if input_ids.shape[1] != kwargs["position_ids"].shape[1]: # in decode steps + if input_ids is not None and input_ids.shape[1] != kwargs["position_ids"].shape[1]: # in decode steps input_ids = torch.gather(input_ids, dim=1, index=kwargs["position_ids"]) model_inputs = {"input_ids": input_ids} @@ -1082,12 +1082,27 @@ class HunyuanImage3Pipeline( # attention mask mask_list = [] - for attention_mask_i, position_ids_i in zip( - model_kwargs["attention_mask"], updated_model_kwargs["position_ids"] + current_starts = timestep_position_ids.reshape(-1) + max_current_start = int(current_starts.max().item()) + for attention_mask_i, position_ids_i, current_start_i in zip( + model_kwargs["attention_mask"], updated_model_kwargs["position_ids"], current_starts ): - mask_list.append( - torch.index_select(attention_mask_i, dim=1, index=position_ids_i.reshape(-1) - offset) + query_mask = torch.index_select(attention_mask_i, dim=1, index=position_ids_i.reshape(-1) - offset) + current_start = int(current_start_i.item()) + prefix_mask = torch.index_select( + query_mask, + dim=2, + index=torch.arange(current_start, device=image_mask.device), ) + if current_start < max_current_start: + prefix_pad = query_mask.new_zeros( + query_mask.shape[0], + query_mask.shape[1], + max_current_start - current_start, + ) + prefix_mask = torch.cat([prefix_mask, prefix_pad], dim=2) + current_mask = torch.index_select(query_mask, dim=2, index=position_ids_i.reshape(-1)) + mask_list.append(torch.cat([prefix_mask, current_mask], dim=2)) attention_mask = torch.stack(mask_list, dim=0) updated_model_kwargs["attention_mask"] = attention_mask updated_model_kwargs["gen_timestep_scatter_index"] = model_kwargs["gen_timestep_scatter_index"] @@ -1219,9 +1234,14 @@ class HunyuanImage3Pipeline( ) custom_pos_emb = self.get_pos_emb(custom_pos_emb, position_ids) - inputs_embeds = self.model.embed_tokens(input_ids) - - bsz, seq_len, n_embd = inputs_embeds.shape + if input_ids is not None: + inputs_embeds = self.model.embed_tokens(input_ids) + bsz, seq_len, n_embd = inputs_embeds.shape + else: + inputs_embeds = None + bsz = images.shape[0] if isinstance(images, torch.Tensor) else len(images) + seq_len = 0 + n_embd = self.config.hidden_size # Instantiate placeholder tokens: , for the gen image if mode == "gen_text": @@ -1232,6 +1252,7 @@ class HunyuanImage3Pipeline( token_h, token_w = None, None else: if first_step: + assert inputs_embeds is not None inputs_embeds, token_h, token_w = self.instantiate_vae_image_tokens( inputs_embeds, images, timestep, image_mask ) @@ -1253,6 +1274,8 @@ class HunyuanImage3Pipeline( inputs_embeds = self.instantiate_vit_image_tokens( inputs_embeds, cond_vit_images, cond_vit_image_mask, vit_kwargs ) + assert inputs_embeds is not None + bsz, seq_len, n_embd = inputs_embeds.shape # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) from vllm.forward_context import set_forward_context @@ -1291,7 +1314,7 @@ class HunyuanImage3Pipeline( diffusion_prediction = None else: logits = None - hidden_states = hidden_states.to(input_ids.device) + hidden_states = hidden_states.to(inputs_embeds.device) assert hidden_states.numel() == bsz * seq_len * n_embd, ( f"Shape mismatch: {hidden_states.shape} cannot reshape to ({bsz}, {seq_len}, {n_embd})" ) diff --git a/vllm_omni/diffusion/models/hunyuan_image3/prompt_utils.py b/vllm_omni/diffusion/models/hunyuan_image3/prompt_utils.py index b178b021f..f054fef26 100644 --- a/vllm_omni/diffusion/models/hunyuan_image3/prompt_utils.py +++ b/vllm_omni/diffusion/models/hunyuan_image3/prompt_utils.py @@ -219,7 +219,7 @@ def build_prompt( effective_sys_type = sys_type or preset_sys_type system_prompt = get_system_prompt(effective_sys_type, bot_task, custom_system_prompt) - sys_text = system_prompt or "" + sys_text = system_prompt.strip() if system_prompt is not None else "" has_image_input = task in ("i2t", "it2i") if has_image_input: @@ -288,7 +288,7 @@ def build_prompt_tokens( ) system_prompt = get_system_prompt(effective_sys_type, bot_task, custom_system_prompt) - sys_text = system_prompt or "" + sys_text = system_prompt.strip() if system_prompt is not None else "" ids: list[int] = [bos_id] if sys_text: From 9abf9545df269177e982774e48676c66822e36b7 Mon Sep 17 00:00:00 2001 From: Aleksi Vesanto Date: Wed, 27 May 2026 14:33:17 +0300 Subject: [PATCH 045/112] [ROCm] Add support for AITER GroupNorm (#3419) Signed-off-by: Aleksi Vesanto --- vllm_omni/platforms/rocm/patch/__init__.py | 6 ++ .../platforms/rocm/patch/worker/__init__.py | 3 + .../rocm/patch/worker/patch_groupnorm.py | 56 +++++++++++++++++++ vllm_omni/platforms/rocm/platform.py | 5 ++ 4 files changed, 70 insertions(+) create mode 100644 vllm_omni/platforms/rocm/patch/__init__.py create mode 100644 vllm_omni/platforms/rocm/patch/worker/__init__.py create mode 100644 vllm_omni/platforms/rocm/patch/worker/patch_groupnorm.py diff --git a/vllm_omni/platforms/rocm/patch/__init__.py b/vllm_omni/platforms/rocm/patch/__init__.py new file mode 100644 index 000000000..ecf416ee3 --- /dev/null +++ b/vllm_omni/platforms/rocm/patch/__init__.py @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 + + +def apply_patches(): + """Apply all ROCm-specific patches.""" + from vllm_omni.platforms.rocm.patch import worker # noqa: F401 diff --git a/vllm_omni/platforms/rocm/patch/worker/__init__.py b/vllm_omni/platforms/rocm/patch/worker/__init__.py new file mode 100644 index 000000000..824d33c5b --- /dev/null +++ b/vllm_omni/platforms/rocm/patch/worker/__init__.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 + +from vllm_omni.platforms.rocm.patch.worker import patch_groupnorm # noqa: F401 diff --git a/vllm_omni/platforms/rocm/patch/worker/patch_groupnorm.py b/vllm_omni/platforms/rocm/patch/worker/patch_groupnorm.py new file mode 100644 index 000000000..7de866e73 --- /dev/null +++ b/vllm_omni/platforms/rocm/patch/worker/patch_groupnorm.py @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Patch ``initialize_model`` to replace VAE GroupNorm with AITER GroupNorm on ROCm.""" + +import torch.nn as nn +from vllm.logger import init_logger + +import vllm_omni.diffusion.registry as _registry_mod + +logger = init_logger(__name__) + +_original_initialize_model = _registry_mod.initialize_model + + +def _replace_groupnorm_with_aiter(vae: nn.Module) -> bool: + from aiter.ops.groupnorm import GroupNorm as AiterGroupNorm + + targets = [ + (parent, name, child) + for parent in vae.modules() + for name, child in parent.named_children() + if isinstance(child, nn.GroupNorm) and child.affine + ] + + for parent, name, child in targets: + new_group_norm = AiterGroupNorm( + num_groups=child.num_groups, + num_channels=child.num_channels, + eps=child.eps, + affine=True, + device=child.weight.device, + dtype=child.weight.dtype, + ) + new_group_norm.weight = child.weight + new_group_norm.bias = child.bias + setattr(parent, name, new_group_norm) + + return len(targets) > 0 + + +def _patched_initialize_model(od_config): + model = _original_initialize_model(od_config) + + if hasattr(model, "vae"): + try: + from vllm._aiter_ops import is_aiter_found_and_supported + + if is_aiter_found_and_supported() and _replace_groupnorm_with_aiter(model.vae): + logger.info("AITER GroupNorm is enabled for VAE.") + except Exception: + logger.warning("Failed to apply AITER GroupNorm to VAE.") + + return model + + +_registry_mod.initialize_model = _patched_initialize_model diff --git a/vllm_omni/platforms/rocm/platform.py b/vllm_omni/platforms/rocm/platform.py index d7ad2b0f7..c2e3fae67 100644 --- a/vllm_omni/platforms/rocm/platform.py +++ b/vllm_omni/platforms/rocm/platform.py @@ -10,6 +10,7 @@ from vllm.platforms.rocm import RocmPlatform from vllm_omni.diffusion.attention.backends.registry import DiffusionAttentionBackendEnum from vllm_omni.platforms.interface import OmniPlatform, OmniPlatformEnum +from vllm_omni.platforms.rocm.patch import apply_patches logger = init_logger(__name__) @@ -51,6 +52,10 @@ class RocmOmniPlatform(OmniPlatform, RocmPlatform): _omni_enum = OmniPlatformEnum.ROCM + def __init__(self): + super().__init__() + apply_patches() + @classmethod def get_omni_ar_worker_cls(cls) -> str: return "vllm_omni.worker.gpu_ar_worker.GPUARWorker" From 36dc32ff810d337de67f427d828a0573174cd4de Mon Sep 17 00:00:00 2001 From: JohnJan Date: Wed, 27 May 2026 20:43:02 +0800 Subject: [PATCH 046/112] [Feature] support SP for FLUX.2-dev (#3244) --- docs/user_guide/diffusion_features.md | 2 +- .../test_flux_2_dev_expansion.py | 28 +- .../models/flux2/flux2_transformer.py | 291 +++++++++++++++--- .../diffusion/models/flux2/pipeline_flux2.py | 4 +- 4 files changed, 277 insertions(+), 48 deletions(-) diff --git a/docs/user_guide/diffusion_features.md b/docs/user_guide/diffusion_features.md index 2a296a059..52c24ab9e 100644 --- a/docs/user_guide/diffusion_features.md +++ b/docs/user_guide/diffusion_features.md @@ -113,7 +113,7 @@ The following tables show which models support each feature: | **FLUX.1-schnell** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | | **FLUX.2-klein** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | | **FLUX.1-Kontext-dev** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | -| **FLUX.2-dev** | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| **FLUX.2-dev** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | | **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | | **Hidream-I1-Full** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | diff --git a/tests/e2e/online_serving/test_flux_2_dev_expansion.py b/tests/e2e/online_serving/test_flux_2_dev_expansion.py index 953cb448a..aaf118112 100644 --- a/tests/e2e/online_serving/test_flux_2_dev_expansion.py +++ b/tests/e2e/online_serving/test_flux_2_dev_expansion.py @@ -3,6 +3,8 @@ End-to-end diffusion coverage for FLUX.2-dev in online serving mode. Coverage: - CPU offload +- Ulysses sequence parallelism +- Ring sequence parallelism This test verifies that FLUX.2-dev can be launched with CPU offload enabled, accepts text-to-image requests through the OpenAI-compatible API, and returns @@ -28,7 +30,7 @@ PARALLEL_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"}, num_cards=2) def _get_flux_2_dev_feature_cases(model: str): - """Return FLUX.2-dev diffusion feature cases for CPU offload.""" + """Return FLUX.2-dev diffusion feature cases for CPU offload and SP.""" return [ pytest.param( @@ -53,6 +55,30 @@ def _get_flux_2_dev_feature_cases(model: str): id="parallel_cfg_2", marks=PARALLEL_FEATURE_MARKS, ), + pytest.param( + OmniServerParams( + model=model, + server_args=[ + "--enable-cpu-offload", + "--ulysses-degree", + "2", + ], + ), + id="ulysses_2", + marks=PARALLEL_FEATURE_MARKS, + ), + pytest.param( + OmniServerParams( + model=model, + server_args=[ + "--enable-cpu-offload", + "--ring-degree", + "2", + ], + ), + id="ring_2", + marks=PARALLEL_FEATURE_MARKS, + ), ] diff --git a/vllm_omni/diffusion/models/flux2/flux2_transformer.py b/vllm_omni/diffusion/models/flux2/flux2_transformer.py index ef2cdd025..0c6896c13 100644 --- a/vllm_omni/diffusion/models/flux2/flux2_transformer.py +++ b/vllm_omni/diffusion/models/flux2/flux2_transformer.py @@ -25,7 +25,13 @@ from vllm.model_executor.model_loader.weight_utils import default_weight_loader from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata from vllm_omni.diffusion.attention.layer import Attention -from vllm_omni.diffusion.layers.rope import RotaryEmbedding +from vllm_omni.diffusion.data import DiffusionParallelConfig, OmniDiffusionConfig +from vllm_omni.diffusion.distributed.sp_plan import ( + SequenceParallelInput, + SequenceParallelOutput, +) +from vllm_omni.diffusion.forward_context import get_forward_context, is_forward_context_available +from vllm_omni.diffusion.layers.rope import RotaryEmbedding, apply_rope_to_qk if TYPE_CHECKING: from vllm.model_executor.layers.quantization.base_config import QuantizationConfig @@ -91,6 +97,7 @@ class Flux2FeedForward(nn.Module): class Flux2Attention(nn.Module): def __init__( self, + parallel_config: DiffusionParallelConfig, query_dim: int, heads: int = 8, dim_head: int = 64, @@ -106,6 +113,7 @@ class Flux2Attention(nn.Module): prefix: str = "", ): super().__init__() + self.parallel_config = parallel_config self.head_dim = dim_head self.inner_dim = out_dim if out_dim is not None else dim_head * heads self.query_dim = query_dim @@ -206,34 +214,82 @@ class Flux2Attention(nn.Module): encoder_query = self.norm_added_q(encoder_query) encoder_key = self.norm_added_k(encoder_key) - query = torch.cat([encoder_query, query], dim=1) - key = torch.cat([encoder_key, key], dim=1) - value = torch.cat([encoder_value, value], dim=1) - - if image_rotary_emb is not None: - cos, sin = image_rotary_emb - cos = cos.to(query.dtype) - sin = sin.to(query.dtype) - query = self.rope(query, cos, sin) - key = self.rope(key, cos, sin) - - attn_metadata = None - if attention_mask is not None: - if attention_mask.dim() == 3: - attention_mask = attention_mask.unsqueeze(1) - attn_metadata = AttentionMetadata(attn_mask=attention_mask) - - hidden_states = self.attn(query, key, value, attn_metadata) - hidden_states = hidden_states.flatten(2, 3).to(query.dtype) - - if encoder_hidden_states is not None: - context_len = encoder_hidden_states.shape[1] - encoder_hidden_states, hidden_states = hidden_states.split_with_sizes( - [context_len, hidden_states.shape[1] - context_len], - dim=1, + sp_size = self.parallel_config.sequence_parallel_size + use_sp_joint_attention = ( + sp_size is not None + and sp_size > 1 + and is_forward_context_available() + and not get_forward_context().split_text_embed_in_sp ) - # Contiguous for FP8 quantization in RowParallelLinear - encoder_hidden_states = self.to_add_out(encoder_hidden_states.contiguous()) + + if use_sp_joint_attention and image_rotary_emb is not None: + cos, sin = image_rotary_emb + cos = cos.to(query.dtype) + sin = sin.to(query.dtype) + txt_len = encoder_query.shape[1] + txt_cos, img_cos = cos[:txt_len], cos[txt_len:] + txt_sin, img_sin = sin[:txt_len], sin[txt_len:] + + query = self.rope(query, img_cos, img_sin) + key = self.rope(key, img_cos, img_sin) + encoder_query = self.rope(encoder_query, txt_cos, txt_sin) + encoder_key = self.rope(encoder_key, txt_cos, txt_sin) + + attn_metadata = AttentionMetadata( + joint_query=encoder_query, + joint_key=encoder_key, + joint_value=encoder_value, + joint_strategy="front", + ) + hidden_states_mask: torch.Tensor | None = kwargs.get("hidden_states_mask", None) + encoder_hidden_states_mask: torch.Tensor | None = kwargs.get("encoder_hidden_states_mask", None) + if hidden_states_mask is not None: + attn_metadata.attn_mask = hidden_states_mask + if encoder_hidden_states_mask is not None: + attn_metadata.joint_attn_mask = encoder_hidden_states_mask + + hidden_states = self.attn(query, key, value, attn_metadata) + hidden_states = hidden_states.flatten(2, 3).to(query.dtype) + + txt_len = encoder_hidden_states.shape[1] + encoder_hidden_states, hidden_states = hidden_states.split_with_sizes( + [txt_len, hidden_states.shape[1] - txt_len], + dim=1, + ) + encoder_hidden_states = self.to_add_out(encoder_hidden_states.contiguous()) + else: + query = torch.cat([encoder_query, query], dim=1) + key = torch.cat([encoder_key, key], dim=1) + value = torch.cat([encoder_value, value], dim=1) + + query, key = apply_rope_to_qk(self.rope, query, key, image_rotary_emb) + + attn_metadata = None + if attention_mask is not None: + if attention_mask.dim() == 3: + attention_mask = attention_mask.unsqueeze(1) + attn_metadata = AttentionMetadata(attn_mask=attention_mask) + + hidden_states = self.attn(query, key, value, attn_metadata) + hidden_states = hidden_states.flatten(2, 3).to(query.dtype) + + context_len = encoder_hidden_states.shape[1] + encoder_hidden_states, hidden_states = hidden_states.split_with_sizes( + [context_len, hidden_states.shape[1] - context_len], + dim=1, + ) + encoder_hidden_states = self.to_add_out(encoder_hidden_states.contiguous()) + else: + query, key = apply_rope_to_qk(self.rope, query, key, image_rotary_emb) + + attn_metadata = None + if attention_mask is not None: + if attention_mask.dim() == 3: + attention_mask = attention_mask.unsqueeze(1) + attn_metadata = AttentionMetadata(attn_mask=attention_mask) + + hidden_states = self.attn(query, key, value, attn_metadata) + hidden_states = hidden_states.flatten(2, 3).to(query.dtype) hidden_states = self.to_out[0](hidden_states.contiguous()) hidden_states = self.to_out[1](hidden_states) @@ -250,6 +306,7 @@ class Flux2ParallelSelfAttention(nn.Module): def __init__( self, + parallel_config: DiffusionParallelConfig, query_dim: int, heads: int = 8, dim_head: int = 64, @@ -265,6 +322,7 @@ class Flux2ParallelSelfAttention(nn.Module): prefix: str = "", ): super().__init__() + self.parallel_config = parallel_config self.head_dim = dim_head self.inner_dim = out_dim if out_dim is not None else dim_head * heads self.query_dim = query_dim @@ -327,20 +385,59 @@ class Flux2ParallelSelfAttention(nn.Module): query = self.norm_q(query) key = self.norm_k(key) - if image_rotary_emb is not None: + sp_size = self.parallel_config.sequence_parallel_size + text_seq_len = kwargs.get("text_seq_len", None) + use_sp_single_stream = ( + sp_size is not None + and sp_size > 1 + and text_seq_len is not None + and is_forward_context_available() + and not get_forward_context().split_text_embed_in_sp + ) + + if use_sp_single_stream and image_rotary_emb is not None: cos, sin = image_rotary_emb cos = cos.to(query.dtype) sin = sin.to(query.dtype) - query = self.rope(query, cos, sin) - key = self.rope(key, cos, sin) + txt_cos, img_cos = cos[:text_seq_len], cos[text_seq_len:] + txt_sin, img_sin = sin[:text_seq_len], sin[text_seq_len:] - attn_metadata = None - if attention_mask is not None: - if attention_mask.dim() == 3: - attention_mask = attention_mask.unsqueeze(1) - attn_metadata = AttentionMetadata(attn_mask=attention_mask) + img_query = query[:, text_seq_len:] + img_key = key[:, text_seq_len:] + img_value = value[:, text_seq_len:] + text_query = query[:, :text_seq_len] + text_key = key[:, :text_seq_len] + text_value = value[:, :text_seq_len] - attn_output = self.attn(query, key, value, attn_metadata) + img_query = self.rope(img_query, img_cos, img_sin) + img_key = self.rope(img_key, img_cos, img_sin) + text_query = self.rope(text_query, txt_cos, txt_sin) + text_key = self.rope(text_key, txt_cos, txt_sin) + + attn_metadata = AttentionMetadata( + joint_query=text_query, + joint_key=text_key, + joint_value=text_value, + joint_strategy="front", + ) + hidden_states_mask: torch.Tensor | None = kwargs.get("hidden_states_mask", None) + encoder_hidden_states_mask: torch.Tensor | None = kwargs.get("encoder_hidden_states_mask", None) + if hidden_states_mask is not None: + attn_metadata.attn_mask = hidden_states_mask + if encoder_hidden_states_mask is not None: + attn_metadata.joint_attn_mask = encoder_hidden_states_mask + + attn_output = self.attn(img_query, img_key, img_value, attn_metadata) + else: + query, key = apply_rope_to_qk(self.rope, query, key, image_rotary_emb) + + attn_metadata = None + if attention_mask is not None: + if attention_mask.dim() == 3: + attention_mask = attention_mask.unsqueeze(1) + attn_metadata = AttentionMetadata(attn_mask=attention_mask) + + attn_output = self.attn(query, key, value, attn_metadata) attn_output = attn_output.flatten(2, 3).to(query.dtype) mlp_hidden_states = self.mlp_act_fn(mlp_hidden_states) @@ -352,6 +449,7 @@ class Flux2ParallelSelfAttention(nn.Module): class Flux2SingleTransformerBlock(nn.Module): def __init__( self, + parallel_config: DiffusionParallelConfig, dim: int, num_attention_heads: int, attention_head_dim: int, @@ -364,6 +462,7 @@ class Flux2SingleTransformerBlock(nn.Module): super().__init__() self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=eps) self.attn = Flux2ParallelSelfAttention( + parallel_config=parallel_config, query_dim=dim, dim_head=attention_head_dim, heads=num_attention_heads, @@ -387,6 +486,13 @@ class Flux2SingleTransformerBlock(nn.Module): split_hidden_states: bool = False, text_seq_len: int | None = None, ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]: + """ + Forward pass for Flux2SingleTransformerBlock with SP support. + + In SP mode: image hidden_states is chunked (B, img_len/SP, D), + text encoder_hidden_states is full (B, txt_len, D). + The block concatenates them for joint attention. + """ if encoder_hidden_states is not None: text_seq_len = encoder_hidden_states.shape[1] hidden_states = torch.cat([encoder_hidden_states, hidden_states], dim=1) @@ -400,6 +506,7 @@ class Flux2SingleTransformerBlock(nn.Module): attn_output = self.attn( hidden_states=norm_hidden_states, image_rotary_emb=image_rotary_emb, + text_seq_len=text_seq_len, **joint_attention_kwargs, ) @@ -416,6 +523,7 @@ class Flux2SingleTransformerBlock(nn.Module): class Flux2TransformerBlock(nn.Module): def __init__( self, + parallel_config: DiffusionParallelConfig, dim: int, num_attention_heads: int, attention_head_dim: int, @@ -432,6 +540,7 @@ class Flux2TransformerBlock(nn.Module): self.norm1_context = nn.LayerNorm(dim, elementwise_affine=False, eps=eps) self.attn = Flux2Attention( + parallel_config=parallel_config, query_dim=dim, added_kv_proj_dim=dim, dim_head=attention_head_dim, @@ -543,6 +652,47 @@ class Flux2PosEmbed(nn.Module): return freqs_cos, freqs_sin +class Flux2RopePrepare(nn.Module): + """Prepares RoPE embeddings for sequence parallel. + + This module encapsulates the RoPE computation for Flux.2-dev. + For dual-stream attention, text components (outputs 0, 1) are replicated + across SP ranks, while image components (outputs 2, 3) are sharded. + + NOTE: The hidden_states projection is handled separately in forward() + so that _sp_plan can shard it at the root level. + """ + + def __init__(self, pos_embed: Flux2PosEmbed): + super().__init__() + self.pos_embed = pos_embed + + def forward( + self, + img_ids: torch.Tensor, + txt_ids: torch.Tensor, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Compute RoPE embeddings for text and image sequences. + + Args: + img_ids: Image position IDs (img_seq_len, n_axes) + txt_ids: Text position IDs (txt_seq_len, n_axes) + + Returns: + Tuple of cosine / sine components for text & image + in the order: (txt_cos, txt_sin, img_cos, img_sin) + + NOTE: careful about output orders if this is refactored in the + future; we need to match the _sp_plan indices, since text + components (0 & 1) need to be replicated across SP ranks, + while image components (2 & 3) must be sharded. + """ + img_freqs_cos, img_freqs_sin = self.pos_embed(img_ids) + txt_freqs_cos, txt_freqs_sin = self.pos_embed(txt_ids) + return txt_freqs_cos, txt_freqs_sin, img_freqs_cos, img_freqs_sin + + class Flux2TimestepGuidanceEmbeddings(nn.Module): def __init__( self, @@ -601,9 +751,22 @@ class Flux2Modulation(nn.Module): class Flux2Transformer2DModel(nn.Module): """ The Transformer model introduced in Flux 2. + + Supports Sequence Parallelism (Ulysses and Ring) when configured via OmniDiffusionConfig. """ _repeated_blocks = ["Flux2TransformerBlock", "Flux2SingleTransformerBlock"] + _sp_plan = { + "": { + "hidden_states": SequenceParallelInput(split_dim=1, expected_dims=3, auto_pad=True), + }, + "rope_prepare": { + 2: SequenceParallelInput(split_dim=0, expected_dims=2, split_output=True, auto_pad=True), + 3: SequenceParallelInput(split_dim=0, expected_dims=2, split_output=True, auto_pad=True), + }, + "proj_out": SequenceParallelOutput(gather_dim=1, expected_dims=3), + } + """SP plan: shard hidden_states at root level, shard img_freqs at rope_prepare, gather output at proj_out.""" @staticmethod def _is_transformer_block(name: str, module) -> bool: @@ -628,6 +791,7 @@ class Flux2Transformer2DModel(nn.Module): eps: float = 1e-6, guidance_embeds: bool = True, quant_config: "QuantizationConfig | None" = None, + od_config: OmniDiffusionConfig | None = None, ): super().__init__() self.guidance_embeds = guidance_embeds @@ -652,7 +816,13 @@ class Flux2Transformer2DModel(nn.Module): guidance_embeds=guidance_embeds, ) + if od_config is not None: + self.parallel_config = od_config.parallel_config + else: + self.parallel_config = DiffusionParallelConfig() + self.pos_embed = Flux2PosEmbed(theta=rope_theta, axes_dim=axes_dims_rope) + self.rope_prepare = Flux2RopePrepare(self.pos_embed) self.time_guidance_embed = Flux2TimestepGuidanceEmbeddings( in_channels=timestep_guidance_channels, embedding_dim=self.inner_dim, @@ -670,6 +840,7 @@ class Flux2Transformer2DModel(nn.Module): self.transformer_blocks = nn.ModuleList( [ Flux2TransformerBlock( + parallel_config=self.parallel_config, dim=self.inner_dim, num_attention_heads=num_attention_heads, attention_head_dim=attention_head_dim, @@ -686,6 +857,7 @@ class Flux2Transformer2DModel(nn.Module): self.single_transformer_blocks = nn.ModuleList( [ Flux2SingleTransformerBlock( + parallel_config=self.parallel_config, dim=self.inner_dim, num_attention_heads=num_attention_heads, attention_head_dim=attention_head_dim, @@ -722,6 +894,9 @@ class Flux2Transformer2DModel(nn.Module): joint_attention_kwargs = joint_attention_kwargs or {} num_txt_tokens = encoder_hidden_states.shape[1] + sp_size = self.parallel_config.sequence_parallel_size + if sp_size and sp_size > 1 and is_forward_context_available(): + get_forward_context().split_text_embed_in_sp = False timestep = timestep.to(hidden_states.dtype) * 1000 if guidance is not None: @@ -742,18 +917,43 @@ class Flux2Transformer2DModel(nn.Module): txt_ids = txt_ids[0] if is_torch_npu_available(): - freqs_cos_image, freqs_sin_image = self.pos_embed(img_ids.cpu()) - image_rotary_emb = (freqs_cos_image.npu(), freqs_sin_image.npu()) - freqs_cos_text, freqs_sin_text = self.pos_embed(txt_ids.cpu()) - text_rotary_emb = (freqs_cos_text.npu(), freqs_sin_text.npu()) + txt_freqs_cos, txt_freqs_sin, img_freqs_cos, img_freqs_sin = self.rope_prepare(img_ids.cpu(), txt_ids.cpu()) + txt_freqs_cos = txt_freqs_cos.npu() + txt_freqs_sin = txt_freqs_sin.npu() + img_freqs_cos = img_freqs_cos.npu() + img_freqs_sin = img_freqs_sin.npu() else: - image_rotary_emb = self.pos_embed(img_ids) - text_rotary_emb = self.pos_embed(txt_ids) + txt_freqs_cos, txt_freqs_sin, img_freqs_cos, img_freqs_sin = self.rope_prepare(img_ids, txt_ids) concat_rotary_emb = ( - torch.cat([text_rotary_emb[0], image_rotary_emb[0]], dim=0), - torch.cat([text_rotary_emb[1], image_rotary_emb[1]], dim=0), + torch.cat([txt_freqs_cos, img_freqs_cos], dim=0), + torch.cat([txt_freqs_sin, img_freqs_sin], dim=0), ) + # Create separate masks for image and text portions for Ulysses SP joint attention + hidden_states_mask = None + encoder_hidden_states_mask = None + if is_forward_context_available(): + ctx = get_forward_context() + else: + ctx = None + if ctx is not None and ctx.sp_original_seq_len is not None and ctx.sp_padding_size > 0: + batch_size = hidden_states.shape[0] + img_padded_seq_len = ctx.sp_original_seq_len + ctx.sp_padding_size + hidden_states_mask = torch.ones( + batch_size, + img_padded_seq_len, + dtype=torch.bool, + device=hidden_states.device, + ) + hidden_states_mask[:, ctx.sp_original_seq_len :] = False + if hidden_states_mask.all(): + hidden_states_mask = None + + if hidden_states_mask is not None: + joint_attention_kwargs["hidden_states_mask"] = hidden_states_mask + if encoder_hidden_states_mask is not None: + joint_attention_kwargs["encoder_hidden_states_mask"] = encoder_hidden_states_mask + for index_block, block in enumerate(self.transformer_blocks): encoder_hidden_states, hidden_states = block( hidden_states=hidden_states, @@ -773,6 +973,7 @@ class Flux2Transformer2DModel(nn.Module): temb_mod_params=single_stream_mod, image_rotary_emb=concat_rotary_emb, joint_attention_kwargs=joint_attention_kwargs, + text_seq_len=num_txt_tokens, ) hidden_states = hidden_states[:, num_txt_tokens:, ...] diff --git a/vllm_omni/diffusion/models/flux2/pipeline_flux2.py b/vllm_omni/diffusion/models/flux2/pipeline_flux2.py index 8700309e5..948f922d4 100644 --- a/vllm_omni/diffusion/models/flux2/pipeline_flux2.py +++ b/vllm_omni/diffusion/models/flux2/pipeline_flux2.py @@ -398,7 +398,9 @@ class Flux2Pipeline(nn.Module, CFGParallelMixin, SupportImageInput, ProgressBarM self._execution_device ) transformer_kwargs = get_transformer_config_kwargs(od_config.tf_model_config, Flux2Transformer2DModel) - self.transformer = Flux2Transformer2DModel(quant_config=od_config.quantization_config, **transformer_kwargs) + self.transformer = Flux2Transformer2DModel( + quant_config=od_config.quantization_config, od_config=od_config, **transformer_kwargs + ) self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) if getattr(self, "vae", None) else 8 self.image_processor = Flux2ImageProcessor(vae_scale_factor=self.vae_scale_factor * 2) From a698269e4712ee31d78c0440aa5a032a93dd94d7 Mon Sep 17 00:00:00 2001 From: Zheng Wengang Date: Wed, 27 May 2026 23:21:20 +0800 Subject: [PATCH 047/112] [Model] Add Ming-flash-omni-2.0 Image Generation (Diffusion) Stage (#2875) Signed-off-by: ZhengWG Signed-off-by: Zheng Wengang Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Yuanheng Zhao <54058983+yuanheng-zhao@users.noreply.github.com> --- docs/models/supported_models.md | 1 + .../ming_flash_omni/README.md | 14 +- .../online_serving/ming_flash_omni/README.md | 92 ++++ .../run_curl_multimodal_generation.sh | 42 +- pyproject.toml | 2 + vllm_omni/diffusion/data.py | 11 + vllm_omni/diffusion/forward_context.py | 17 + .../models/ming_flash_omni/__init__.py | 20 + .../models/ming_flash_omni/byte5_encoder.py | 223 ++++++++ .../ming_flash_omni/condition_encoder.py | 373 +++++++++++++ .../ming_zimage_transformer.py | 50 ++ .../ming_flash_omni/pipeline_ming_imagegen.py | 520 ++++++++++++++++++ .../models/ming_flash_omni/t5_block_mapper.py | 129 +++++ .../models/z_image/pipeline_z_image.py | 5 +- vllm_omni/diffusion/registry.py | 6 + vllm_omni/engine/cfg_companion_tracker.py | 22 +- vllm_omni/engine/orchestrator.py | 29 +- vllm_omni/entrypoints/openai/serving_chat.py | 51 +- .../models/ming_flash_omni/ming_flash_omni.py | 52 +- .../ming_flash_omni_thinker.py | 294 +++++++++- .../models/ming_flash_omni/prompt_utils.py | 86 ++- .../stage_configs/ming_flash_omni_dual.yaml | 98 ++++ .../stage_input_processors/ming_flash_omni.py | 449 ++++++++++++++- .../transformers_utils/configs/__init__.py | 2 + .../configs/ming_flash_omni.py | 68 ++- 25 files changed, 2614 insertions(+), 42 deletions(-) create mode 100644 vllm_omni/diffusion/models/ming_flash_omni/__init__.py create mode 100644 vllm_omni/diffusion/models/ming_flash_omni/byte5_encoder.py create mode 100644 vllm_omni/diffusion/models/ming_flash_omni/condition_encoder.py create mode 100644 vllm_omni/diffusion/models/ming_flash_omni/ming_zimage_transformer.py create mode 100644 vllm_omni/diffusion/models/ming_flash_omni/pipeline_ming_imagegen.py create mode 100644 vllm_omni/diffusion/models/ming_flash_omni/t5_block_mapper.py create mode 100644 vllm_omni/model_executor/stage_configs/ming_flash_omni_dual.yaml diff --git a/docs/models/supported_models.md b/docs/models/supported_models.md index d26a579ed..03a9a5789 100644 --- a/docs/models/supported_models.md +++ b/docs/models/supported_models.md @@ -19,6 +19,7 @@ th { |--------------|--------|-------------------|------------|---------|-----|-----------| | `Qwen3OmniMoeForConditionalGeneration` | Qwen3-Omni | `Qwen/Qwen3-Omni-30B-A3B-Instruct` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | | `Qwen2_5OmniForConditionalGeneration` | Qwen2.5-Omni | `Qwen/Qwen2.5-Omni-7B`, `Qwen/Qwen2.5-Omni-3B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | +| `MingFlashOmniForConditionalGeneration` + `MingImagePipeline` | Ming-flash-omni-2.0 (omni-speech + imagegen1) | `Jonathan1909/Ming-flash-omni-2.0` | ✅︎ | | | | | `BagelForConditionalGeneration` | BAGEL (DiT-only) | `ByteDance-Seed/BAGEL-7B-MoT` | ✅︎ | ✅︎ | | ✅︎ | | `InternVLAA1Pipeline` | InternVLA-A1 | `InternRobotics/InternVLA-A1-3B` | ✅︎ | ✅︎ | | | | `HunyuanImage3ForCausalMM` | HunyuanImage3.0 (DiT-only) | `tencent/HunyuanImage-3.0`, `tencent/HunyuanImage-3.0-Instruct` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | diff --git a/examples/offline_inference/ming_flash_omni/README.md b/examples/offline_inference/ming_flash_omni/README.md index 4b798619e..83259a021 100644 --- a/examples/offline_inference/ming_flash_omni/README.md +++ b/examples/offline_inference/ming_flash_omni/README.md @@ -2,12 +2,13 @@ [Ming-flash-omni-2.0](https://github.com/inclusionAI/Ming) is an omni-modal model supporting text, image, video, and audio understanding, with text and speech outputs. -vLLM-Omni supports two deployment modes: +vLLM-Omni supports three deployment modes: | Mode | Deploy config | Output | |------|--------------|--------| | Thinker + Talker (omni-speech, default) | `vllm_omni/deploy/ming_flash_omni.yaml` | Text + Audio | | Thinker only (multimodal understanding) | `vllm_omni/deploy/ming_flash_omni_thinker_only.yaml` | Text | +| Thinker + Imagegen (text-to-image / img2img) | `vllm_omni/model_executor/stage_configs/ming_flash_omni_dual.yaml` | Image (online-serving only at the moment) | For standalone TTS (talker only), see the [Ming-flash-omni-TTS section in the Text-To-Speech hub](../text_to_speech/README.md#ming-flash-omni-tts). @@ -87,6 +88,17 @@ The default deploy YAML allocates thinker on GPUs 0–3 and talker on GPU 3 for Pass `--deploy-config /path/to/your_deploy.yaml` to any of the commands above to override the bundled deploy config. +### Image generation (text-to-image / img2img) + +The diffusion-stage image-generation path is currently only wired through +the online OpenAI-compatible chat endpoint (`/v1/chat/completions` with +`"modalities": ["image"]`). For payloads, optional knobs +(`steps`/`cfg`/`seed`/`byte5_text`/`negative_prompt`), and the img2img +reference-image flow, see the +[image-gen section in the online-serving README](../../online_serving/ming_flash_omni/README.md#image-generation-text-to-image). +`end2end.py` does not yet exercise the imagegen stage; that is tracked as +follow-up work. + ## Online serving For online serving via the OpenAI-compatible API, see [examples/online_serving/ming_flash_omni/README.md](../../online_serving/ming_flash_omni/README.md). diff --git a/examples/online_serving/ming_flash_omni/README.md b/examples/online_serving/ming_flash_omni/README.md index 533a30e1c..7b538efe8 100644 --- a/examples/online_serving/ming_flash_omni/README.md +++ b/examples/online_serving/ming_flash_omni/README.md @@ -10,6 +10,7 @@ Please refer to [README.md](../../../README.md) |------|---------------|--------| | Thinker + Talker (omni-speech, default) | `vllm serve ... --omni` | Text + Audio | | Thinker only (multimodal understanding) | `vllm serve ... --omni --deploy-config vllm_omni/deploy/ming_flash_omni_thinker_only.yaml` | Text | +| Thinker + Imagegen (text-to-image / img2img) | `vllm serve ... --omni --stage-configs-path vllm_omni/model_executor/stage_configs/ming_flash_omni_dual.yaml` | Image | For standalone TTS (talker only), see the [Ming-flash-omni-TTS section in the Text-To-Speech hub](../text_to_speech/README.md#ming-flash-omni-tts). @@ -56,8 +57,98 @@ bash run_curl_multimodal_generation.sh use_image bash run_curl_multimodal_generation.sh use_audio bash run_curl_multimodal_generation.sh use_video bash run_curl_multimodal_generation.sh use_mixed_modalities +bash run_curl_multimodal_generation.sh use_image_gen ``` +## Image generation (text-to-image) + +Ming-flash-omni-2.0 also exposes an image-generation (diffusion) stage. +Launch with the dual-stage YAML, which adds an image-gen stage behind the +thinker: + +```bash +vllm serve Jonathan1909/Ming-flash-omni-2.0 --omni \ + --stage-configs-path vllm_omni/model_executor/stage_configs/ming_flash_omni_dual.yaml \ + --stage-init-timeout 1800 \ + --port 8188 +``` + +Then request image output by passing `"modalities": ["image"]`: + +```bash +curl http://127.0.0.1:8188/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "Jonathan1909/Ming-flash-omni-2.0", + "messages": [{"role": "user", "content": "Draw a beautiful girl with short black hair and red dress."}], + "modalities": ["image"] + }' -o /tmp/ming_response.json + +python - <<'PY' +import base64, json +r = json.load(open('/tmp/ming_response.json')) +url = r['choices'][0]['message']['content'][0]['image_url']['url'] +open('/tmp/ming_gen.png', 'wb').write(base64.b64decode(url.split(',', 1)[1])) +print('saved /tmp/ming_gen.png') +PY +``` + +### Optional knobs + +Pass image-gen overrides via the diffusion-stage `sampling_params_list[1].extra_args.image_gen`: + +| Key | Default | Description | +| --- | --- | --- | +| `height` / `width` | from config (1024) | Output resolution (multiples of `vae_scale_factor * 2`, currently 16). | +| `steps` | 30 | Number of FlowMatchEuler denoise steps. | +| `cfg` | 2.0 | Classifier-free guidance scale. | +| `seed` | 42 | Per-request RNG seed (deterministic when ByT5 is also seed-stable). | +| `byte5_text` | (auto) | Override the glyph text for ByT5 enhancement; raw strings are auto-wrapped to Ming's `Text "...". ` format. | +| `negative_prompt` | empty | Real CFG negative conditioning (must be set on **stage-0 thinker** `sampling_params` so `expand_cfg_prompts` spawns the companion). | + +Example with all the knobs: + +```bash +curl http://127.0.0.1:8188/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "Jonathan1909/Ming-flash-omni-2.0", + "modalities": ["image"], + "sampling_params_list": [ + {"temperature":0.4,"top_p":0.9,"top_k":1,"max_tokens":1,"seed":42, + "extra_args":{"image_gen":{"negative_prompt":"ugly, blurry, distorted"}}}, + {"seed":42,"extra_args":{"image_gen":{ + "steps":6,"cfg":1.5,"height":512,"width":512,"seed":123, + "byte5_text":["理解与生成统一"]}}} + ], + "messages": [{"role": "user", "content": "Draw a poster."}] + }' -o /tmp/ming_response.json +``` + +### img2img (reference image) + +Add an `image_url` content part to the user message; the parser routes it +into the diffusion stage as `extra[reference_image]`: + +```jsonc +"messages": [{ + "role": "user", + "content": [ + {"type": "text", "text": "Change the background to a sandy beach at sunset."}, + {"type": "image_url", "image_url": {"url": "data:image/png;base64,"}} + ] +}] +``` + +### GPU layout + +The shipped `ming_flash_omni_dual.yaml` allocates the thinker on GPUs 0–3 +(TP=4) and the diffusion stage on GPU 4 (TP=1). Copy the YAML and edit +`runtime.devices` per stage to relocate; with fewer GPUs available, drop +the thinker TP to 2 and run the diffusion stage on a free card. Image-gen +warmup takes roughly an extra 30–60 s on top of the thinker — set +`--stage-init-timeout 1800` if the default 300 s is too tight. + ## Modality control | `modalities` | Server config | Output | @@ -65,6 +156,7 @@ bash run_curl_multimodal_generation.sh use_mixed_modalities | `["text"]` or omitted | Thinker only | Text | | `["audio"]` | Thinker + Talker | Audio (speech) | | `["text", "audio"]` | Thinker + Talker | Text + Audio | +| `["image"]` | Thinker + Imagegen (dual YAML) | Image (PNG, base64 in `choices[0].message.content`) | For ready-to-copy curl examples (text / audio / multimodal input, SSE streaming, reasoning mode), see the recipe at diff --git a/examples/online_serving/ming_flash_omni/run_curl_multimodal_generation.sh b/examples/online_serving/ming_flash_omni/run_curl_multimodal_generation.sh index 715f23edd..3b8f4ddc1 100755 --- a/examples/online_serving/ming_flash_omni/run_curl_multimodal_generation.sh +++ b/examples/online_serving/ming_flash_omni/run_curl_multimodal_generation.sh @@ -7,14 +7,15 @@ PORT="${PORT:-8091}" QUERY_TYPE="${1:-text}" # Validate query type -if [[ ! "$QUERY_TYPE" =~ ^(text|use_audio|use_image|use_video|use_mixed_modalities)$ ]]; then +if [[ ! "$QUERY_TYPE" =~ ^(text|use_audio|use_image|use_video|use_mixed_modalities|use_image_gen)$ ]]; then echo "Error: Invalid query type '$QUERY_TYPE'" - echo "Usage: $0 [text|use_audio|use_image|use_video|use_mixed_modalities]" + echo "Usage: $0 [text|use_audio|use_image|use_video|use_mixed_modalities|use_image_gen]" echo " text: Text-only query" echo " use_audio: Audio + Text query" echo " use_image: Image + Text query" echo " use_video: Video + Text query" echo " use_mixed_modalities: Audio + Image + Video + Text query" + echo " use_image_gen: Text-to-image (diffusion stage) — saves the generated PNG locally" exit 1 fi @@ -100,6 +101,43 @@ esac echo "Running query type: $QUERY_TYPE" echo "" +if [[ "$QUERY_TYPE" == "use_image_gen" ]]; then + # Image-gen branch: dual-stage diffusion endpoint, modalities=["image"]. + # The shipped ming_flash_omni_dual.yaml runs the thinker on cards 0-3 + # (TP=4) and the diffusion stage on card 4 (TP=1). See the image-gen + # section in README.md for knobs and img2img. + out_path="${OUT_PATH:-/tmp/ming_imagegen.png}" + request_body=$(cat <<'EOF' +{ + "model": "Jonathan1909/Ming-flash-omni-2.0", + "modalities": ["image"], + "messages": [ + {"role": "user", "content": "Draw a beautiful girl with short black hair and red dress."} + ] +} +EOF +) + output=$(curl -sS --retry 3 --retry-delay 3 --retry-connrefused \ + -X POST "http://localhost:${PORT}/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d "$request_body") + # Server returns base64-encoded PNG either as a data URL in + # ``choices[0].message.content[0].image_url.url`` or as a raw string. + b64=$(echo "$output" | jq -r '.choices[0].message.content + | if type == "array" + then (map(select(.image_url? != null)) | .[0].image_url.url // "") + elif type == "string" then . else "" end') + if [[ -z "$b64" ]]; then + echo "Error: no image returned. Raw response:" + echo "$output" | jq '.' + exit 1 + fi + if [[ "$b64" == data:image* ]]; then b64="${b64#*,}"; fi + printf '%s' "$b64" | base64 --decode > "$out_path" + echo "Saved generated image: $out_path ($(stat -c%s "$out_path" 2>/dev/null || stat -f%z "$out_path") bytes)" + exit 0 +fi + request_body=$(cat <\\.\\.\\.nd", @@ -261,3 +262,4 @@ Vai = "Vai" tockens = "tockens" CANN = "CANN" ND = "ND" +byt5 = "byt5" diff --git a/vllm_omni/diffusion/data.py b/vllm_omni/diffusion/data.py index a6fe1e4e9..0675c226a 100644 --- a/vllm_omni/diffusion/data.py +++ b/vllm_omni/diffusion/data.py @@ -891,6 +891,17 @@ class OmniDiffusionConfig: self.model_class_name = "SenseNovaU1Pipeline" self.tf_model_config = TransformerConfig() self.update_multimodal_support() + elif "BailingMM2NativeForConditionalGeneration" in architectures or model_type in ( + "bailingmm_moe_v2_lite", + "ming_flash_omni", + "ming_flash_omni_thinker", + ): + # Ming-flash-omni-2.0 — imagegen stage uses the custom + # ``MingImagePipeline`` (ZImage DiT + Qwen2 connector). See + # vllm_omni/diffusion/models/ming_flash_omni/pipeline_ming_imagegen.py. + self.model_class_name = "MingImagePipeline" + self.tf_model_config = TransformerConfig() + self.update_multimodal_support() elif model_type == "nextstep": if self.model_class_name is None: self.model_class_name = "NextStep11Pipeline" diff --git a/vllm_omni/diffusion/forward_context.py b/vllm_omni/diffusion/forward_context.py index 35c93c799..f2b6927d2 100644 --- a/vllm_omni/diffusion/forward_context.py +++ b/vllm_omni/diffusion/forward_context.py @@ -2,6 +2,7 @@ from __future__ import annotations from contextlib import contextmanager from dataclasses import dataclass +from typing import TYPE_CHECKING import vllm.ir from vllm.config import VllmConfig @@ -11,6 +12,9 @@ from vllm_omni.diffusion.attention.backends.abstract import ( ) from vllm_omni.diffusion.data import OmniDiffusionConfig +if TYPE_CHECKING: + import torch + @dataclass class ForwardContext: @@ -23,6 +27,8 @@ class ForwardContext: attn_metadata: dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]] | None = None split_text_embed_in_sp: bool = False denoise_step_idx: int | None = None + # Per-request reference latent for img2img DiT models (e.g. Ming) + ref_latent: torch.Tensor | None = None # whether to split the text embed in sequence parallel, if True, the text embed will be split in sequence parallel # Sequence Parallel padding support @@ -171,3 +177,14 @@ def set_forward_context_denoise_step_idx(step_idx: int | None) -> None: """Set the current diffusion denoise step on the active ForwardContext.""" if _forward_context is not None: _forward_context.denoise_step_idx = step_idx + + +def set_forward_context_ref_latent(ref_latent: torch.Tensor | None) -> None: + """Set the per-request reference latent on the active ForwardContext. + + Used by img2img-capable DiT models (e.g. Ming-flash-omni-2.0) so the + transformer can read the reference latent from request scope instead of + module instance state. + """ + if _forward_context is not None: + _forward_context.ref_latent = ref_latent diff --git a/vllm_omni/diffusion/models/ming_flash_omni/__init__.py b/vllm_omni/diffusion/models/ming_flash_omni/__init__.py new file mode 100644 index 000000000..d10aed623 --- /dev/null +++ b/vllm_omni/diffusion/models/ming_flash_omni/__init__.py @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025 The vLLM-Omni team. + +from .byte5_encoder import MingByT5Encoder +from .condition_encoder import MingConditionEncoder +from .ming_zimage_transformer import MingZImageTransformer2DModel +from .pipeline_ming_imagegen import ( + MingImagePipeline, + get_ming_image_post_process_func, +) +from .t5_block_mapper import T5EncoderBlockByT5Mapper + +__all__ = [ + "MingByT5Encoder", + "MingConditionEncoder", + "MingImagePipeline", + "MingZImageTransformer2DModel", + "T5EncoderBlockByT5Mapper", + "get_ming_image_post_process_func", +] diff --git a/vllm_omni/diffusion/models/ming_flash_omni/byte5_encoder.py b/vllm_omni/diffusion/models/ming_flash_omni/byte5_encoder.py new file mode 100644 index 000000000..e0748abcc --- /dev/null +++ b/vllm_omni/diffusion/models/ming_flash_omni/byte5_encoder.py @@ -0,0 +1,223 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025 The vLLM-Omni team. + +"""ByT5 glyph/text encoder for Ming-flash-omni-2.0 image generation. + +Ported from Ming's ``load_byte5_and_byte5_tokenizer`` + +``T5EncoderBlockByT5Mapper`` + ``get_condition_embeds_for_image_gen`` byte5 +branch. The released checkpoint's byte5 weights were trained with font/color +special tokens, so we replicate that vocabulary extension before loading — +otherwise ``byte5_model.pt`` would shape-mismatch at the embedding layer. + +Typical forward: takes a list of user-supplied prompt strings (possibly with +```` / ```` markers), returns ``[B, byte5_max_length, +diffusion_c_input_dim]`` features ready to be concatenated onto cap_feats +along the sequence dimension. +""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path +from types import SimpleNamespace + +import torch +import torch.nn as nn +from transformers import AutoTokenizer, T5ForConditionalGeneration + +from vllm_omni.diffusion.models.ming_flash_omni.t5_block_mapper import ( + T5EncoderBlockByT5Mapper, +) + +logger = logging.getLogger(__name__) + + +def _add_multilingual_special_tokens( + tokenizer, + text_encoder: nn.Module, + font_ann_path: Path, + color_ann_path: Path, + add_font: bool, + add_color: bool, + add_align: bool = False, +) -> None: + """Extend the byte5 vocab with per-language font + color markers. + + Mirrors ``add_special_token_multilingual`` in Ming's bizgen utils. The token + set must match the one the checkpoint was trained with, otherwise the + resized embedding table shape won't line up with the shipped weights. + """ + idx_font_dict = json.loads(font_ann_path.read_text()) + idx_color_dict = json.loads(color_ann_path.read_text()) + + font_tokens: list[str] = [] + for font_code in idx_font_dict: + prefix = font_code[:3] + if prefix in ("cn-", "en-", "jp-", "kr-"): + font_tokens.append(f"<{prefix}font-{idx_font_dict[font_code]}>") + else: + font_tokens.append(f"") + color_tokens = [f"" for i in range(len(idx_color_dict))] + align_tokens = [f"" for i in range(3)] + + extra: list[str] = [] + if add_color: + extra += color_tokens + if add_font: + extra += font_tokens + if add_align: + extra += align_tokens + tokenizer.add_tokens(extra, special_tokens=True) + text_encoder.resize_token_embeddings(len(tokenizer)) + + +class MingByT5Encoder(nn.Module): + """Bundles byte5 tokenizer + T5 encoder + T5EncoderBlockByT5Mapper. + + Build with ``MingByT5Encoder.from_checkpoint(/byte5)`` when the + checkpoint ships byte5 weights; otherwise callers can skip this and the + pipeline falls back to no-byte5 conditioning. + """ + + def __init__( + self, + tokenizer, + text_encoder: nn.Module, + mapper: T5EncoderBlockByT5Mapper, + max_length: int, + ) -> None: + super().__init__() + self.tokenizer = tokenizer + self.text_encoder = text_encoder + self.mapper = mapper + self.max_length = max_length + + @classmethod + def from_checkpoint( + cls, + byte5_dir: Path, + *, + device: torch.device, + dtype: torch.dtype, + ) -> MingByT5Encoder: + # Wrap the whole load in fork_rng so any nn.init.normal_ inside + # T5ForConditionalGeneration.from_pretrained / vocab-resize cannot + # advance the default CPU/CUDA generator state. Without this, the + # diffusion pipeline's seeded noise becomes order-dependent across + # requests (same-seed replays diverge). + cuda_devs = list(range(torch.accelerator.device_count())) if torch.cuda.is_available() else [] + with torch.random.fork_rng(devices=cuda_devs, enabled=True): + return cls._from_checkpoint_impl(byte5_dir, device=device, dtype=dtype) + + @classmethod + def _from_checkpoint_impl( + cls, + byte5_dir: Path, + *, + device: torch.device, + dtype: torch.dtype, + ) -> MingByT5Encoder: + byte5_dir = Path(byte5_dir) + # Ming checkpoint uses ``byt5`` (no 'e') in filenames and JSON keys + # variable names below keep the ``byte5_`` spelling for readability. + cfg_path = byte5_dir / "byt5.json" + cfg_raw = json.loads(cfg_path.read_text()) + cfg = SimpleNamespace(**cfg_raw) + byte5_config = cfg.byt5_config + mapper_config = cfg.byt5_mapper_config + max_length = int(cfg.byt5_max_length) + + # ---- Tokenizer + T5 encoder (base). + ckpt_key = byte5_config.get("byt5_ckpt_path") + byte5_ckpt_path = byte5_dir / ckpt_key.lstrip("./") + tokenizer = AutoTokenizer.from_pretrained(byte5_ckpt_path, local_files_only=True) + text_encoder = T5ForConditionalGeneration.from_pretrained(byte5_ckpt_path, local_files_only=True).get_encoder() + + # ---- Extend vocab with font/color markers so the shipped weights load. + if byte5_config.get("special_token"): + if not byte5_config.get("multilingual", True): + raise NotImplementedError( + "Non-multilingual byte5 vocab extension is not ported; " + "the released Ming checkpoint uses multilingual=True." + ) + _add_multilingual_special_tokens( + tokenizer, + text_encoder, + font_ann_path=byte5_dir / byte5_config["font_ann_path"].lstrip("./"), + color_ann_path=byte5_dir / byte5_config["color_ann_path"].lstrip("./"), + add_font=bool(byte5_config.get("font_special_token")), + add_color=bool(byte5_config.get("color_special_token")), + ) + + # ---- Load byt5 text-encoder weights (base.pt wraps the backbone in a + # trainable-module container; byt5_model.pt has the top-level encoder + # state). Follow Ming's two-step load. + base_state = torch.load(byte5_dir / "byt5_model" / "base.pt", map_location="cpu", weights_only=False) + prefix = "module.text_tower.encoder." + base_filtered = { + name[len(prefix) :]: state for name, state in base_state["state_dict"].items() if name.startswith(prefix) + } + text_encoder.load_state_dict(base_filtered, strict=True) + del base_state, base_filtered + + encoder_state = torch.load(byte5_dir / "byt5_model" / "byt5_model.pt", map_location="cpu", weights_only=False) + text_encoder.load_state_dict(encoder_state) + del encoder_state + + text_encoder.to(device=device, dtype=dtype).eval() + + # ---- Mapper. + mapper = T5EncoderBlockByT5Mapper( + byte5_config=text_encoder.config, + num_layers=int(mapper_config["num_layers"]), + sdxl_channels=int(mapper_config["sdxl_channels"]), + ) + mapper_state = torch.load(byte5_dir / "byt5_mapper" / "byt5_mapper.pt", map_location="cpu", weights_only=False) + # mapper now uses vllm-omni TP-aware T5 layers (fused qkv_proj / wi); + mapper.load_weights(mapper_state.items()) + del mapper_state + mapper.to(device=device, dtype=dtype).eval() + + logger.info( + "[MingByT5Encoder] ready: d_model=%d mapper_layers=%d sdxl_channels=%d max_length=%d vocab=%d", + text_encoder.config.d_model, + mapper_config["num_layers"], + mapper_config["sdxl_channels"], + max_length, + len(tokenizer), + ) + return cls(tokenizer, text_encoder, mapper, max_length) + + @torch.inference_mode() + def forward(self, texts: list[str]) -> torch.Tensor: + """Tokenize → T5 encode → mapper; masks out padded positions. + + Returns ``[B, max_length, sdxl_channels]``. Padded positions are zeroed + so the downstream ``torch.cat`` with cap_feats doesn't inject garbage. + """ + device = next(self.text_encoder.parameters()).device + dtype = next(self.text_encoder.parameters()).dtype + + tokens = self.tokenizer( + texts, + padding="max_length", + max_length=self.max_length, + truncation=True, + add_special_tokens=True, + return_tensors="pt", + ) + input_ids = tokens.input_ids.to(device) + attention_mask = tokens.attention_mask.to(device) + + encoder_out = self.text_encoder( + input_ids=input_ids, + attention_mask=attention_mask.float(), + ) + hidden_states = encoder_out[0] + feats = self.mapper(hidden_states, attention_mask) + feats = feats * attention_mask.unsqueeze(-1).to(dtype=feats.dtype) + return feats.to(dtype=dtype) + + +__all__ = ["MingByT5Encoder"] diff --git a/vllm_omni/diffusion/models/ming_flash_omni/condition_encoder.py b/vllm_omni/diffusion/models/ming_flash_omni/condition_encoder.py new file mode 100644 index 000000000..6ebca4f5c --- /dev/null +++ b/vllm_omni/diffusion/models/ming_flash_omni/condition_encoder.py @@ -0,0 +1,373 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025 The vLLM-Omni team. +# +# Adapted from Ming repository (inclusionAI/Ming) — the ``get_condition_embeds +# _for_image_gen`` path in modeling_bailingmm2.py. + +"""Ming-flash-omni-2.0 condition encoder for image generation. + +Pipeline (runs inside the imagegen stage): + + thinker hidden states at query-token positions [B, N, 4096] + │ + ▼ proj_in (Linear, bias=True) + [B, N, 1536] + │ + ▼ Qwen2ForCausalLM connector (is_causal=False) + loaded from /connector/ + [B, N, 1536] + │ + ▼ proj_out (Linear, bias=True) + [B, N, 2560] + │ + ▼ F.normalize(dim=-1) × 1000 (text_encoder_norm) + [B, N, 2560] + │ + ▼ + cap_feats consumed by ZImageTransformer2DModel +""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path + +import torch +import torch.nn as nn + +from vllm_omni.transformers_utils.configs.ming_flash_omni import MingImageGenConfig + +logger = logging.getLogger(__name__) + + +class MingConditionEncoder(nn.Module): + """Wraps a Qwen2 connector + norm/projection, producing DiT condition embeds. + + The connector is a ``Qwen2ForCausalLM`` loaded from the ``connector/`` + subfolder of the Ming checkpoint. We run its base model in a non-causal + (bidirectional) mode, because the connector is used as an encoder over the + pre-baked query-token hidden states, not as an autoregressive decoder. + + Args: + image_gen_config: ``MingImageGenConfig`` from ``MingFlashOmniConfig``. + thinker_hidden_size: Hidden size of the thinker (BailingMoeV2) model. + Used to build a ``proj_in`` layer when the connector embedding + dim differs. For the released checkpoint this is 4096. + device: Placement for the module. + dtype: Parameter dtype (typically bfloat16 / float16). + """ + + def __init__( + self, + image_gen_config: MingImageGenConfig, + *, + thinker_hidden_size: int = 4096, + device: torch.device | str | None = None, + dtype: torch.dtype | None = None, + ) -> None: + super().__init__() + self.config = image_gen_config + self.thinker_hidden_size = thinker_hidden_size + self._target_device = torch.device(device) if device is not None else None + self._target_dtype = dtype + + # Populated lazily by ``load_from_checkpoint`` to keep this module + # cheap to construct (useful for dummy-init paths and unit tests). + self.connector: nn.Module | None = None + self.connector_hidden_size: int | None = None + self.proj_in: nn.Module = nn.Identity() + self.proj_out: nn.Module = nn.Identity() + self.norm: nn.Module = nn.Identity() + + # ------------------------------------------------------------------ + # Weight loading + # ------------------------------------------------------------------ + + def load_from_checkpoint(self, model_path: str | Path) -> None: + """Load the Qwen2 connector + optional projection/norm weights. + + This uses HF transformers directly (not vllm's weight loader) because + the connector is small (~1.5B params) and only runs once per request + as an encoder — vllm's distributed loading machinery is overkill. + """ + from transformers import AutoConfig, Qwen2ForCausalLM + + model_path = Path(model_path) + connector_path = model_path / self.config.connector_subfolder + logger.info("[MingConditionEncoder] loading connector from %s", connector_path) + + connector_cfg = AutoConfig.from_pretrained(connector_path, trust_remote_code=True, local_files_only=True) + # Disable causal masking: the connector is used as a bidirectional + # encoder over query-token hidden states in image-gen mode. + connector_cfg.is_decoder = False + self.connector_hidden_size = int(connector_cfg.hidden_size) + logger.info( + "[MingConditionEncoder] connector hidden_size=%d, layers=%d", + self.connector_hidden_size, + getattr(connector_cfg, "num_hidden_layers", -1), + ) + + connector = Qwen2ForCausalLM.from_pretrained( + connector_path, + config=connector_cfg, + torch_dtype=self._target_dtype, + local_files_only=True, + ) + # Force bidirectional attention in every self-attn module. Qwen2 layers + # do not branch on ``is_causal`` directly, so we monkey-patch the flag + # on each attention block (defensive — some transformers versions read + # ``self_attn.is_causal`` in forward). + patched = 0 + for module in connector.modules(): + if hasattr(module, "is_causal"): + module.is_causal = False + patched += 1 + logger.info("[MingConditionEncoder] disabled is_causal on %d sub-modules", patched) + + # We only need the base encoder (no LM head). + base = getattr(connector, "model", connector) + self.connector = base + + # proj_in: align thinker_hidden_size -> connector_hidden_size. + # Observed layout of mlp/ on the released checkpoint + # (inclusionAI/Ming-flash-omni-2.0): + # proj_in.{weight,bias} : Linear(4096 -> 1536) + # proj_out.{weight,bias} : Linear(1536 -> 2560) + # query_tokens_dict.16x16 : learnable tokens consumed on the thinker + # side (NOT loaded here). + logger.info( + "[MingConditionEncoder] adding proj_in: %d -> %d (bias=True)", + self.thinker_hidden_size, + self.connector_hidden_size, + ) + self.proj_in = nn.Linear(self.thinker_hidden_size, self.connector_hidden_size, bias=True) + + # NOTE: ``text_encoder_norm=True`` in Ming's mlp/config.json refers to + # applying ``F.normalize(..., dim=-1)`` (L2 normalization) on the + # FINAL cap_feats AFTER proj_out — NOT an intermediate RMSNorm. See + # modeling_bailingmm2.py::get_condition_embeds_for_image_gen. We keep + # ``self.norm = nn.Identity`` and apply the L2 normalize explicitly + # at the end of ``forward``. + self.norm = nn.Identity() + + # proj_out: align connector_hidden_size -> diffusion_c_input_dim. + c_out = self.config.diffusion_c_input_dim + logger.info( + "[MingConditionEncoder] adding proj_out: %d -> %d (bias=True)", + self.connector_hidden_size, + c_out, + ) + self.proj_out = nn.Linear(self.connector_hidden_size, c_out, bias=True) + + mlp_path = model_path / self.config.mlp_subfolder + mlp_cfg_path = mlp_path / "config.json" + if mlp_cfg_path.exists() and not json.loads(mlp_cfg_path.read_text()).get("use_identity_mlp", False): + # MingImagePipeline skips ZImageModel_withMLP, only correct when its inner MLP is Identity. + raise NotImplementedError(f"{mlp_cfg_path} has use_identity_mlp=False; ToClipMLP path not implemented.") + self._load_optional_mlp_weights(mlp_path) + + if self._target_device is not None: + self.to(self._target_device) + if self._target_dtype is not None: + self.to(dtype=self._target_dtype) + + logger.info( + "[MingConditionEncoder] ready: in=%d -> conn=%d -> out=%d", + self.thinker_hidden_size, + self.connector_hidden_size, + c_out, + ) + + def _load_optional_mlp_weights(self, mlp_path: Path) -> None: + """Load proj_in / proj_out (and optional norm) weights from mlp/. + + Expected keys — observed on inclusionAI/Ming-flash-omni-2.0: + proj_in.weight [1536, 4096] + proj_in.bias [1536] + proj_out.weight [2560, 1536] + proj_out.bias [2560] + query_tokens_dict.16x16 [256, 4096] -> thinker-side, skipped here + (optional) norm.weight [1536] + + Any extra keys are logged as warnings so we don't silently miss new + fields. Missing proj weights are fatal: they are required for the + condition path to be meaningful. + """ + if not mlp_path.exists(): + logger.warning( + "[MingConditionEncoder] mlp/ subfolder missing at %s — proj/norm " + "will stay randomly initialized. EXPECT BAD IMAGES until this " + "is fixed on real hardware.", + mlp_path, + ) + return + + try: + from safetensors.torch import load_file # type: ignore + except ImportError: + logger.exception("[MingConditionEncoder] safetensors not installed") + return + + candidates = sorted(mlp_path.glob("*.safetensors")) + if not candidates: + candidates = sorted(mlp_path.glob("*.bin")) + if not candidates: + logger.warning("[MingConditionEncoder] no weight files under %s", mlp_path) + return + + state: dict[str, torch.Tensor] = {} + for p in candidates: + logger.info("[MingConditionEncoder] reading mlp weights: %s", p) + if p.suffix == ".safetensors": + state.update(load_file(str(p))) + else: + state.update(torch.load(str(p), map_location="cpu")) + logger.info( + "[MingConditionEncoder] mlp/ keys: %s", + sorted(state.keys()), + ) + + handled: set[str] = set() + + def _copy(dst: nn.Parameter | torch.Tensor, src_key: str) -> bool: + src = state.get(src_key) + if src is None: + logger.error("[MingConditionEncoder] mlp/ missing key %r", src_key) + return False + if tuple(src.shape) != tuple(dst.shape): + logger.error( + "[MingConditionEncoder] mlp/%s shape mismatch: checkpoint=%s, module=%s", + src_key, + tuple(src.shape), + tuple(dst.shape), + ) + return False + with torch.no_grad(): + dst.copy_(src.to(dtype=dst.dtype, device=dst.device)) + handled.add(src_key) + logger.info( + "[MingConditionEncoder] loaded mlp/%s -> %s", + src_key, + tuple(dst.shape), + ) + return True + + ok_w = _copy(self.proj_in.weight, "proj_in.weight") + ok_b = _copy(self.proj_in.bias, "proj_in.bias") + ok_ow = _copy(self.proj_out.weight, "proj_out.weight") + ok_ob = _copy(self.proj_out.bias, "proj_out.bias") + if not (ok_w and ok_b and ok_ow and ok_ob): + logger.error( + "[MingConditionEncoder] proj_in/proj_out NOT fully loaded; diffusion conditioning will be garbage." + ) + + # Optional norm weight (Ming uses plain RMSNorm; may or may not ship). + if "norm.weight" in state and hasattr(self.norm, "weight"): + _copy(self.norm.weight, "norm.weight") + + # query_tokens_dict lives in mlp/ but is consumed on the thinker side + # — we neither need nor load it here. Just log the shape so the + # thinker-side patch knows what to expect. + for k, v in state.items(): + if k.startswith("query_tokens_dict"): + logger.info( + "[MingConditionEncoder] thinker-side parameter %s shape=%s (NOT loaded here; thinker must own it)", + k, + tuple(v.shape), + ) + handled.add(k) + + leftover = set(state.keys()) - handled + if leftover: + logger.warning("[MingConditionEncoder] mlp/ unhandled keys: %s", sorted(leftover)) + + # ------------------------------------------------------------------ + # Forward + # ------------------------------------------------------------------ + + def forward( + self, + thinker_hidden_states: torch.Tensor, + attention_mask: torch.Tensor | None = None, + ) -> torch.Tensor: + """Encode thinker hidden states into DiT condition embeddings. + + Args: + thinker_hidden_states: ``[B, N, thinker_hidden_size]`` — sliced at + the learnable query-token positions by the stage input + processor before being passed here. + attention_mask: Optional ``[B, N]`` mask. Defaults to all-ones. + + Returns: + ``[B, N, diffusion_c_input_dim]`` condition tensor ready for the + ZImage transformer's ``cap_feats`` input. + """ + if self.connector is None: + raise RuntimeError("MingConditionEncoder.load_from_checkpoint() must be called before forward().") + if thinker_hidden_states.dim() != 3: + raise ValueError(f"expected [B, N, H], got shape {tuple(thinker_hidden_states.shape)}") + + b, n, _ = thinker_hidden_states.shape + logger.debug( + "[MingCondEnc] input shape=%s dtype=%s", tuple(thinker_hidden_states.shape), thinker_hidden_states.dtype + ) + + x = self.proj_in(thinker_hidden_states) # [B, N, conn_hidden] + + # Ming's ``get_condition_embeds_for_image_gen`` passes a 4D full-ones + # attention mask of shape [B, 1, N, N] to the Qwen2 connector, + # forcing full bidirectional self-attention over all query positions. + if attention_mask is None: + attention_mask = torch.ones( + (b, 1, n, n), + dtype=x.dtype, + device=x.device, + ) + elif attention_mask.dim() == 2: + mask_2d = attention_mask.to(x.dtype) + attention_mask = mask_2d[:, None, None, :].expand(b, 1, n, n) + + out = self.connector( + inputs_embeds=x, + attention_mask=attention_mask, + use_cache=False, + output_hidden_states=True, + return_dict=True, + ) + hidden = out.hidden_states[-1] # [B, N, conn_hidden] + + cap_feats = self.proj_out(hidden) # [B, N, diffusion_c_input_dim] + + # L2 normalize + ×1000 rescale. Ming's ``get_condition_embeds_for_image_gen`` + # applies F.normalize; ``ZImageModel_withMLP`` then rescales by 1000 + # when ``text_encoder_norm=True``. We fold both into one place. + cap_feats = torch.nn.functional.normalize(cap_feats, dim=-1) + if self.config.text_encoder_norm: + cap_feats = cap_feats * 1000.0 + + logger.debug("[MingCondEnc] output shape=%s dtype=%s", tuple(cap_feats.shape), cap_feats.dtype) + return cap_feats + + # ------------------------------------------------------------------ + # Utility + # ------------------------------------------------------------------ + + @torch.no_grad() + def zero_negative( + self, + cap_feats: torch.Tensor, + ) -> torch.Tensor: + """Return a zero tensor shaped like ``cap_feats`` for CFG negatives.""" + return torch.zeros_like(cap_feats) + + def extra_repr(self) -> str: + return ( + f"thinker_hidden_size={self.thinker_hidden_size}, " + f"connector_hidden_size={self.connector_hidden_size}, " + f"diffusion_c_input_dim={self.config.diffusion_c_input_dim}, " + f"text_encoder_norm={self.config.text_encoder_norm}" + ) + + +__all__ = ["MingConditionEncoder"] diff --git a/vllm_omni/diffusion/models/ming_flash_omni/ming_zimage_transformer.py b/vllm_omni/diffusion/models/ming_flash_omni/ming_zimage_transformer.py new file mode 100644 index 000000000..4d3925f6d --- /dev/null +++ b/vllm_omni/diffusion/models/ming_flash_omni/ming_zimage_transformer.py @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025 The vLLM-Omni team. + +"""Ming-specific subclass of ZImageTransformer2DModel that supports ``ref_x``. + +Ming's img2img path concatenates a VAE-encoded reference latent along the +frame axis before patchification, then drops the reference portion from the +unpatchified output. This is a surgical override — everything else (attention, +RoPE, final layer) stays on the parent implementation. + +""" + +from __future__ import annotations + +import torch + +from vllm_omni.diffusion.forward_context import get_forward_context, is_forward_context_available +from vllm_omni.diffusion.models.z_image.z_image_transformer import ZImageTransformer2DModel + + +class MingZImageTransformer2DModel(ZImageTransformer2DModel): + """ZImage DiT with Ming's reference-latent support.""" + + def forward( + self, + x: list[torch.Tensor], + t, + cap_feats: list[torch.Tensor], + patch_size=2, + f_patch_size=1, + ): + ref_latent = get_forward_context().ref_latent if is_forward_context_available() else None + if ref_latent is not None: + per_item = ref_latent[0].unsqueeze(1).to(dtype=x[0].dtype, device=x[0].device) # [C, 1, H, W] + x = [torch.cat([img, per_item], dim=1) for img in x] + return super().forward(x, t, cap_feats, patch_size=patch_size, f_patch_size=f_patch_size) + + def unpatchify( + self, + x: list[torch.Tensor], + size: list[tuple], + patch_size, + f_patch_size, + ) -> list[torch.Tensor]: + out = super().unpatchify(x, size, patch_size, f_patch_size) + # No-op when F==1 (pure t2i); drops the reference-frame prediction when F==2. + return [t[:, :1, :, :] for t in out] + + +__all__ = ["MingZImageTransformer2DModel"] diff --git a/vllm_omni/diffusion/models/ming_flash_omni/pipeline_ming_imagegen.py b/vllm_omni/diffusion/models/ming_flash_omni/pipeline_ming_imagegen.py new file mode 100644 index 000000000..0da26aff8 --- /dev/null +++ b/vllm_omni/diffusion/models/ming_flash_omni/pipeline_ming_imagegen.py @@ -0,0 +1,520 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025 The vLLM-Omni team. + +"""Ming-flash-omni-2.0 imagegen (text-to-image / img2img) diffusion pipeline. + +Cross-stage data flow: + + Stage 0 (thinker, llm) Stage 1 (imagegen, diffusion) + ──────────────────── ────────────────────────────── + forward returns thinker2imagegen hook slices + multimodal_output[ final_hidden_states at + "final_hidden_states"] positions, + ↓ returns list[dict] with + shared_memory_connector {"extra": {"thinker_hidden_states"}} + ↓ ──── via OmniMsgpackEncoder ────> + MingImagePipeline.forward(req): + hidden = req.prompts[0]["extra"][...] + cond = condition_encoder(hidden) + img = ZImagePipeline-style loop + return DiffusionOutput(output=img) +""" + +from __future__ import annotations + +import logging +import os +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import torch +import torch.nn as nn +from diffusers.image_processor import VaeImageProcessor +from diffusers.schedulers import FlowMatchEulerDiscreteScheduler + +from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig +from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl import DistributedAutoencoderKL +from vllm_omni.diffusion.distributed.utils import get_local_device +from vllm_omni.diffusion.forward_context import set_forward_context_ref_latent +from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader +from vllm_omni.diffusion.model_loader.hub_prefetch import prefetch_subfolders +from vllm_omni.diffusion.models.ming_flash_omni.byte5_encoder import ( + MingByT5Encoder, +) +from vllm_omni.diffusion.models.ming_flash_omni.condition_encoder import ( + MingConditionEncoder, +) +from vllm_omni.diffusion.models.ming_flash_omni.ming_zimage_transformer import ( + MingZImageTransformer2DModel, +) +from vllm_omni.diffusion.models.z_image.pipeline_z_image import ZImagePipeline +from vllm_omni.diffusion.request import OmniDiffusionRequest +from vllm_omni.model_executor.model_loader.weight_utils import ( + download_weights_from_hf_specific, +) +from vllm_omni.transformers_utils.configs.ming_flash_omni import MingImageGenConfig + +logger = logging.getLogger(__name__) + + +@dataclass +class _ZPipelineSamplingParams: + """Typed shim satisfying the attributes ZImagePipeline.forward reads + from ``req.sampling_params``. Unlike SimpleNamespace this fails loudly + at construction if a required field is missing.""" + + height: int + width: int + num_inference_steps: int + guidance_scale: float + generator: torch.Generator | None = None + strength: float | None = None + sigmas: list[float] | None = None + max_sequence_length: int = 512 + guidance_rescale: float | None = None + num_outputs_per_prompt: int = 1 + + +@dataclass +class _ZPipelineRequest: + """Typed shim satisfying the attributes ZImagePipeline.forward reads + from ``req``.""" + + request_id: str + sampling_params: _ZPipelineSamplingParams + prompts: list[dict[str, Any]] = field(default_factory=lambda: [{"prompt": "", "negative_prompt": ""}]) + + +class MingImagePipeline(ZImagePipeline): + """Ming-flash-omni-2.0 text-to-image diffusion pipeline. + + Ming-specific components added on top of the inherited contract: + * ``condition_encoder`` — Qwen2 connector + proj_in/out + F.normalize×1000 + * ``byte5`` — Optional ByT5 glyph encoder (loaded if checkpoint + ships ``byt5/``) + """ + + def __init__( + self, + *, + od_config: OmniDiffusionConfig, + prefix: str = "", # noqa: ARG002 + ) -> None: + # Skip ZImagePipeline.__init__ (it would eagerly load the Z-Image text + # encoder/tokenizer that Ming replaces with its own condition_encoder). + nn.Module.__init__(self) + + model_path = od_config.model + if not os.path.exists(model_path): + model_path = download_weights_from_hf_specific(model_path, od_config.revision, ["*"]) + + dtype = getattr(od_config, "dtype", torch.bfloat16) + local_files_only = os.path.exists(model_path) + + self.od_config = od_config + self._execution_device = get_local_device() + self.device = self._execution_device # Ming convention alias + self._dtype = dtype + + # Ming's per-checkpoint image-gen configuration. We cannot rely on + # ``od_config.hf_config.image_gen_config`` because the diffusion + # stage is started with ``hf_config_name: thinker_config`` (the + # BailingMM2Config), which does not carry a MingImageGenConfig. + # Fall back to defaults that match the released checkpoint. + self.image_gen_config = MingImageGenConfig() + logger.info( + "[MingImagePipeline] init: model=%s dtype=%s image_gen_config=%s", + model_path, + dtype, + self.image_gen_config, + ) + + # ----- weights_sources: DiT transformer + VAE. + self.weights_sources = [ + DiffusersPipelineLoader.ComponentSource( + model_or_path=model_path, + subfolder=self.image_gen_config.transformer_subfolder, + revision=od_config.revision, + prefix="transformer.", + fall_back_to_pt=True, + ), + DiffusersPipelineLoader.ComponentSource( + model_or_path=model_path, + subfolder=self.image_gen_config.vae_subfolder, + revision=od_config.revision, + prefix="vae.", + ), + ] + + prefetch_subfolders( + model_path, + [self.image_gen_config.scheduler_subfolder, self.image_gen_config.vae_subfolder], + local_files_only=local_files_only, + ) + + # ----- Scheduler: load config-only from disk + Ming-specific override. + self.scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained( + model_path, + subfolder=self.image_gen_config.scheduler_subfolder, + local_files_only=local_files_only, + ) + # Ming forces use_dynamic_shifting=True at runtime regardless of what + # the checkpoint scheduler_config.json ships. + self.scheduler.config["use_dynamic_shifting"] = True + logger.info( + "[MingImagePipeline] scheduler: %s (use_dynamic_shifting=True)", + type(self.scheduler).__name__, + ) + + # ----- VAE: DistributedAutoencoderKL. + vae_config = DistributedAutoencoderKL.load_config( + model_path, subfolder=self.image_gen_config.vae_subfolder, local_files_only=local_files_only + ) + self.vae = DistributedAutoencoderKL.from_config(vae_config).to(self._execution_device, dtype=dtype) + self.vae.eval() + + # ----- DiT transformer. + self.transformer = MingZImageTransformer2DModel(quant_config=None) + + # Ming brings its own conditioning path — no Z-Image text_encoder / + # tokenizer. + self.text_encoder = None + self.tokenizer = None + + self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1) + self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * 2, do_convert_rgb=True) + self.setup_diffusion_pipeline_profiler( + enable_diffusion_pipeline_profiler=getattr(od_config, "enable_diffusion_pipeline_profiler", False) + ) + + # ----- Condition encoder (Qwen2 connector + proj_in/out + norm×1000). + self.condition_encoder = MingConditionEncoder( + self.image_gen_config, + thinker_hidden_size=self.image_gen_config.thinker_hidden_size, + device=self.device, + dtype=dtype, + ) + self.condition_encoder.load_from_checkpoint(model_path) + + # Optional ByT5 glyph/text encoder. Only loaded when the checkpoint + # ``extra_args.image_gen.byte5_text`` requests will be ignored. + byte5_dir = Path(model_path) / "byt5" + if byte5_dir.exists(): + self.byte5 = MingByT5Encoder.from_checkpoint(byte5_dir, device=self.device, dtype=dtype) + else: + self.byte5 = None + logger.info("[MingImagePipeline] no byt5/ subfolder at %s; ByT5 enhancement disabled", byte5_dir) + + logger.info("[MingImagePipeline] ready — vae_scale_factor=%d", self.vae_scale_factor) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _resolve_byte5_texts(extra: dict, sampling_params) -> list[str]: + """Resolve byte5 glyph texts. + + Two sources, in order of priority: + 1. ``extra["byte5_text"]`` — auto-extracted from the user prompt's + quoted spans by ``thinker2imagegen`` (already wrapped as + ``'Text "". '`` by Ming's ``get_text_from_prompt``). + 2. ``sampling_params.extra_args["image_gen"]["byte5_text"]`` — an + explicit override for programmatic callers; raw strings without + the ``Text "..."`` wrapper are auto-wrapped here to match the + distribution ByT5 was trained on. + """ + # Source 1: auto-extracted, already wrapped. Return as-is if non-empty. + raw = extra.get("byte5_text") + if isinstance(raw, str): + raw = [raw] + if isinstance(raw, list): + cleaned = [t for t in raw if isinstance(t, str) and t.strip()] + if cleaned: + return cleaned + + # Source 2: explicit override — wrap raw strings so the byte5 encoder + # sees the same ``Text "". `` format Ming used during training. + raw = ((getattr(sampling_params, "extra_args", None) or {}).get("image_gen") or {}).get("byte5_text") + if isinstance(raw, str): + raw = [raw] + if isinstance(raw, list): + out: list[str] = [] + for t in raw: + if not isinstance(t, str): + continue + s = t.strip() + if not s: + continue + # Don't double-wrap if the caller already supplied ``Text "...". ``. + out.append(s if s.startswith('Text "') else f'Text "{s}". ') + if out: + return out + return [] + + @torch.inference_mode() + def _encode_reference_image(self, ref, height: int, width: int) -> torch.Tensor | None: + """Turn a PIL/tensor reference image into a VAE latent for ``ref_x``. + + Applies the same shift/scale Ming uses (``(z - shift_factor) * scaling_factor``) + so the concatenated frame lives in the DiT's latent space. + """ + if ref is None: + return None + if not isinstance(ref, torch.Tensor): + ref = self.image_processor.preprocess(ref, height, width) + ref = ref.to(device=self.device, dtype=self.vae.dtype) + latent = self.vae.encode(ref).latent_dist.mode() + return (latent - self.vae.config.shift_factor) * self.vae.config.scaling_factor + + # ------------------------------------------------------------------ + # Forward + # ------------------------------------------------------------------ + + @torch.inference_mode() + def forward(self, req: OmniDiffusionRequest) -> DiffusionOutput: + """Run one text-to-image generation request. + + Args: + req: Diffusion request. The cross-stage thinker hidden states + must be present at + ``req.prompts[0]["extra"]["thinker_hidden_states"]`` as a + ``[N, H]`` (or ``[1, N, H]``) tensor, placed there by + ``thinker2imagegen``. + + Returns: + DiffusionOutput with ``.output`` set to a ``[B, 3, H, W]`` + image tensor in ``[-1, 1]``. The vllm-omni diffusion engine's + output adapter converts this to PIL/base64 downstream. + """ + first_prompt = req.prompts[0] if req.prompts else None + if isinstance(first_prompt, str): + prompt_dict: dict[str, Any] = {} + elif isinstance(first_prompt, dict): + prompt_dict = first_prompt + elif first_prompt is not None and hasattr(first_prompt, "_asdict"): + prompt_dict = first_prompt._asdict() + elif first_prompt is not None and hasattr(first_prompt, "__dict__"): + prompt_dict = vars(first_prompt) + else: + prompt_dict = {} + + extra = prompt_dict.get("extra") or {} + hidden = extra.get("thinker_hidden_states") + if hidden is None: + # Same dual-path convention as glm_image: also check + # ``sampling_params.extra_args``. + hidden = (req.sampling_params.extra_args or {}).get("thinker_hidden_states") + if hidden is None: + scale = self.image_gen_config.img_gen_scales[-1] + num_query_tokens = scale * scale + hidden = torch.zeros( + (num_query_tokens, self.image_gen_config.thinker_hidden_size), + dtype=self._dtype, + device=self.device, + ) + logger.warning( + "[MingImagePipeline.forward] 'thinker_hidden_states' missing " + "from request; falling back to zero-conditioning %s. This is " + "expected during warmup; for real requests verify that " + "`custom_process_input_func: thinker2imagegen` is set on the " + "diffusion stage in the YAML.", + tuple(hidden.shape), + ) + + if not isinstance(hidden, torch.Tensor): + raise TypeError( + f"[MingImagePipeline] 'thinker_hidden_states' must be a Tensor, got {type(hidden).__name__}" + ) + + # Move to the pipeline's device+dtype. + target_device = next(self.parameters()).device + target_dtype = next(self.parameters()).dtype + hidden = hidden.to(device=target_device, dtype=target_dtype) + if hidden.dim() == 2: + hidden = hidden.unsqueeze(0) # [N, H] -> [1, N, H] + logger.debug( + "[MingImagePipeline.forward] thinker_hidden_states=%s on %s (%s)", + tuple(hidden.shape), + target_device, + target_dtype, + ) + + # ----- Condition encoder → cap_feats + cap_feats = self.condition_encoder(hidden) + logger.debug("[MingImagePipeline.forward] cap_feats=%s", tuple(cap_feats.shape)) + + # Real negative CFG conditioning (opt-in). See expand_cfg_prompts. + negative_hidden = extra.get("negative_thinker_hidden_states") + negative_cap_feats = None + if isinstance(negative_hidden, torch.Tensor): + negative_hidden = negative_hidden.to(device=target_device, dtype=target_dtype) + if negative_hidden.dim() == 2: + negative_hidden = negative_hidden.unsqueeze(0) + negative_cap_feats = self.condition_encoder(negative_hidden) + logger.debug("[MingImagePipeline.forward] negative_cap_feats=%s", tuple(negative_cap_feats.shape)) + + # ByT5 text enhancement (opt-in). Appends glyph-aware features along + # the sequence dim; negative side gets zeros for the byte5 portion so + # CFG doesn't push away from the rendered text. + byte5_texts = self._resolve_byte5_texts(extra, req.sampling_params) + if byte5_texts and self.byte5 is not None: + byte5_feats = self.byte5(byte5_texts).to(device=target_device, dtype=target_dtype) + cap_feats = torch.cat((cap_feats, byte5_feats), dim=1) + if negative_cap_feats is not None: + negative_cap_feats = torch.cat((negative_cap_feats, torch.zeros_like(byte5_feats)), dim=1) + logger.debug("[MingImagePipeline.forward] byte5 cat'd: cap_feats=%s", tuple(cap_feats.shape)) + + # Sampling knobs: extra_args.image_gen.* > sampling_params.* > MingImageGenConfig defaults. + sp = req.sampling_params + cfg = self.image_gen_config + ig = (sp.extra_args or {}).get("image_gen") or {} + resolved: dict[str, Any] = {} + for ig_key, sp_attr, default in ( + ("height", "height", cfg.default_height), + ("width", "width", cfg.default_width), + ("steps", "num_inference_steps", cfg.num_inference_steps), + ("cfg", "guidance_scale", cfg.guidance_scale), + ("seed", "seed", None), + ): + for v in (ig.get(ig_key), getattr(sp, sp_attr), default): + if v is not None: + resolved[ig_key] = v + break + + height = int(resolved["height"]) + width = int(resolved["width"]) + num_inference_steps = int(resolved["steps"]) + guidance_scale = float(resolved["cfg"]) + seed = resolved.get("seed") + + # Always rebuild the generator from the resolved seed. Reusing + # ``sp.generator`` causes two problems: + # (1) if the caller pre-seeded it with sp.seed (e.g. the top-level + # ``seed`` key on OmniDiffusionSamplingParams), any override via + # ``extra_args.image_gen.seed`` would be silently ignored; and + # (2) a persistent generator instance accumulates state across + # requests → same-seed replays produce different outputs. + if seed is not None: + generator = torch.Generator(device=target_device).manual_seed(int(seed)) + else: + generator = sp.generator + + # Format prompt_embeds / negative_prompt_embeds as list[Tensor] + # (one entry per request) — matches ZImagePipeline's contract when + # prompt_embeds are pre-computed. + prompt_embeds = [cap_feats[i] for i in range(cap_feats.shape[0])] + if negative_cap_feats is not None: + negative_prompt_embeds = [negative_cap_feats[i] for i in range(negative_cap_feats.shape[0])] + else: + negative_prompt_embeds = [self.condition_encoder.zero_negative(e) for e in prompt_embeds] + + z_sp = _ZPipelineSamplingParams( + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + generator=generator, + ) + z_req = _ZPipelineRequest( + request_id=req.request_id or "ming-imagegen", + sampling_params=z_sp, + ) + + # Reference image (img2img) → VAE-encoded latent published on the + # active ForwardContext so MingZImageTransformer2DModel can read it + # from request scope inside its forward(). + ref_latent = self._encode_reference_image(extra.get("reference_image"), height, width) + set_forward_context_ref_latent(ref_latent) + + logger.debug( + "[MingImagePipeline.forward] running z_pipeline hw=(%d,%d) steps=%d cfg=%.2f seed=%s overrides=%s ref=%s", + height, + width, + num_inference_steps, + guidance_scale, + seed, + ig, + None if ref_latent is None else tuple(ref_latent.shape), + ) + try: + output = super().forward( + z_req, + prompt=None, + height=height, + width=width, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + prompt_embeds=prompt_embeds, + negative_prompt_embeds=negative_prompt_embeds, + generator=generator, + output_type="pt", + return_dict=True, + ) + finally: + set_forward_context_ref_latent(None) + + if hasattr(output, "output") and output.output is not None: + raw = output.output + elif hasattr(output, "images"): + raw = output.images + else: + raw = output + if not isinstance(raw, torch.Tensor): + raise RuntimeError(f"ZImagePipeline returned non-tensor output: {type(raw).__name__}") + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + "[MingImagePipeline.forward] produced image tensor shape=%s range=[%.3f,%.3f]", + tuple(raw.shape), + raw.float().min().item(), + raw.float().max().item(), + ) + return DiffusionOutput(output=raw) + + +# ---------------------------------------------------------------------- +# Helpers +# ---------------------------------------------------------------------- + + +def get_ming_image_post_process_func(od_config: OmniDiffusionConfig): + """Return a post-process callable that converts the raw VAE tensor to PIL. + + The diffusion engine calls ``post_process_func(output_data)`` where + ``output_data`` is the ``DiffusionOutput.output`` tensor returned by + ``MingImagePipeline.forward``. It has shape ``[B, 3, H, W]`` in ``[-1, 1]`` + (Z-image VAE convention). We run the standard ``VaeImageProcessor`` + postprocess to convert it to ``list[PIL.Image]`` which vllm-omni's + ``OmniRequestOutput.from_diffusion`` then bubbles up as + ``omni_outputs.images`` for serving_chat to base64-encode. + + Registered via ``_DIFFUSION_POST_PROCESS_FUNCS["MingImagePipeline"]`` + in vllm_omni/diffusion/registry.py. + """ + import json + + model_path = od_config.model + vae_config_path = os.path.join(model_path, "vae", "config.json") + try: + with open(vae_config_path) as f: + vae_cfg = json.load(f) + block_out_channels = vae_cfg.get("block_out_channels", [128, 256, 512, 512]) + vae_scale_factor = 2 ** (len(block_out_channels) - 1) + except Exception: + vae_scale_factor = 8 # Ming's Flux-format VAE default + + image_processor = VaeImageProcessor(vae_scale_factor=vae_scale_factor * 2, do_convert_rgb=True) + + def post_process_func(images: torch.Tensor): + # VaeImageProcessor.postprocess with default output_type="pil" + # returns ``list[PIL.Image]``. + return image_processor.postprocess(images.float()) + + return post_process_func + + +__all__ = [ + "MingImagePipeline", + "get_ming_image_post_process_func", +] diff --git a/vllm_omni/diffusion/models/ming_flash_omni/t5_block_mapper.py b/vllm_omni/diffusion/models/ming_flash_omni/t5_block_mapper.py new file mode 100644 index 000000000..75243529e --- /dev/null +++ b/vllm_omni/diffusion/models/ming_flash_omni/t5_block_mapper.py @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2025 The vLLM-Omni team. + +"""T5EncoderBlockByT5Mapper — Ming's per-block T5 stack mapping byte5 features +onto the DiT condition space. +""" + +from __future__ import annotations + +from collections.abc import Iterable + +import torch +import torch.nn as nn +from diffusers.models.modeling_utils import ModelMixin +from vllm.model_executor.layers.layernorm import RMSNorm +from vllm.model_executor.model_loader.weight_utils import default_weight_loader + +from vllm_omni.diffusion.models.t5_encoder.t5_encoder import T5Block + + +class T5EncoderBlockByT5Mapper(ModelMixin): + """Stacks ``num_layers`` T5 encoder blocks on top of byte5 features and + projects them to ``sdxl_channels`` (= Ming's ``diffusion_c_input_dim``). + """ + + def __init__(self, byte5_config, num_layers: int, sdxl_channels: int | None = None) -> None: + super().__init__() + if num_layers > 0: + self.blocks = nn.ModuleList( + [ + T5Block( + byte5_config, + has_relative_attention_bias=(i == 0), + prefix=f"blocks.{i}", + ) + for i in range(num_layers) + ] + ) + else: + self.blocks = None + self.layer_norm = RMSNorm(byte5_config.d_model, eps=byte5_config.layer_norm_epsilon) + if sdxl_channels is not None: + self.channel_mapper = nn.Linear(byte5_config.d_model, sdxl_channels) + self.final_layer_norm = RMSNorm(sdxl_channels, eps=byte5_config.layer_norm_epsilon) + else: + self.channel_mapper = None + self.final_layer_norm = None + + def get_extended_attention_mask(self, attention_mask: torch.Tensor, dtype: torch.dtype) -> torch.Tensor: + if attention_mask.dim() == 3: + extended = attention_mask[:, None, :, :] + elif attention_mask.dim() == 2: + extended = attention_mask[:, None, None, :] + else: + raise ValueError(f"Unexpected attention_mask shape {tuple(attention_mask.shape)}") + extended = extended.to(dtype=dtype) + return (1.0 - extended) * torch.finfo(dtype).min + + def forward(self, inputs_embeds: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor: + extended_mask = self.get_extended_attention_mask(attention_mask, dtype=self.dtype) + + hidden_states = inputs_embeds + position_bias = None + + if self.blocks is not None: + for block in self.blocks: + hidden_states, position_bias = block( + hidden_states, + mask=extended_mask, + position_bias=position_bias, + ) + + hidden_states = self.layer_norm(hidden_states) + if self.channel_mapper is not None: + hidden_states = self.channel_mapper(hidden_states) + hidden_states = self.final_layer_norm(hidden_states) + return hidden_states + + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: + """Load Ming's HF-format byte5_mapper checkpoint into the fused + TP-aware layers. + + Source format (from ``byte5_mapper.pt``): + blocks.{i}.layer.0.SelfAttention.{q,k,v,o}.weight + blocks.{i}.layer.1.DenseReluDense.{wi_0,wi_1,wo}.weight + blocks.{i}.layer.{0,1}.layer_norm.weight + {layer_norm, channel_mapper, final_layer_norm}.{weight,bias} + + Target format (after ``T5Block`` from t5_encoder.py): + blocks.{i}.layer.0.SelfAttention.qkv_proj.weight (fused q+k+v) + blocks.{i}.layer.1.DenseReluDense.wi.weight (fused wi_0+wi_1) + (others identical) + """ + stacked_params_mapping = [ + (".qkv_proj.", ".q.", "q"), + (".qkv_proj.", ".k.", "k"), + (".qkv_proj.", ".v.", "v"), + (".DenseReluDense.wi.", ".DenseReluDense.wi_0.", 0), + (".DenseReluDense.wi.", ".DenseReluDense.wi_1.", 1), + ] + params_dict = dict(self.named_parameters()) + loaded_params: set[str] = set() + + for name, loaded_weight in weights: + matched = False + for target_substr, source_substr, shard_id in stacked_params_mapping: + if source_substr not in name: + continue + target_name = name.replace(source_substr, target_substr, 1) + if target_name in params_dict: + param = params_dict[target_name] + param.weight_loader(param, loaded_weight, shard_id) + loaded_params.add(target_name) + matched = True + break + + if matched: + continue + + if name in params_dict: + param = params_dict[name] + weight_loader = getattr(param, "weight_loader", default_weight_loader) + weight_loader(param, loaded_weight) + loaded_params.add(name) + + return loaded_params + + +__all__ = ["T5EncoderBlockByT5Mapper"] diff --git a/vllm_omni/diffusion/models/z_image/pipeline_z_image.py b/vllm_omni/diffusion/models/z_image/pipeline_z_image.py index 345ccca65..388b6316c 100644 --- a/vllm_omni/diffusion/models/z_image/pipeline_z_image.py +++ b/vllm_omni/diffusion/models/z_image/pipeline_z_image.py @@ -832,5 +832,8 @@ class ZImagePipeline(nn.Module, DiffusionPipelineProfilerMixin): loaded_weights = loader.load_weights(weights) # Record components loaded by diffusers submodules to satisfy strict checks. loaded_weights |= {f"vae.{name}" for name, _ in self.vae.named_parameters()} - loaded_weights |= {f"text_encoder.{name}" for name, _ in self.text_encoder.named_parameters()} + # downstream pipelines (e.g. MingImagePipeline) may set ``self.text_encoder = None`` when they + # bring their own conditioning path. + if self.text_encoder is not None: + loaded_weights |= {f"text_encoder.{name}" for name, _ in self.text_encoder.named_parameters()} return loaded_weights diff --git a/vllm_omni/diffusion/registry.py b/vllm_omni/diffusion/registry.py index d8302c115..fe7d00c77 100644 --- a/vllm_omni/diffusion/registry.py +++ b/vllm_omni/diffusion/registry.py @@ -141,6 +141,11 @@ _DIFFUSION_MODELS = { "pipeline_bagel", "BagelPipeline", ), + "MingImagePipeline": ( + "ming_flash_omni", + "pipeline_ming_imagegen", + "MingImagePipeline", + ), "InternVLAA1Pipeline": ( "internvla_a1", "pipeline_internvla_a1", @@ -467,6 +472,7 @@ _DIFFUSION_POST_PROCESS_FUNCS = { "WanI2VDMD2Pipeline": "get_wan22_i2v_post_process_func", "LongCatImagePipeline": "get_longcat_image_post_process_func", "BagelPipeline": "get_bagel_post_process_func", + "MingImagePipeline": "get_ming_image_post_process_func", "InternVLAA1Pipeline": "get_internvla_a1_post_process_func", "LongCatImageEditPipeline": "get_longcat_image_post_process_func", "StableDiffusion3Pipeline": "get_sd3_image_post_process_func", diff --git a/vllm_omni/engine/cfg_companion_tracker.py b/vllm_omni/engine/cfg_companion_tracker.py index 1105e887c..aeec9e848 100644 --- a/vllm_omni/engine/cfg_companion_tracker.py +++ b/vllm_omni/engine/cfg_companion_tracker.py @@ -22,17 +22,18 @@ class CfgCompanionTracker: self._companion_to_parent: dict[str, str] = {} # companion -> parent self._done: dict[str, set[str]] = {} # parent -> completed companion ids self._pending_parents: dict[str, dict[str, Any]] = {} # parent -> deferred result + self._companion_outputs: dict[str, Any] = {} # companion_id -> engine output def is_companion(self, req_id: str) -> bool: return req_id in self._companion_ids - def has_companions(self, parent_id: str) -> bool: - return parent_id in self._companion_map - def get_parent_id(self, req_id: str) -> str | None: """Return the parent request id for a companion, or None.""" return self._companion_to_parent.get(req_id) + def has_companions(self, parent_id: str) -> bool: + return parent_id in self._companion_map + def all_companions_done(self, parent_id: str) -> bool: role_map = self._companion_map.get(parent_id, {}) done_set = self._done.get(parent_id, set()) @@ -52,6 +53,20 @@ class CfgCompanionTracker: self._companion_ids.add(companion_id) self._companion_to_parent[companion_id] = parent_id + def set_companion_output(self, companion_id: str, output: Any) -> None: + """Stash companion engine output for the parent to bundle at forward time.""" + self._companion_outputs[companion_id] = output + + def pop_companion_outputs(self, parent_id: str) -> list[Any]: + """Pop companion outputs (role-registration order) for bundling into set_engine_outputs.""" + role_map = self._companion_map.get(parent_id, {}) + outputs = [] + for cid in role_map.values(): + out = self._companion_outputs.pop(cid, None) + if out is not None: + outputs.append(out) + return outputs + def on_companion_completed(self, companion_id: str) -> str | None: """Mark done. Returns parent_id only if parent is pending and all companions finished.""" parent_id = self._companion_to_parent.get(companion_id) @@ -86,6 +101,7 @@ class CfgCompanionTracker: for companion_id in companion_ids: self._companion_ids.discard(companion_id) self._companion_to_parent.pop(companion_id, None) + self._companion_outputs.pop(companion_id, None) self._done.pop(parent_id, None) self._pending_parents.pop(parent_id, None) return companion_ids diff --git a/vllm_omni/engine/orchestrator.py b/vllm_omni/engine/orchestrator.py index ae0c82765..48026abab 100644 --- a/vllm_omni/engine/orchestrator.py +++ b/vllm_omni/engine/orchestrator.py @@ -741,7 +741,10 @@ class Orchestrator: finished = output.finished submit_ts = req_state.stage_submit_ts.get(stage_id) + # CFG companion: stash output so parent can bundle [parent, *companions] + # into source_outputs for the bridge (e.g. thinker2imagegen). if finished and self._cfg_tracker.is_companion(req_id): + self._cfg_tracker.set_companion_output(req_id, output) await self._handle_cfg_companion_ready(req_id) await self._cleanup_request_ids([req_id]) return @@ -933,12 +936,34 @@ class Orchestrator: requires_multimodal_data = getattr(next_client, "requires_multimodal_data", False) if next_pool.stage_type == "diffusion": + companion_outputs = self._cfg_tracker.pop_companion_outputs(req_id) + expected = len(self._cfg_tracker.get_companion_request_ids(req_id)) + if expected > len(companion_outputs): + logger.warning( + "[Orchestrator] req=%s: only %d/%d CFG companion outputs arrived; " + "downstream CFG conditioning may degrade", + req_id, + len(companion_outputs), + expected, + ) + diffusion_source_outputs = [output, *companion_outputs] if next_client.custom_process_input_func is not None: _t_ar2d = _time.perf_counter() - diffusion_prompt = next_client.custom_process_input_func( - source_outputs, + _fn = next_client.custom_process_input_func + _extra_kwargs: dict[str, Any] = {} + # TODO: replace signature probe with explicit kwarg contract. + try: + import inspect as _inspect + + if "sampling_params" in _inspect.signature(_fn).parameters: + _extra_kwargs["sampling_params"] = params + except (TypeError, ValueError): + pass + diffusion_prompt = _fn( + diffusion_source_outputs, req_state.prompt, requires_multimodal_data, + **_extra_kwargs, ) _dt_ar2d = (_time.perf_counter() - _t_ar2d) * 1000 req_state.pipeline_timings["ar2diffusion_ms"] = _dt_ar2d diff --git a/vllm_omni/entrypoints/openai/serving_chat.py b/vllm_omni/entrypoints/openai/serving_chat.py index ec10606a1..06ccfcd70 100644 --- a/vllm_omni/entrypoints/openai/serving_chat.py +++ b/vllm_omni/entrypoints/openai/serving_chat.py @@ -4,6 +4,7 @@ import json import time import uuid from collections.abc import AsyncGenerator, AsyncIterator, Callable +from dataclasses import fields, is_dataclass from datetime import datetime, timedelta, timezone from io import BytesIO from typing import Any, Final, cast @@ -410,8 +411,9 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): engine_prompt_image = None # Override the prompts produced by chat-template preprocessing. + is_img2img = engine_prompt_image is not None tprompt: OmniTextPrompt = {"prompt": extracted_prompt} - if engine_prompt_image: + if is_img2img: tprompt["modalities"] = ["img2img"] else: tprompt["modalities"] = ["image"] @@ -428,6 +430,10 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): mm_processor_kwargs["target_h"] = height if width is not None: mm_processor_kwargs["target_w"] = width + # Pass output modalities so model-specific MM processors can + # detect image-generation requests and apply their own prompt + # rewrites (e.g. query-token expansion, placeholder injection). + mm_processor_kwargs["modalities"] = ["img2img"] if is_img2img else ["image"] tprompt["mm_processor_kwargs"] = mm_processor_kwargs if engine_prompt_image is not None: tprompt["multi_modal_data"] = engine_prompt_image @@ -442,6 +448,21 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): for k, v in engine_prompt_image.items() } + # Forward image_gen extra_args from diffusion-stage sampling + # params into the prompt dict so that thinker2imagegen can + # access them (e.g. byte5_text for ByT5 glyph rendering). + if hasattr(request, "sampling_params_list") and request.sampling_params_list: + for sp_raw in request.sampling_params_list: + ea = ( + (sp_raw.get("extra_args") or {}) + if isinstance(sp_raw, dict) + else (getattr(sp_raw, "extra_args", None) or {}) + ) + ig = ea.get("image_gen") or {} + if ig: + tprompt["image_gen_extra_args"] = ig + break + engine_prompts = [tprompt] # Store height/width for applying to diffusion stage sampling params later _image_gen_height = height @@ -770,13 +791,31 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): return new_messages - def _to_sampling_params_list(self, sampling_params_list: list[dict]) -> list[SamplingParams]: - final_sampling_params_list = [] - for sampling_params in sampling_params_list: + def _to_sampling_params_list(self, sampling_params_list: list[dict]) -> list[Any]: + """Convert request dicts to stage-typed sampling params objects. + + For diffusion stages, build ``OmniDiffusionSamplingParams`` so + downstream ``StageDiffusionClient._sampling_params_to_dict`` (which + requires a dataclass) works. For LLM stages build ``SamplingParams``. + """ + stage_configs = list(getattr(self.engine_client, "stage_configs", []) or []) + final_sampling_params_list: list[Any] = [] + for idx, sampling_params in enumerate(sampling_params_list): + stage_type = get_stage_type(stage_configs[idx]) if idx < len(stage_configs) else "llm" + target_cls = OmniDiffusionSamplingParams if stage_type == "diffusion" else SamplingParams if isinstance(sampling_params, dict): - final_sampling_params_list.append(SamplingParams(**sampling_params)) - elif isinstance(sampling_params, SamplingParams): + final_sampling_params_list.append(target_cls(**sampling_params)) + elif isinstance(sampling_params, target_cls): final_sampling_params_list.append(sampling_params) + elif isinstance(sampling_params, SamplingParams | OmniDiffusionSamplingParams): + # Cross-typed (e.g. user passed SamplingParams but this is a + # diffusion stage) — rebuild via a dict round-trip so we end + # up with the correct target class. + as_dict = { + f.name: getattr(sampling_params, f.name) + for f in (fields(sampling_params) if is_dataclass(sampling_params) else []) + } or sampling_params.__dict__ + final_sampling_params_list.append(target_cls(**as_dict)) else: raise ValueError(f"Invalid sampling params: {sampling_params}") return final_sampling_params_list diff --git a/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni.py b/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni.py index 0111f8069..fd52dcbf8 100644 --- a/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni.py +++ b/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni.py @@ -126,15 +126,21 @@ class MingFlashOmniForConditionalGeneration( self.make_empty_intermediate_tensors = self.thinker.make_empty_intermediate_tensors elif self.model_stage == "imagegen": - # TODO: Implement image generator stage + # Image generation is a separate diffusion stage; it does not run + # through this AR wrapper. See + # ``vllm_omni/diffusion/models/ming_flash_omni/pipeline_ming_imagegen.py`` + # and ``model_arch: MingImagePipeline`` in the stage YAML. raise NotImplementedError( - "Image generation stage is not yet implemented. Please use model_stage='thinker' for now." + "Image generation stage is not implemented in this AR wrapper. " + "Use stage_type: diffusion with model_arch: MingImagePipeline " + "for the imagegen stage (see stage_configs/ming_flash_omni.yaml)." ) else: raise ValueError( f"Invalid model_stage: {self.model_stage!r}. Must be one of: 'thinker', 'imagegen'. " - f"For the talker stage, use MingFlashOmniTalkerForConditionalGeneration directly." + f"For the talker stage, use MingFlashOmniTalkerForConditionalGeneration directly. " + f"For image generation use stage_type: diffusion with model_arch: MingImagePipeline." ) def forward( @@ -177,9 +183,43 @@ class MingFlashOmniForConditionalGeneration( raise NotImplementedError("get_mrope_input_positions not available on current stage") def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: - stripped = ((name.removeprefix("thinker."), value) for name, value in weights) - thinker_loaded = self.thinker.load_weights(stripped) - return add_prefix_to_loaded_weights(thinker_loaded, "thinker") + loaded_weights: set[str] = set() + thinker_weights: list[tuple[str, torch.Tensor]] = [] + + for name, value in weights: + if name.startswith("thinker."): + thinker_weights.append((name, value)) + elif name.startswith("imagegen."): + # Imagegen weights are loaded by MingImagePipeline (diffusion + # stage) from its own checkpoint subfolders; silently drop + # them here so the AR wrapper does not try to load them. + continue + elif name.startswith("talker."): + # Talker weights are loaded by + # MingFlashOmniTalkerForConditionalGeneration directly; drop. + continue + else: + # Weights without prefix go to thinker by default + thinker_weights.append((name, value)) + + if self.model_stage == "thinker" and thinker_weights: + stripped = ((name.removeprefix("thinker."), value) for name, value in thinker_weights) + thinker_loaded = self.thinker.load_weights(stripped) + thinker_loaded = add_prefix_to_loaded_weights(thinker_loaded, "thinker") + loaded_weights.update(thinker_loaded) + + # ``query_tokens_dict.*`` parameters are pre-loaded inside + # ``MingFlashOmniThinker.__init__`` from + # ``/mlp/model.safetensors`` (not the main shard index), + # so vllm's default loader does not see them via the standard + # ``load_weights`` path. Report them as loaded here to satisfy the + # post-load completeness check. + query_tokens_module = getattr(self.thinker, "query_tokens_dict", None) + if query_tokens_module is not None: + for scale_name, _param in query_tokens_module.items(): + loaded_weights.add(f"thinker.query_tokens_dict.{scale_name}") + + return loaded_weights def get_mm_mapping(self) -> MultiModelKeys: return MultiModelKeys.from_string_field( diff --git a/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni_thinker.py b/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni_thinker.py index bde7477b9..0a9ce0f5c 100644 --- a/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni_thinker.py +++ b/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni_thinker.py @@ -6,6 +6,7 @@ """Ming-flash-omni-2.0 Thinker stage implementation (multimodal understanding).""" +import os from collections.abc import Iterable, Iterator, Mapping, Sequence from typing import Annotated, Any @@ -113,7 +114,9 @@ class MingFlashOmniThinkerProcessingInfo(Qwen2VLProcessingInfo): return 1 def get_supported_mm_limits(self) -> Mapping[str, int | None]: - return {"image": None, "video": None, "audio": None} + # ``img2img`` is the serving-layer alias for a reference image in the + # image-gen path; treated as a regular image by ``_MingMultiModalDataParser``. + return {"image": None, "video": None, "audio": None, "img2img": 1} def get_mm_max_tokens_per_item( self, @@ -137,7 +140,14 @@ class MingFlashOmniThinkerProcessingInfo(Qwen2VLProcessingInfo): return mm_max_tokens - def get_feature_extractor(self, **kwargs: object) -> MingWhisperFeatureExtractor: + def get_feature_extractor(self, **kwargs: object): + """Return the audio feature extractor from the processor. + + The processor may be loaded via trust_remote_code paths that return + a stock transformers ``WhisperFeatureExtractor`` rather than + vllm-omni's subclass ``MingWhisperFeatureExtractor``. We accept both + as long as the caller-needed attributes (``sampling_rate``) exist. + """ hf_processor = self.get_hf_processor(**kwargs) feature_extractor = hf_processor.audio_processor assert isinstance(feature_extractor, MingWhisperFeatureExtractor) @@ -145,13 +155,56 @@ class MingFlashOmniThinkerProcessingInfo(Qwen2VLProcessingInfo): def get_data_parser(self): feature_extractor = self.get_feature_extractor() - return MultiModalDataParser( + return _MingMultiModalDataParser( target_sr=feature_extractor.sampling_rate, target_channels=self.get_target_channels(), expected_hidden_size=self._get_expected_hidden_size(), ) +class _MingMultiModalDataParser(MultiModalDataParser): + """Rewrite ``img2img`` → ``image`` in-place so the thinker embeds the ref. + + Matches Ming's upstream (``processing_bailingmm2.py::__call__``): when + ``images`` is provided the image_processor runs → pixel_values + image_grid_thw, + and ``_expand_image_tokens`` expands ```` placeholders in the text + to ``*N``. The same PIL then ALSO goes to the diffusion stage + as ``image_gen_pixel_values_reference``. + + On vllm-omni's serving path serving_chat emits + ``multi_modal_data={"img2img": PIL}`` (Bagel convention). We mutate the + caller's mm_data dict here — moving the PIL from the ``img2img`` key into + the ``image`` key — so that: + + * vLLM's renderer validator sees consistent keys in + ``mm_data`` and the parser output (``mm_data_items["image"]``) + * The thinker-side image path (``_call_hf_processor`` reads + ``mm_data["images"]``, ``_get_prompt_updates`` replaces ```` + placeholders, ``_get_mm_fields_config`` declares image field layout) + all run normally with the ref PIL acting as a regular image input + * ``thinker2imagegen`` still finds the ref PIL — but now under + ``multi_modal_data["image"]`` rather than ``["img2img"]``. + + A companion prepend in ``_apply_hf_processor_main`` ensures the prompt + text carries a matching ```` placeholder when needed. + """ + + def parse_mm_data(self, mm_data): + if "img2img" in mm_data: + img2img = mm_data.pop("img2img") + if not isinstance(img2img, list): + img2img = [img2img] + existing = mm_data.get("image") + if existing is None: + existing_list: list[Any] = [] + elif isinstance(existing, list): + existing_list = list(existing) + else: + existing_list = [existing] + mm_data["image"] = existing_list + img2img + return super().parse_mm_data(mm_data) + + class MingFlashOmniThinkerDummyInputsBuilder(BaseDummyInputsBuilder[MingFlashOmniThinkerProcessingInfo]): def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str: num_images = mm_counts.get("image", 0) @@ -185,6 +238,8 @@ class MingFlashOmniThinkerDummyInputsBuilder(BaseDummyInputsBuilder[MingFlashOmn audio_length = int(audio_duration * sample_rate) + num_img2img = mm_counts.get("img2img", 0) + mm_data: MultiModalDataDict = { "image": self._get_dummy_images( width=image_width, @@ -200,6 +255,13 @@ class MingFlashOmniThinkerDummyInputsBuilder(BaseDummyInputsBuilder[MingFlashOmn "audio": [(np.random.randn(audio_length).astype(np.float32), sample_rate) for _ in range(num_audios)], } + if num_img2img > 0: + mm_data["img2img"] = self._get_dummy_images( + width=image_width, + height=image_height, + num_images=num_img2img, + ) + return mm_data @@ -396,6 +458,61 @@ class MingFlashOmniThinkerMultiModalProcessor(BaseMultiModalProcessor[MingFlashO ) -> bool: return False + def _apply_hf_processor_main( + self, + prompt: str | list[int], + mm_items: MultiModalDataItems, + hf_processor_mm_kwargs: Mapping[str, object], + tokenization_kwargs: Mapping[str, object], + *, + enable_hf_prompt_update: bool, + ): + """Apply Ming-specific prompt rewrites before delegating to vllm's + default text / multimodal processing pipeline. + + This is the single point in the base MM-processor call graph where we + still have BOTH the full prompt and the request-level + ``hf_processor_mm_kwargs`` together. Downstream, vllm's MM caching + design splits the work into + :meth:`_apply_hf_processor_text_only` (prompt, empty kwargs) and + :meth:`_apply_hf_processor_mm_only` (dummy text, real kwargs), so a + per-request flag like ``modalities`` cannot reach the real + tokenization from inside :meth:`_call_hf_processor`. + + For Ming-flash-omni-2.0, image-generation requests carry + ``mm_processor_kwargs["modalities"]`` set to ``["image"]`` (t2i) or + ``["img2img"]`` (image edit). We expand the prompt here with the + ``*N`` query-token block so the expanded + form flows through both the tokenization and (eventual) MM paths. + For img2img, we also prepend ```` so the thinker-side prompt + replacement can locate the reference-image placeholder. + """ + modalities = hf_processor_mm_kwargs.get("modalities") or [] + is_image_gen = "image" in modalities or "img2img" in modalities + if isinstance(prompt, str) and is_image_gen: + from vllm_omni.model_executor.models.ming_flash_omni.prompt_utils import ( + DEFAULT_NUM_QUERY_TOKENS, + maybe_expand_image_gen_prompt, + ) + + # img2img: prepend ```` so the ref-image placeholder survives + # MM caching (when the ref image is cache-warm, the thinker sees 0 + # missing items and cannot inject the placeholder itself). + if "img2img" in modalities: + prompt = "" + prompt + + ig = getattr(self.info.ctx.model_config.hf_config, "image_gen_config", None) + num_query_tokens = getattr(ig, "num_query_tokens", DEFAULT_NUM_QUERY_TOKENS) + prompt = maybe_expand_image_gen_prompt(prompt, num_query_tokens=int(num_query_tokens)) + + return super()._apply_hf_processor_main( + prompt=prompt, + mm_items=mm_items, + hf_processor_mm_kwargs=hf_processor_mm_kwargs, + tokenization_kwargs=tokenization_kwargs, + enable_hf_prompt_update=enable_hf_prompt_update, + ) + def _call_hf_processor( self, prompt: str, @@ -408,6 +525,11 @@ class MingFlashOmniThinkerMultiModalProcessor(BaseMultiModalProcessor[MingFlashO We call the image/audio sub-processors directly (instead of going through `MingFlashOmniProcessor.__call__`) so that the high-level placeholder tokens remain **unexpanded** in the tokenized output. + + NOTE: Image-gen prompt expansion is handled in + :meth:`_apply_hf_processor_main` — by the time we get here, vllm's + MM caching has already split text/mm processing and ``mm_kwargs`` + is empty on the text-only branch. """ hf_processor = self.info.get_hf_processor() tokenizer = self.info.get_tokenizer() @@ -425,6 +547,8 @@ class MingFlashOmniThinkerMultiModalProcessor(BaseMultiModalProcessor[MingFlashO videos = mm_data.get("videos", None) if videos is not None: + # TODO: ``videos=`` on image_processor is deprecated since + # transformers v4.57 (removed in v5); migrate to Qwen2VLVideoProcessor. video_outputs = hf_processor.image_processor( images=None, videos=videos, @@ -487,8 +611,13 @@ class MingFlashOmniThinkerForConditionalGeneration( return "

+?TV2Jsfnq>388Q zLtRy$QLQpYmiiDH0KcA=a$Cq+jB~2E^gB!&IXU$l_G^#hZ_zBL3#L7jN@pYNh+1o7vvCKU>%i^(c3w3w0^ zAf*a-(c69Sm3N5BHAAab$$g!U#(?m6MVD41B>5-eddp-(}R^WR5rd`07>~I_}sLTtzLplhHW+1K|KjtjBSUDZo#HPd;v8j=tq<#yQ~07 zXiCr)G#6HYaJXu6AliNT!?0zH;61P)*)h22bH0Aum?pr_zT-nng`aED&zwxBzG>Kw5=ISq;8iPsRS$Fb0}0A*e#6`Z5j!Dy;M|^rwNFLXt7FDIm%kedtsI?U z#u_p&@^}s3Mmm4Dk|nvVKfI3A3t`5&Y69LXsF@y*gYAMeY$wA}&T1ZK;G4W004_u3 zPjE!dgEHm;k)wmccwnVlpYy&32VGX$>HYk*-O#jzb?hGTqdTs9 zSaTH$gWiGyl~NJ0Lv+*etn353g}4}Xg61-en(U*g%N=)K zI2Ez^YJ*!zAY;$;(R5*~-n($|`Hc7?D*^q%7(G7!E#$$IQhD_DILbhj>bNFf5EkJo zoDQ;hqTwWHV~5e6I9Z(e-_`2QoVWytWOE4 zM!!}u&SX9~yTp6=j#H0y*v7~Av^0l9xw0`_cLL}D3pztYw~&;NC6xxs2pN<#Y|4_8 zt~h$hp=4;-4vmrt(A|JtDu%!9>BZb1574r`1bz3Br8#ns?OGA@QY~|bZ$I?+|2v5m zkX}vN3jEY@igZLW*shAu2%L5$bWkg6llUtbda}-OwS353<^mAfGDkzFb!c*1f(6|A zp~Zh9y)>;6{zB1()fFZxd%3a}nIWPt-s~{Cly(jc8n~OrjS$Jh#ibzQeO6Xmtgr z$+r$l`BEKH-)HSNwQJjq!YyJ^Ogz;Mvp@l^(9VRKJTN=N^SM~kHl!_uJX^R|YfRQv z-;xtAH~a`o8Ky0H>*1ChX>VCHah2J#N4iG-SGFb$79Hz65feIH^JH+iSrp~Tnp(;G zD=;A7*tIQI>ym!I`u<_I%^zD$3Ac5xoP7<@s~bi3h6*3y&g|`1f@~^~0^gK+2MKw=dx@*(QARXkF&+4T5q33R#0hjjle2|1(SAK}ub zU@>Y(rY)taGlddDCx%iD8*Jp%Wa>ZQo7D(%2F$f zsab--DA>YG%twJdkhm9yuuZY{R2D_1gKHlCr*tz)*6s=AB{8DoF0AU^{ld6;_ANZ&@}qb z_e@}(IoNU)Qy1kaCawZt@Pw`0irCWUplRQnS@O+I#6*f-!ZvIZ(t15$bC091lw`u7 zH+zT8d`gZPvW@fD-(hvX8*?tmN$zqC07R88TIwNUQXn9S>KKUa+180V#)7UPIW-M; z?01@v#z{kjq{L}_T^*F1Fdv}`3z7a*j3Q%XImFu&moqAOjX&Dto*Vt?!2OcWh+^@8 zbNq!}g;)2y9Der)5#$M1OrR!1inIuT-1$FiZYFHNe%7$?pMXC<3xjIa)dYLy9yAeB z?`G~mJ~fjT1)$!t{uJwuL*HE9#E|vT@Gi_t!CLmtrs#%@vpCdtx_{cvmtH#B@qpp( z3fSjp>J*dvO#p7>3N`f#YPm|HIw7G`P{mSu$+%a8cA+8aJq(F6>JT|hfRCf`a*8&i zwyloO0R@A$8>3oB`ec2Rkj8AB$1l70E})rgJver^Eb_j9SG`%YYzu+=k&Q3S*IW~X zQ3kpAq8G9ZEfp2s)ZR*33yuNMB;3r4)4~j*GB(H&iA}WTCIfy~=dnL?=Nrp2<Z3t7iH3XN3{@vvv-(syaeJ;LTn3rzc_gAca zndT}xcJ51~ly_R#GqAb;lM4bgISaLZfG<+3592MI%2=fqBo~Np$+EaG%^OgkW~eU} zwiB|9{KNeXT8+s+!wXnk1}H-MBJGJbyPUeGo4Up>=H&wzm^qSNT`@LY7k^#c`0++@ znD5N0k6a9Z*_PuZ_$X^e7w-_D!4GB$A4>#WPjvk(0W&a_Co2J%nWxc|2}0@O9Wqf0 zNYqwRehVbE;HC&~`jR1q7z(Y54j|8E)XIHs>)*%^G@68qD)e(@=^SFUtK&s>C zMVLB2>;KZ=N{cEx7IvOE|1U9Apb~wRxak~g__6w$_P7DI%oAT4q^X7?spuAiOS1%! zee?@SOB|2nGta+qj5WA~1;C%0h0pDiFFAU_J+k%;+?;xhrq2=EH#lwTa&*7Pu=DED zeM&PQBKt&`sTQ58j@YYDz~TSOYtjJJBn!}86#P&jOu_$Qw^YwL0ui{ka9ezsQn7iw zM;_q0Afb!I>mTojceM74DB|-$1}G1yl649vmIa0lV%*x@1&rbr|3IuJFXY9if(4cm ztMUGBCv(BGZj3DVIM8t=a74VI0tz3mza42YIhLNX=1;ikREC>(lh!JHDP>Ip8QTLi z7@^7y+5~C1gQ)>M4wJ?m&Za8mvAg?kf zp1hw|)f1ub;J(lFXQGPDmIcn)$5sW-v4%c(UMBIT#|zb7<<%f_p(;1S=8CUq9xtVQbOzR&6CHrDuh zIh16wi4PAK%O}KfQb99mrHe8i`CldV_q&GE^Y@Z`+Eph1bRvreJ2-Y3DKR4~2(SI)AX`uECz-oUZU`10v=y|LBW(hrl_LsO$sTyE{M59^4dj@b_ zH$cpMq_&tPj7St82+;jq>2*9|ZB(>N4+R;CG$C`=2Kp4Lk&WQ|6q3 zv6j;k&+hRrQdZ)L{S-(5CxYphR`%8q=5*6_D7Ez!+z zv7Fe-$>48+!D;}rX*bsiQtk}0_ zB4Mg5wy*CGgMZJ#7sqH$gMDOY`#g#Q4;O^wap^^p&VmuB(yo;j=y^ElwLH+_z_UI_ zI{a`XYkK;TQGmba!*m@*ux?Ami}M!c9d$2M7i!`s%E2;#=aKdt`|U^wjLW4{bRn_kz`?qs#mQ{f9hGSLURYeL0@1 zSngp&lJ5}=eNv@pZbPRh#FA!v`Xwk#aD}4Fw`bVx73F{Lw(3kx(C=~dE((LP$8G!d z&yHsO5dMR^DxiWEb5_euYxCW;IVYE{vc*n;oXHtH?k`ZcJ>{eXvvvZ3zoRg`2P6I*SFzyfgg$r zi-tGw8mF%H@jg7R9PC&NWoQ=oxF%2P5}UJ402*I1Vn|QM9Ap~XS9{+~?lO6;OWEIT zooher;0~&h6})|AWfqT>`I$O#4AXu2sjHU%Oz>$Wtip9jMw4aUd(cjVwpTPcs4+U) z)`a#zUWz+!vzx#pUYF*HI` zPmxxF6Fu__g~x77CtVjj(Jxg~ncm%w9wx~Of!IjaK0X4P!Sa@%iSk`~?md%b`vdjb zoE*`9Sl!}ep1wP;qhZHcxwGuyWPJFyy-$0YJ4+eq!fC^h6q|KKIf#b_1*3KS*s1`0 z5jW()NH&yI7E4r0)Mnv5{v%nwcGEwG`4iizbI0nw6mQh8c(pBT#wxWq+_0^T@zQcq zxvlxz!G@bg?tULVRU=%5FTThfDCAjS=I0t9^=VN0r>QsMt#*P6s&Wxx`x+E6C`<6I z*j`izc9*r?kkgklGAG=5m8fR{qhHq93JFpTGjGT{N$)ItoYqwLRVxkL5UWQt;?=U4 zcEqVs)=30mbl?bK6?O+$(MNlqM-RcP8B;s9c%*h(RcD~Q4=4JFzML8VVpTR7Z$(Wa z8CxHnvh)cxWlX9U*<=K}bszewyk8sPez@nck8@01#7coEh6#*lK3P)?f^sl4V6L)8 zg$PA8k^W0RB_zXjcRlPRfO`(Zc&t_&l!cDQ(lVbochV|GeVh3yMs`fD zMs+*?vnb1-mEHiV-8u|cVfHK`&H6YIR&yuUes_P?xjv-DV9Wcb2C6&uSn7Ep+T(W% zMrK;FVz(Ajq0u_49JR|Z&G1y-y{>fkGH18y{%Z5G1FshaeQX=_?+&TU+`==mWw!`q z->7hyM6bfL z%ZjN7(qXM19$9L;VkEgl`VfMc)Pbn2nd`2b?7t;%v{yI%c#5S8!MmjYDANF zpi|^4_DjS?HyjaH{la3G^3U0#bW7!}jtG;og7EC9mt+eoS^B?UT`sKgT9rElUD>c9 zE9CULZ6%l7cdQ@~R=5$C^v{5vsV2HWS}cM0&6|sLAnoM!-Sz zljey}7v|&71y58Nk4L7(2e<|J-=wMocS}b6a|>IO3ZbHKAJTkm6)nDaRNv04HRe?O zshE{D{pZ;~y6$Q~UM%SCq=ogsNVNoXrxp7hZpL~SctoUsqa!9R?n0Esc~wnKm;0`k?u}l1zq~BQcGEQV zf?y81O?x~7q~U-23zVfWTkgV#0Pt@%^h_h;WC~zF#yN}7L@6^*rz@w$X?wR5`K@c| zm?0%8vSF7@NJ=4dEvGsHg#CFY&w!O=(7e02mfS+z0bd7>FbtCR(q;)Js2G%A@I?*^ zNea84Z-s0cnrP7l;~|=yX`3hXs>(92H~KpgM9?iHtdZ925P`>G>;bAWVzrCkiVQ96=+pb@%{0U~XF0=UZ)sHtI=uvkDF z3Xyp#i`8p~aIvJCEM)19@C^__25!P!g6=@J_sgdG8b3aWA~j2vGwS7@t<)zYX?m2TVeGb6xPUV?{aSR3-Lf+zFXT;1Lqk&}D68 z>>1bt6${XAG-O*%x$Q9L8!@Qv{(@e2$D)l zN{q&6FS0qU!QkF*8Ef(2sJ_V9A8hk!OpI5?2Z1JrN;z*~E)%}q5R^AUp z{zLA2U4e9pif&5TNW8?U{YKcgM4&?nIrD?|gsz0Qr-tbD?6BgYcA5r=^GX2JbXIU; z0yU49w6JvX*9L+Wj>{1{S$u>2a3dV2$V-$_7BTnrHY_@8b@#YnMFaVSbi$aq2Y~UE zhUH;$U7!BunW15h9NOZwgfVs6#eD^#jDJ;OV zQbOVbe$f=-cIYixX6fc+(9uJ_6r_zF0hFD7o<+q4%c+UQevHI+ijk9XxP1**mj1b* zdun8IXz3@H+P)1>pH%lvM4p*AG%OhBfTaPlY_fuA5Qzoa9zup3oHa!W+G7&E7zVae z2CZc>VY_CIE@netNnMq^8~*r6PSj#XqN$HQPve_-6XBp- zR(}cF4#3+*FlZtKX0%?DZS&s|Uz5_X^*<5c7exHgI&wRU6`S35H{_x9Z&3Hx!R`nf z<>E2#sw*K+pVWw|?|ylZX1TYc2>;1k0~8>1Vt}OjO~b`@+(YeD_tE9BP|4~R zlo4Z6B^@^;=|W#E2}}sknG1SG{ENN^zTKL{$5R=$phcse-3;kZ?JJ8hcStw7Yuyp0 zN<{@_pX;vw9_YG~8~iv_d@YGvI8>4o@BAyjN>F{17I5bjG-Oy@Jfbq(%Ly(Rv9-N; znqA~K7&a4`Q#|MlqQgx5pIakBf`dcO+z1f+i33`V8+kma;jD1pj|E;EHf&4(li-W$ zjEWDG-V3kGyH-Auos~6y;_?ztGHQB^++sluR`3;2|9tzLO zdKI;*a9kXpbF5vb?etN7J2SIrI!N85#T58=&vAf7<~>Aj%RCdQ#*mDT9YCw(9?Kit zOE4!5r(uVbWk8b<8z{d+>m#(i(aW1N6HGCiu1PAo|MnL%Sj9e75-xtP^4UHbD-Plm z289`j|Iq$o44Fvai&?iz!b$M2l1p|X!w8KsD_KoLNe4v}mKI;EBBJATSXTtb(Q$Fg zL-N(bmQJtZ1H5~ppGHOwZ#mT)qhr4D{Ij|%TkVZOTapVfd&oO{=3_>|C}|-jlB;tx z{9-w$co@wIL_OfW3TIeZsC;MJZ~vu=F5`$@TM{;M_IIzpF{QhQL`0!L(TD2VyVhk! z-hxnzV$B1vTrxgHfe%O$A=tBns$yx%roeDZh~|%kA5f|gW_UMe``oyP?ei2nkxasL z$HNl(V-cC#dH`Ltk|+X@6I>8VqiZT^vr%4>QaxJv|5CdRrrd zn#!HlAe*MK70$TM$ukX<@*v_$%mFzMGQG+XyJ@W!{{~7G)Eehk-KhQYGAIsAZ7Js& zJBi(mDN2K_yQcoKTz2#0DrP6j?8gkDSWvS)gXRD)maEuL! zc=f92<7R0p+!I#3$0LTn#IhrF>qoQ8bBzFVqj;R4?E^&HE_-xs>l0_+&?r zOl_CYM5>^V$Csk)7E@g|rmb*fhwJGHwZdn@41=XG{THoKzSdm=a2SM>0726dvJ zX1^RBl?Pm_`8Ln;N574(jQlNgraD$(0L(S*rAXfc0vX-xi|M&5{^3-Gw9=@V90M>V zZMdtvRFP|-x&X6<68!;e2j@`I@Hba@Ix~i10vZ8AXd&`=-~G=L1CoE~%6t|RT=33s zXw2Ci@9yrNR&bMm$G$+pR;0J%PB3)EhDt~npy)(TtU{B2eG`dq!1kfR@HxqIrzPp$ zP+ev}zJj>=7`j7s1>vSyGIteMr`s0C=A*uW^*r8+V>*jAr0qRO95YFK-8GeA-eSL( z&HZeDJG1KkSawWi#e?vfQ~%i2MV1zZpYIA95odAYBEJRh=}(u5h$l=DxPV z!L-!a@PFe@H_Lo+F8f9GoYPPT@(D}9L0PH7yM;Ov+Q5ak|98;|v|I2$1H%deV`c8Z zyW~n#@uW&moI8;`1vkcnbYxy5n3`h)fjS7rXDBB*8mfc{8g*K}6Skmj3;LhA!T#}N zZ0#B_47v-b|CKAy0DYD)w4<3r{Wj7qF#4>e#z3yS`hV$m@`bfQZ257P@Y)Od@bOzUMEc}0uBh5SqiM@`k2f#|xd7s|l#T2DPd zB+}w0?x3^kECQaDl+sLD%Uq_p=Va1xZ_FS_580U2HO60o?E2H7F7la-v+}c@#r5)x z6J?>kcq8xg%6Qv4$3?52#|{_zpnpgPCh?yguQ{y0NP7yUTgIdA@kY^BASZn;ZX zmT2hM9!@0%-V0Ezou||QTVW?pX7TL@n5U3rr4I6-HE%VT95F^BYh{0;QTZt8d3LTS z=MjVDS_x+X(nxOr8p|$Di3v1vds+`N{1K0gqPw(^f-uzz_eX)F1X7pF|4lD{e}%5N zwsQa7CeWjd-$OgWB+;(@g5hC7nn3J|FL|oT#;ve(=xxM7W}u8ibflh_9Ao9QPQA~P zMUhUc{FsMWO?u%nvQ5V314vsHvO_2qRY3U};emS;&K{@R6uv#tcgH8o+>iE8SQLpS zo>Q)Ye)kEi&<>mf)seceVG=Zq%HBYb^9qfhw=gfNl@o1eHA z`Oz+Z$jaoeYv^U#&>y40Eu{Vqz?uC44*!$nYZ~dAM8zZl4rS^9{)VTa`+nMttpYXi zDK!QB2B6-9AyPsorP?!<%Qs;J@4v(JhS{z4x<0#pz}5l{x>CI>!FaR$Kq38MI3P%4 zBuhc|#*>>vB>m4OTb?Gm^*WlDjO`sf47Ujuq=Mm{OHk@6=pG$g4dlEI{Qm{*peZGo zt1;6&ggQMtYp+8sn<=m-cjkb0GY2!{gF?mRKHn)%;G%v3cRP^qiUYd?>Zi??Q`O)4 zFC~n)oOuCRRrCxoS6xdy)Z>|qMbV_Xu28E#`7&lBBQ0fww15TyR`Mi;P*f{VHv$9Q z@$?qM_H-M%-Vm*Bq(=rzt})lmQTu|?b?twF@SD0MUvr_s99#)p+R<;ScWYBfLCMCffI{;7R-9k1o{m#lL%RW=qkRjaUvy%ZFo8;(w zZ^Vg$-j;^IqAF0r92q9T?9}6MN5@1mFF=R9cyvW*HaTUm#jyW6Wt$VAaSA!`feu%> z!DY~^iR-M9N>UADrmr)1gh24OraEd6o;evLV{Z1RC zNvi%Y-rh8-skCVug+@h02!ap=A-1Tf3~3dlMM&Ba6)`G}CFQ#eLOE3ld15K}rW!A9qd|V9J*n3&i@$Mc|mXU@xhE@g|oc7RrT>?~1r) zOkL#je&hpNu};oiHtr&bpP{5waZOw;3Bsurl(5q|6E|6ekS@mh!IwhceO#F*blhTK z%j;(SFBMt4sduJMKEp180u|jnA?+TlDe-5}Ko1PD*#m!g%`ZH~fcAFRYoRoeOX=+g zo>v?*j&lkplmG}U*N?e$vN#jZcU{eF6&OmCfvD)JNfpGZgx8AN}8$` zJmetsJ`{XN5Y$K3X`wa3CQ43YIF z0v5h;(&+E+wTqC+G={_dUJ%yNHB)DCtkZsd<(u_g1>KHW`*qakCRF*FG;n4fP`@+w zLXZf}FEUBG?*5KGj?mGZV`)j|IYs{8U*>2A-CclfEG)=Wkq=%oOQLs zalpn8^lNzkl(dvwg;LTWssIk8fNNlAMDvGk6Ilbq4~`2CQ`9R);5Hk&dO5Xa=fz)f zlifMSix>+4Ry(?Kb;S28C$4o0z;Mk?2&QwM6Hx1s6wpRF)xol3)F`i_V~DLV43_sp z^7VoosIk72X$fZx{Rm5vDxig<*uqYxm)w(h)%s_M^^ZV#5m2DXGa-!Qf({)e z=;p+_kk10r4_)~?T7&@Q8%W^~(D^>qUQn{ec(nMo39aE5UGkD!X+vb=!FS_>#5dv4 z@(E~}+PSDSt#pDD>055@q^bBkX5#Xmh%Xn0L7k8Eta#a&LO z5eRA)Jloi2GB)V^7aAZ~dJZ$~n_z8%6>nVlaSjoXSuoew6>=|Le>aboUKy1?$l>|+ zQHo;HcWigs;dj!(>?ho0>Awi5h0wzjW5@B;V7M@(U@RjnzZJ`UD0wFgI(_}eW_9Nrx)YNuSmeV<6AVh$)n@sAe@>+SSt9!%faQ2mQos6 zZm_c`6nv<{Qsk)>1;_U$xdxp*vzRFE_*s`@8BnS+nye{>*lm z(834mAWnWIy0V03=mrSOGcPSko&mG_V1kIcm?7{k>%9N0qKxBJ-Bs&W9O|4t$MoU! z9Dfq33-~M+e*a1od`K>6wDK-{h<#HKL{2G`L7C*m>fPPt zY`wjR^DZF9s#oH9**KJWO4ldWHg$1zZD ztc${?$kJ-+r=lwLG9p$_J_Bif=B7AJpHBdFgB#PbqNhBXYXokto%PleE(_}md2OX3 z=0TGhS)Kl|?r_F=*Y8^V3}Oi7H^)q@}D@%nA-1dCpJL5%mH_P_9G$a@0lDV1-B4*Ibgf z8Tp=I2=_jL19-)NhggOb{AF^abH=a#ZA1)#ns5l}EpNvmclgu(z&8kj_g7gGg7O3l z@LI=Bhs?iNW8ZEE;Uxy@{FSsEFqNEOkl{SoB`5}W)S4-^@rW`A{FusmmBEPxbKc3v z)G;+^jof;Om`Js{d+af1ADRk1@F*P3CL{=b2?;ac7*QYL93igQ>hpf1F<+>(5Q-_P z&)(El>F#B$uTptqF9%olI8h8yw(>Tr2TB8@;HbF4yCtK(l4e3e#_`9w z<*8-}aBZs+zKz>lT?ILyWVjYxf;}%LK#RaJ2kYMjm+yX2*0y zIS%pZ7ikxmWTZDv9k_Nq+bXBm_VoIP*{ixE)^~M&DgM~gS}L-=*i*H}cv$c9Rp>Vk zhnuiqB?O+i!Al_RA@+@6OWak*CIH&PP7nmMEwI;CKq-V5k}H3mc3K)t+7ukH4cV4d zFg9m+SKjJkPj4ZaJ%!Vx@GyrLVxG-8Z9kkEt>FeiAwl_r+aAnu8URZGlK#!VYGs9t z+=!4Qe$}cXir9(uI-RaN3qncuu+=>X_SyqG&8AG-dORE!z|&UWCCH(~QrGVzP8=;+ z+g(!bziYg6V}M8S@e8XSUxj{#o^aI}Qy4QJY500uG9| z+sBdd7mvD-<6YefQc`Lj^Ma!maq0OX+tz%({}rFiXU#^-sKyS;l37jIsi~>rwgUn0PEa%Xh@HJFvVq{vSpDSFP_~ z0u$Rt$MmlR#zY6rSGJ|In=e_#B=riNWhEFlbnOsqv;iW z;!|I?*l@!Z)X0zE8qIBIDcH9l6}T>iIuaOs@RnyM$G&Q{U1xKQ9%-97s_h;KK#N0e zPC*@XH7g>M!G%7>dd*d%8-PiLkfl!v)29bP{1DcuT%ga=99-iIj%S269=b|K3~bbR z9qc9;s6-su2SdDHp)|=qZ3B*mfNt63j4_`?$FsnIH&p+uabxXV?hM1Tj z4`xJ`qVz=8jt&;c++A5K$E+L>!+Y>-%b<;>K$My;MR~FsVpx zxoPFGuH!Xu9x&(N_Pn}V^vPjlQU1E82Z9{PHFMZ8NIFgc+8By?Qja~lT}5O_lyZJZa0R>kil|zk$$dhi8Q$ zkT}EYF$5KiyB}>=OnZjwsQEOF7f5Q_aEYYvkzTd2$MG(85Qu4vf|s&3g%9E*o4pD; zY{x$T$ZjO*mccmSNG2yy$`AO9FEyDm733D(L|W5r?^yG5dU};_)8~feKmVMI#{S2{ z(+)XRyo0=|68XdJ?+7s>^FJP}I|4#SGB zM93t169OYUl50OI0SuHhM?NiY0DVMU$T&i~1D)K9Q>KiXhgWHTxatON;r=H-dIrq7 zXdayshyj~7A3meZMtBPPgAg)~8Lc{ma%D_ka>J-o7?=XEhRF%_OSs9tOyAxV=1y6b z&j~n>Q@^bSO}G%5A6#vg|HxqLuFi0khT>(O{tUWse{?mWJ8hmR_WlCsqn%*xK}J#p z(n@ZHbFBI(s6mY;FjLek2WzXxqcmBok9Tct2UA}*WyETQbZeo~5G#CFL0U<8wEdap zdOJXGp4m_=Pf=9AejI(}*I&|q@cA;(S>aFF{bpK0+xvzXo^@t_eR;QIiI3;A`GU7A z!d7pn`mx}z+DgVpAY1bB=VZBf_+NS@>yo;=_hX|1sa%3O0}jBF9V!acaFDg0)hl=s z)EG?_6J)($&;XnozIQGtf$1luEJ71xL5VCg^?EcCj{p1v;#4J~;u!(`Cn}ItR!gf8 zO2Ul5@Lj++oVkM3M`MuqmujbctPUJL0o`VU=?BWGrf85bgF6ZM2PMu_WD)zpxYZVf z+|+iBFXQX=g=WOnrS&Gc{*Y~nrt#5h<(`Im60dIj?)I51|Kc#7`T4Ym0V~4RfsDwT z{dRX#_Bv#0(oJQ~7-`NJ=E|5FpkEKK_?jr{&Db+Q$>+^8m&We+v%1K}zYGJ}H1*Q(|$1x?@b zQD?dPjcLBoi*u7qKM0=ny6X#(3{$*9tSdX62u3aSFAu%p|I?LI|DFBkondyIBT#0L zdN&{dEPVb|E7uUAVwO{VfZ@Yks0HnU;9qn_xK&g}ic*ONMuSl3uXY9ZYyxR9D%b`4 zHy1`a$4^7c83`xQqyk#CGBfWV+mn5vQFZaD`M%@$#et z9eq5+HdT9|_v8{I#%DS1R>Xk22-}Qsl4m^XK;T}7EGqeRHtJSMacEU%Yh-kEbxJXn3A+Z*cIjT!iXfU}GB7W9tnZIn4^l_~(G9zy-(^jm+Sx()T_=t1$|H953A z#KlLLwHtt5HwHKLR`3^3_|C@3s(MF|167^nMV5z^pKN2?_{8^>cRrkxVFyUtKyQc& zp8!=1WU9U*1!Z>GPE;WUMaK1UVIvi~&{mUI zzY^6Ibq1T~5B|a2DFxD=cUtx$l(>j%G&|bEQ5PlWf_*x#z+W%y{QaNS-h5(BoQL#( zgBpNl%sW62@)1JlhS|v`M4)7&EAf*Gn5&EdB3|57kQ=19`VFz;>m4A-_<+>%lINRA zrmn06FY8TJdld>2nI7S1l6NSM**ov;EPok8IhL&){@C-ycJubG61R_D}0+^Xm=P={&>LLSb0z6%&k;6Gx|A)+VUBhVDypD?RdH#@ryU?WxG?t=U_YH|gMEbdC z|AzX;kd~^ z5it)#@m0iiO#>f3ym~^pbjg)%*R}uC{#UO&q1^;HlqVvrVo)%BnrahD5t8hXMX@r7 zo$NvcGH>j}K|<$@M~xeyd63ZlRqJfqO{*OpIo%*9p3cKkoU>M!{BFfb4jIHgMzLuvcXG zm?Tb6(WW>JC-(zErVA?9QL=G_g&$!VL-0Icb1!B?lbEnfn7dK;KyVo?6@!*El>AfY z>2_-*Z_uVUf@?%Mn_|X24HRZwz|cwSaI!X*^k!aM3jBd69EsWra9B1TLjibo)&vH3 zz0a(IO#Ry*3gE&~kUHN(3TghJH1a`%uUrFt3j_*|f|JfBeByx5I}0W(zG@xj;wCiS zH1#6^=hqDFr&aJEx(x`p?GMh;m z0y>y0)^t=5R-s!(*mg8_h>#%8XQ#O8=A*1$*;JydPyX24ssQ`0@*2sqaZ{I`qL3Y9 zb78OV((>O~9;HuSC3jYHc!}CG5UK##FYvN+&^0T2&9y1$Of=n8n9pRPyAiS+pAtb| zIKeUZP8~^7ZyZt_qRmkg)u!Ks-W@j|zQtX*vQkzq(pQKfj^xryn z|E;!5{apf|YWuPWr(RCMT50kiEMsEktUwEr`n_-Kx0aG8yMY;&YYH$6=0W7tW{?wu zh)m<^VkQjT15Z0knST6+&KEeoe13Sdj#PM*so~;BdOy?6Ziq31m9q+?{Di%??Bv_SyVG7d7Tckr9Yw#t-6UU#()l-rY=%(4xu z;i|-NqKP=*fW^RQ^~Z$b;E}!E;oRk4_-cFtW(Z31>zS-Ja8Dg0-XfRTOco*0hcsE( z3al+EK27>#WqUnA?SNns!W-rur?KLIuel+$q@Vt_t!ppjHRwuCMyy z6VyO_?{x`KQuFGmT?Q8kS$3VL--Ojde|sHkpDD7K*!aq4N!MC>nDY$qc-;+9!&yP- z)LRqfNlj9q=FV_CX$AA3rmWc2d&r(D&lm5j>JIZRyu& z4muPmQ>2akA(7ZxWPR$xf;5@N#H+H)ad}UL3TMQis-M?xm2sCbxb{qe!| z2qh`P!xASPv}`YSi0};Z>v6J8R5)t3dUO>JWsEE5J`JCcR#sLeeMtB5-n+JUHU)gk z6eba66DD&(a7adID87jX1_A<1&^jqSv3@ny_~tNM3JQ&y=TO;BSP;+jd8mIW>^^N? zyy*t6IVE>&?k1EzYaWYI{XBlTJ*|rS0S5-xlDaVcdqBdnw+NEm(4>>UwD>3i!DPWK zu(Wsc?039CDgoUBBDQ2BLTdy$77vVGufqX&&ajl%qV!oU1pC<2`B5j2i*^KDNTW5AOJ@!390^28V7E$PtW*MLztcqOdhfVDa@kZ zp@u!K+j+ozKa3=j92kP3+0=S`a5`qzv#uMQ_b&1x=F(0-(V*d99$O>imN*pv@a-Ra zQV!i3?)2aq(w4c~I^B?Ik-0@v==NDmpM53f3Yd$ji!9h35pm&yS=Jc{#w*XzEJ+(b zTsiilm5(i@Qk_a3J-z;JaB$H7kKElCA}(zELY)Kc)7aC%tE;J10!5$dl=^_?uHYP< z5JFy6UNd#W${KBu5fUR=MlkVet+%wMZ6PgSSv$RUGiadE^%!2lIc1!DvD(KH&GhQx zoap42e{rqb_v!T)$ZisP{d)iV_ycRwos&;w<)&{my|e%QzD>yAH+8IYzbP)(vHfQ8 z0-u?e;C-A$@y$_OHGrJuHX?g6aGW}r7zYJHi5LMP-n1{z?5lOYPpjX6aD*Og!{E8n zC%z6%&9XL6lvnK@Y`Sh&3R{PqF z*F{DDspV8SnhoLr1c=b~ zs+H=4z$AS{y0XtqJ%oHcA$8x~C?*j|EJg@KmQ+8k(B01{%Pr#X>68gi&vN1SH7Wy< z59w%9tPec#13>{``UU=)D$=(gE!YFp&d6m6ia_Md&UV)wh_!qXcPLfA`FK6vSubbY zd{#(F9JEJ+V&rZvuirx}9Us)GxwMT?0xW0Vf{UWnaOR1mx$xuEM2JKarQ!l zClsJ-pvlM_#^=UrUjJ(l6hWr5-vS3iBULu6780qGwJ5}w z-bgf1pF$!<78G3+-%`H`DH%zfp_>M$2bia~$SF6NJ~EB&E?6CazcYQHAf&~on^>_R zB*MbO!#XHZ)9dpk0tn9zE<_4|BQM+yT0@rZz*?TbM1bW6g2@J4Jo9u!-k?wh)kXV( z5h2o=R=Jz?!JdF+r(+6(Z_|ADRA*(?PF)DH%0K(LIpxC2pK!60Wa)?mb#Tj?NqhfQ zt8!+LXOKTo_evZFropIvx6ef^&bhblPV-DTcthLp@=nF_{&!AkGilOz_ieqGyA}^q z6clU;IHHA5HF+d3pVFWu2!epP2fGnuE5rtBxw8V?oJ+c710|{i1YB-7n!u>`Tg3WQ zFKab}y1Qqx)DB3Qj2%_G)Kc|=>o+7%?dq!VKQBCjEaWGb4;W7DM0CFd>|J&Dz^d<> zKegKC9o=i}c;~>{o2J&O%grJ$e|vPf+1b@v2l!LH*b>2fl*t74w}=BZxVUbI;F^~7 zDA1_I;juQBBUyl(U-O$zeqT@#`=hd4h=l zRsd1pk(=f0Ibm@(c zdkDH70Dl5Qq>$z8bKgPQWd{o>>L`{#?bQ3%`cjldXk7sd z)O$;zV2^>I=ZZx&0K$<$SDl~)aebz&B)X|Gz#bmXhijg*mfby;L7BvNMpzO=dJp?f zi35t2cgCu=6?m}a(V}{D1Rv|Vy9D)Us+D%RR6#~{W@cYDR&|5@b zHU&5!S;~~W!SUnSrf@C6&^0jsr5tB;%2#OTd-%7EbRO@7g{n7RqKK?)*K_^n_I}-| zAoVIw@XU*$26sUqke{*K`Qtm+^Za%AxfP;V|sJB1qu1K|hE-}EqW!#mwBw`zx z8_+Ar(-xx1M4_6Rz1pdAP&O_=kp*HU1jl2HjuQl>Rqk5<(|b#P0f15mT%0qy3h zj);liUpi+FsJF}AbOMr_$0IwPN*pv{Q$LM@riMfdl>l8RKdzYH6tEs(fZ+rKCZ!#0 z(OAyh-a8YvW=`miv?PR6`gqdWs}-rV#_CkjX7NPE`LrRb9wW0+9M{9OLrzXqoAC-> zcMSM?n16+|T+J_Yp`vj!m*Z)Hk%gfdSsyC7ypSHVPXiLFWbjbBGX41Fzt-M;H{gHj zz$!C%7u&A0+TFbDuLU}q{Q_wX7xtx#FDc!DiT=bAQOgOFO*}MO7TF}|2T7h5;#luc z6YGgIwKNo2UXlgHv1~)DBjX(T%Y&U+7Kj6sy93Bur0y+3Gk0fnN)9KFBjM`>HI;Wg zF^7NOTtWZi=4vOaR}I0JpPkrF~3JjZU`2r^}%Z-uduF$hl})?TM!*q zYqk#jVsvmj?i?;Y!bEOQJla8e3CIjog@nJVf?&QZGQPB;&G6I!$hzy(bQ2vt&+NN{{Zmls?{TQE{4;YRe;0L07!y+fjry=`Qv#|x?axn-RXi;3=```~^qf*=@kYZ~AOUC))-Oc3A_%p% zQ4Gk)S_~g5!cF=}vSc`)tOD^N-mz$ac9MG#<~9MmH^3foQUiNhI(x}?G+;&hOltIx z>EGWwH(jztsr>mjzZc`$*hPc?x_U4Evz9dg`98XSV2MDR9QfykwXic2cjnrk2BJUD ztj(s)WdKj|gQQQ%RYdi296&CoR5n;&z?KfoDGMjPRiWC=u$GJJ&EOy)sUf>Bqg#O? zv9KbhYfT5slHt=hXxgiumxpd_t^jK|9-RtFXRU6pu($OouNEMSviKpDm?^k_oQ1a= zR#;!hg#t>G^aVkiF*t=SS*JRnK2iyMjE7#xVKvo%2b)z5JuFWdfwR+w=y>%-Fha6h z2I*00jV1;CXbQExc9~pr=3wXAr+yOy&cdkH_urM9nN({*1rh&6mL=SR-~e2|L|&yf zfiOJ*at2TcAnW{5&{J8?34kMoY~69v+7v}p(2WXmqVMk+n;31Xb@wy+Pu0OOU!(So>+spS@pfj_A&TTdkp( z3!k8i=es|YJXV)L4?r?Fqfv=Ttv|empp62H!{@-W@wJw6d-^=RLZ0QO-pEJk4d%KD z%r#@O3AVoLTtHdDSX9hFQ$eoMZJ&OjV$pr*f||oT2M+oX$nKv&%P>2}=N5tiIk+Ei zU2(O0`2zu{vvlePfc2IltTzpkdrJw_F$Zw;O7b&B!aS6Ff? z6kmNgEhoh5#Hb5hK~}HONU1CP&|Kvl%8mrD!H=Qq2lqao(hDBk$~{mAr3hX4Nz)+H zcHor+o3oz4C2_+g`5|-7X;&VeXmI72rO&1QS@ysVNym(tky47enuvhtV99icd5Nz2 zLy`rwYEHZ>n8Y&$K8#Ot-72@V8uzgG0(u28`z)drJC6QUlMGZG&)>gwB5&8qC}VBS zRiJAmkiKW5o9IvzNWgkQLy4%ohjtfW9grS5n4CChj;<>~{X{@Ix>I_IHX~)K^7nSAthgP5KIqs7B`kveX|B;Ap_rmo1K}cJo zxhXjRlHO;dB%!zSnK~duvJ+T1gr4MjlE;8-WwUHG1tFY3Q*!$!jtU>SBRd-E9Xi7e ztc7MBCE53aa^Ch24xV~Pm4rs+zdF_zG8g#?R3i&)VB$?RXjCnX)Lgayw}q7s3901* zA#oKWo`3^yqimFA2qz>?RtT1(xySuBoK*psS+k~R=xFzh2Y=J+#!ftxArF3YR&WrK zPSL+XyLN&IR?En3uI`Ov?#yTGZh;n(44tp5DNx>^c$Bos$`Y;)o|{!JDstQy_{h@2 zJK2+b%x`->ZAZm-hm8+!&>1elVTQn7ugk{Fd)P$*EU~@Z!;SdkUvC_0ca$<0X)^Eb z?JX%Tl|SR|?IG+$p}qGflJduI?^8NY<lReCC=(_gtn5C&$I7k?)AMKgp&R8;%G_{`ER{&@4C-W_ zd^DynJEdjof#;j-gx4NM*eu4 zv=+V71^QK4IRn8gy}(LqQO2LWtY0EOY$^w`UW_TN%em|NE zq@0lTajJj@5Ht4nR%oa`ov!*a=$fFcdn&=zvS8e~Yv3dU3ffhRL^g7b(SoT19rpR` z!+$+P@QD$1=c8$i{5KVT6P4+5intARj>HT>%>+e3`822W`@B#yFSm*hsWVxP z?iXm0|3*Gr%{`VrME-vC;s*>Ey|fzwKGZ~p{rG)k(VzufDkufzQ)vp2G0T)2*5rpc zSBWXt0WXo&N?yUl*^fbGfOOI~iqaY>nj|SMo<#0pS<^^s0FTS{(iOlq zm#B}zHYtK98Li9%R!RB6gO;}%dn}?iP7yyw*rvSYKos?@`Ta1uhrkDfsx5}vfaF6H zw<3byL|)2(MCNA;`ReZxN&^)rdnMF{<@x%%JKUGm`5lL;jeNlB^epeTzmd*|awe+J z=Lh=-RXrWBY8etphv#2KBLViM_S^V?Rw>|MC~RBAvtXKyVSNm2q8cEzG}W(!s5e;J z?S28UqWjI}hN7_pCBfp#-)Ad{j(QJ6dee*L!E1M-CuLx5%AkF3IHZqK_q|c?mQ#$z zkNa+m%NecoF;C|ghl*20mn+S|NUqr3zWamX!>h*)^sohVASnG{_Ku6+iGX_eu2Ua~ylR&f&*; zzAI9eQ+M%AHJOfKenM^;|2Y|~Jq-KEE)AwGWSIqvdu|s3Wc*=BR3%3mw&dl}F1}*@ zj~PA6=1+Phr6nKsrmvJU-g^hGj`r4q{%4O1`hV5riUl^;M-fj*`I8qwtT=(vhak4E zI(r!m`o9U;F6g=-`4=$RB%jX|nchRs)NYq#1MW?;;`rR390dmN{Gn0%+_pmBg8Ocz zxrJl#v~^wLIjluw7ZvWRU>3`z7_>31jWhe-V?xz}|oVZTmaYatmydZEGa0o?k0w5}a)SQ{%#4W}OkS zY!EXjHw>_GNcV!{4}6@Onp&H@c+C&rIg`fL{*vqzV)^4$=obFp1A3Xj3I6AN-o&&I zwq!u%&fKmkB}?-Jl{N}Ioa}#mM8+3^O_;v*Mbe3Zv*OpCDzA>|I`Sd-z3>-f5ef*ZV%WO3DXk`$1Dl z9)$&=RP}yB3R&IL!9m_>n?U^ZhdH=HNcId0W6!w~#T+n9tSbl25~p%VcoFL-Of#qz zL~JdtiHp%_A0BPc8Ksr_d#w!RYlUePtQ?5kF57y*7KsBF<#;T0aB#MVJTM9L6< z2~F0T<6ssXB`D={8#RRxvYKW0RqL~#yV{wkcnoIg{(h%z2>oxrWN(UkFW}qnmSa_6 za(W8Y*fkn)7p422GPmR=lGdO?5oueG!qLyKsDC2Ns^1%N$SssKULTg@C%t+(8zzN_`BCEpCxXw3$wDWKu0O1Q~gR zK#*CS45u4hRlf#tC|h?c4Wh+73UDt77Dp&1#37Z=oLPPC_}4vh*%0_X*T_j^ID)M< zp863@85%g;i!C0I@{`iV>{QpI5FN2@5=TCdAvZxrt803R=hq-rNz*C-I9si)e~jRg zb8klsoP9kj@|_u|%#XQX&|LB85Ie?M39Mya`|kJ4*dV|ulz{?ZDi0-=KIpl2-MK#@ zu)?)OOj1hb%zs3U`)mRp>v0yY3}ARa_7#Vw^d~IBD5u@U>w3j zEIsBLw6HL^=gHd;^8^uSx*8x&%Cs}ClHiOsPvD;^k0@toPAyX(sw(bm_$70;G!6>| zQv_7hNq~8ksBq9T6}aOjRx?@rnb{m%wkuJ!hV>7&7+G{z-nJ0X8_HU4xO(RdN6W~IDaPnmutt@U0AWi4Kh#G!uEniq!7BJ2#8qU1iJfz6!H_`0?=7OqM+Qq zrGaP?Dln{H4LTN{$m>kFe{Ut4FLU!d+FlzYRaorrRTD}=GfMcePBnVv7H!7+d$zfh zRs=mNO`*r!?+>ZQ9)F;g1*xqfRkqAu&=I&-7Su%U1FMjqa~Gh{tf(>@{|2-3MDPdD z)IjOWl2dL4sfD50%i7h;@@T%d*y}6o;|loO?q0a?a4aLD=`}ATfBds}Jtso_E!1#= z{9z*(J|<9+r}}}x`P#^xm~1!_J2=wVrrI#L81%R^59Q!yuYfySDC0NPEg|cJ8Tczw zdQ$z$lp|G8_X8N*EqcLLTibKH!`3{#-sQ#Kk63XFL&hHZHbh1m)Q$J2{PA=3(_7n3 z*Z=OE3<(XeCc<=(;E4pYo^7OKztkmspdJI{PaBpPs+ek^qc**hWylU?KH3NRu1p7% zd_Sae(XdUe&>OWPI)n@8k9KM(3lmZ2Q)4(9fPR)*5oA#>fdk8i#r z{vQ3pCEO=nfJzJ1E5IZnlSu(7{udbqy9{Q`yOaQ2E(9L-pB7}N0PFxPdFS`3Fn#?n z#6yUIC?=n}4&gDAmXK1KWNEF}qrLLc2%Of&@k`){pqu5;$`D@%cHS%esdP^puXet@ zU4?}+&7x*^q4?bZDNA*av-z-&IiyW}HKoQoWBN&|r6BEdt%cy1N(QP>Z5R_&a+)kD z`>07iWKjL*GIlcE;I2k-O{h63K@OHC?1Y2B#=R`MsLD5>P{fM)y^U3GrjZjgHHMFH4dn9!7=rP1yHASxGJrwc0|=e+|8Z?>|^#NR@X zn~=&1{8(+DLtSh=bB|s{B&}<|# zLHRZ8@s{O185NKOzXVGxii*ffLDtP(N?O6tcki81;31kR@&O`* z>dibqv0ri7%fbnKM@aRBr}gzutDixeDOWBIQ+D!KYH|hd2U)IQ)NCc1`&H}VzGu_e z&sWLuTmV%?fC95qHKZ$|Wo-ov6{&eqY&FwnRzw1 zR!6+-cWe2=;v9AP1`mPkn#xZCJB}8sgThu~V6qzZLHyM&RTHvVGz|>91-JkacdxVJ z2cxa>RO=sIejfMag$%1tu2?90T;^?)A@WdvrqK+UMT&YPD1c8lFY@s+m z{uzkEyHLw{-S}xY+QI7h`JE;)J`}jF4!id#q^c5tdM7|>)({yKvB25vQ;TEp#B|JZ z?JpMK_5!|o{T}P31U@v?cb>e81&CM%xE4P!Sfys^!p_f6hQ~M`?PA%CS4W?mh^jSL zOeIc-U24qOmm(V=eP40!V?+rFJIT-OQcMqQ8$X*q-13D0s>_E0=^xbTSSXG>JE4S_ zN+2bR!+s^>${^{i;LA43+cVfpfk>k9nG1ye3Bt7FQBH*PL3&xcFN9 z!jtTE-L}Ypcr5v|XQgHje+b%*Ed|N(?^m!lkn&1(3s#7W_119OwW)w|1g4@fLy_hl z`6q$6s73OmDozkYPo$O~t^sqE*fNj}Idn2ZcN?Dve(rk*Qblzphn*hSbaU= zrZ14zTikRj>%yZ0(INRQ(CMH>d`R5rU6G6qO4cYnKuAgM9zaNo#1ttx4u-dL=a%~3UPnh(dxErS~C%@D)erDHuAT_-{VOzS%I=@F{sJyN*p z3!#^D{-zEAQqSWg;P4v&xE=&L2UCfdsd_U)5fWE2BT)%L-b(@@C+>nQEP$}^Ef8%# zD(F>{1BYVh%^9zgipMzksHv1%x580h-MHyYTh*7-9ksh3O>hDTRTb@PfUG$|?T&+k zu-gbi1_bMoIOvNw2Kr~{GY-S~fvo?RUTxF!R86`dgnkW-tZ{)%PA`Dkx4U8v=sIMm zVYYY98;AwDh$G%t?o#~-gMVAg+YCcPF3^-39JO->@E#b+of{+k7tKw1*2P%=*z0JU zVt-i4`@h3-%>N@S`wSB}zuo|nD@SxKq~ruLuHK9hMOX|7bvCiARp_z7;qgxrp_L9? zpCrTKc3z3L$&8LPOp&Q9cX%cbf20ojA5MR1FXFluD_Wr?KpkO9Z3*tM1tm`btAVV- zVS02K*b3`jhFAb60~qfCz>%QN1c>*is1$}+H|D!$&Rx4h1xiKD!&?|=NtjJ;XE=(zck ziEWp+8NY=1qmo{{hRyxlLB}ftuFf>=TRZw(5S;D`D#7XpEn*Akbl}W^0-TU78D{22Eaja z>HYZO&X3{~rS`n!?e}l5-Fy1el-jfkTVknl2F42P{LK8ixpSI)^-3Z@pHP8M73wV` z#BLr{W|WgxxQ58xY)*Fn1aHfmYrgjgr5)_B0WSvZ)GL~(<|WzNKwUYVj+uW;A9v53 zEZ2YwZ(1(6MMvGDv7;sPQW1R!3O?vZ8-?xvETOF^JgeX-2Y~nRtpnW0BZ7-avxZ;SA=q# z$WK}(Kgg-ZXu`3&o{0ImBVdZ8l{A;(XT_mDxX6d&)%7J0(VvHb(#sqE#}9Xgm2*lt zz^{`atB?X9Y^WGxnX7le>}!ENa-fP_K00&7 zm6$t5FLdWsC?-y>8@m_g`*HNSwX^ZmOy~xkFJ>T${0%h#dw?nAJ{@K8GtD2Qbu2Kj ztEdC2h}%+WVM7CvO&P?icHvX;<}FFkxoOF=K9smifTn<{K758~Hp3F*luqEyS3l|a zDf*@UFVEi4k0F2}@|u2~`>zbAM<|yFi|T*};H7QXdW_JjeX5{l!s0RNf(oiJ)2S-@ zX)dSPg_N1Y{1zUrijKUWYtcc^n~1ucyY|9&<=nQnsp|Q5+uwpunGn5Sg0)N~aOZMa zct8-+g6bu4YL5quE+J6cVaL-sKuI9Q(cW5APk7U*pZUw1aJNWGRkvg2OjTXH_|w9g zzpAUAo;a_NK1j7l-Wfz!nTb;2BIQSPq12;oC3CkXi@CiNH5s~mTQ+fEs+LmBT$1r= zqA3cO7&h57^bZsUSBYk`H&k;Dbvp6{oI?m!_h{gq$D*XL^Mc|09~Q=Zd!N-us2GVW z2TM#zD>aXx5Rh^Lu_gcbgd7G0?Lvo;9bnC=8~$KLa*IdDbc`(p*;^NXFMq7^yPZL& zd3}BjAwjgI$CYr)(%{(Px&KcAA5QZ-ZKMBVnAF8==lSOccvQBz0O*xMV)9V02&X$< za(0dv=SoK)IX+qKR7m1R(+f|yVR3v${8udxsFr5Aud@ehEw%q;TW@90?2+HISG=b# zoF9oT%D9~MkY*kie>d-KKxjs3;T0M$B;#xr&F$m;hvWFN{ED==xU?r8mm;r3{#y(f zC@Y9Qtvzb}_jsO;3IwBAP~`nDn-P6JUjqCpkl-~C)l}fd?=?5}SI~{r=io$`tFTr5 z$ao@@`-n=`OfKUy+@G_ox`I0EjhN28OU>iDQwB%Pz=~PdkSpfCCqvBxsu0^#u<}9g znLXucnoBQx-myWCquj=PmCH_n1l)=s@Z#L(!S~tlwVBsVf+4p+-eg@-GEgpCPZ} zt3?7+1lofDC%?_oWq5(vxS5j=o+1l}|A?>;YRca9wCc{VhX)&v4v9G8mK3$OTVhRy zzFvfeb|l(%w`G1|vUtS<%Hz@hi>Wt(YT^vrzoDq82&f1s2ywTHNL3aKF%dUJinvsE zNG(+e5mPHjl#o%8U8HIy6)I8Lg%DXnlqC__Y88=PK*&N}*vyPVn375S-@fmAzVn^a z)}xjzGtWHFyq@BWESot5Oup2=1O*_qb~_wG_@UbhmaMNllpaiaE`KvY9yU z1(VZf>nWQxciEmY+7eGRztgXxD^g-AC7jDpy9Q>W#W}73m+bD!L*EG%R{p&KJrRUs ze?r{d;18KVl0*U+0&Nz+lSD1!Uh4IDsvDXJ1e9aMY(4TJI60?g^D*bo}e z)-G{j{dcnO@WZ{iNBs8*XrKPCIO&nP6#YORrDPgELBYv7?im(PDN>VH2B zDo2D<4t-vsG$^=~n=`<6M1z{dw+d`ul&9J{B117XMY6K0MgC2;t&P_k1@`e5sa?t^ zV0cKy*ZDNjELaRMok0d*))teblS3X1gmFw!m^O4|nH=jSc9#q-uBtwHGrxb#?o3SV zZ%;}b(H!^H?801`55!}$x5Mf=%z1{u%Uub!zz?gvMyxkJtV7L{)?n)!s3{1fu2Vsz z2C1poa%l7jJy}sqH5j|_H7N7B=W&lU{6oj4NS+xU(@3%CjSM>IVny;xY3^;ZUWQiH z-A9rq;x#I6Y-rI4mwVw{UO~`+bYvzzzOjefW8QOfZByRcJA*YXJNSboSj&#O#>H!1 zy+~ejy7WPV<}F2caFE6ZA(w4z?AbHtW6q;l^49LRZ@W4IgMxwrS=4=*ig)i6C(nmI z%Jw;bGSi2?_WJF?+kf8BC5n*7B@okiC}*})2L_;;?>0X5Mdwu$t%Eea7G`MLU|yBk zC#jb}{dBxdo?55>gknY)dItFQZkBhm8JJ1)%q?M>B$=`rA$IlJj!%xohBgPUbB^K; zyf5pFykqji!}3nDi#*n3Ct8|%Kg^%`I`V&pp7cK-Q%As3EGD6zb^~#ssESj2v9f5I z``US|_ll|;sPUYyi5MFite!zOORdV>tI-b@lg z#n)U*$L5gwss|LtskIRp)GE<~R z;tdFH>5dq#Zwr$h|6mp<74V`;Z-Wpxn7eIuGEC0*L0$=Vb-%v zRpvM7yo%GjA~yyrrDxi>K>Av`=7$mMW$u zTWp=QD(h$$pZ&cINd%?r+Mhbw&KHeY2>)@VpEwb*L=-)fk#p|zVBj-{?yk>+Z(N=! zO$m^`*T0>4S(XRCt|J?U=7(2jfWn$I;3A)srZ5Pw94q#SIO~=w=e5`(7i?{OFAw=D zx+Y%(ws&t&NLiiV#8K}K`?|e9n`fBa+@t$7|AIYWwJl|a2){-8_u}^KZUT(bswM(*RTH@;)7x@PUvtkh9B%A5!}GPwN+a*EC}ScbNef;s>ODHfN5u%ZX+oIWFn{j~c;CM~sLq zJEjvWrDjlS*?&*+%gm}r|fLBYX6fn<;Z2ez%b85rof z`ni4Sr~TvOFOX9(XTBrUw}UFH1Zhz2g@eUK^fP1Bg|$98;UfjoAis6RId9nCVNI>g zw~A~l%TgyCW3pV_-3yx{xpzVTu?L$z2H%+VXB1~4R zqM=e7YS)+L+8wE!=RX>qy&j{bvV)M2VtiWRbd@Du|G>XjeFkWLDU!kkrU zgXBa#;+|@k#ZRshc?YfkA z{fh1H682m-O~fy~(pU4fFQrw2@lg+`AY1h;AJdE$|tpPGAt>z*Vi^PL(ta4O}ljp zL(5W+)pfk>Jv|lSb6EQ=Ro3Ad>e~sqK?QG~{eYp*5x`n2uOQZ^rUof5XGZ&<`&X9Z zXNto!&Q%Lbw&q%-)#Vq2h87OEo?qVXiZCLg7w0uD+AkT~u#@zU6HLWzE7s0C24@8i zHs3MIR!yI(0O zfLLS?l8!t4;GzfT(jDx#g)inC516RF|8MSko6nrK7g9T~KX0~tRb+33=Ml;>_1i(N zM=Q6^!eC5-FkuYe;Y~y+(w7Vk5M)f*!VgsxG$5%%n^Dfi-@}5}(60EO%mb6~Aw%^2 z72^A%aK{T?SA~ovC~-lW;;`L5CM^c>?H*^_$pdWli1;z}LP%scIMkgq21D`*bNSy6 z5eG@e;gW~Ebced5t|{8l9hRWrGVa$?{aW@aDn?_w*32g}J2^x_Ths?r!wcP`AfTKX zto!R>!vpCW8EFK7#fS8V1%#=ScJ5jAVx&IsG{+K8Ri(x$ye@Ipu{ZC!9BaN4uS0hS zqb&)P6{FU(AKyGGzA2X^hhm9wJ~T2t;v@D z(3zo7Xg?}Ozvvu(RNL*47V6h6h%!9e=&UV^iRH5rXIn zqbhjTE?dK0c&MUQN}897SHqa*d(Cws$|?0^7x|bP*^b+&?gCkKnVcH${IdAOiK3w} zYZ7=N8($G`#k{Um)+E+yUklMDY0~CMjjqF%T%6Ok51q)rb$>)U7=&Ex5%L*e5A48P zh$kNF5>6n&UCJr`*}xuVsn9dTugAfoh$-9=aI8z2mpy&BOK92mzE&)*$z&AQ6y-fa z9%Z=y61{>n0!jR=8}MOw!iN=84XCF`hJ>Hy+83P&FFfKa+rwbe(4!eTspp4D{rWPW zCL8KEw2_&F>!*2Ukpg{C^PiR39#l3`mSlXi@q~N+Q3#GvH9bB7qK=<2(}Z|Foz&?w zVWJMxKBJazN9#eWN;i%}NIL0hM)%2UBxaNqpg^}%P{-3N5PDU*bqx}__LHxsyeqJF z{>Y+ihm*4ul9m?jTm(koNrWj>%?8fiAFa-~e&#&DU{|%5zVA?$R{mF7nZ|4~^9KWg zz~}T$UX-lj5kPL9E(qDc2v8ejrMeX6i8A*OOcrR3#(JWVR1j@#N=%=uneaJM*J*Ee znX32GtI4sNvEo!i3j>(sEWHBzcUi_RbSMr1|2a@x43`XmQ7j)fF!?T~-Zb(!>{m=u5{ZDs#djd>@^(mZ4@ z>vH@D`$r(EwHanE>>qVDPn^2r+;XhTUQXc`00j-e`u*1uOGcclu0IGKnWidSX;OW{ zJi0m9u8S&rZH%wJ0~t(8t$R$&a589mL*g<1+o8urZw6WQ0V-FVt1z?P@F;WSEvDnF zy`4XHCA{sNQCY$3(NK+J$Gl-3C?(6EQPFUoW?fv96#tV~)&g~!2_%q&x~0Fa`06&h zjPtCY1J`wy61$d##mT)IJ!6hwo9om&?b6s=Fj6DWT&Qsbm8!dk1X;vttFJzpC{Hbu z@6TWw(f#*bx)%nv7fhy0n*C^X0>5ySs%W7NIT03eh13*kc&NKEZX5Oxb4uFec7;M> z)dZrySM)?#9+$_SM*12Q{D1pf68AW5&?G--jn1cwj*MD_32Sb=KUj7rpB}Ly>V*`4psZ-OJJ~6WCH+{Q z)YxGSQ~C753Q=jY>->Q1ETw z`=>8p^&Wa?jLg=$+OnpikupZVSu(#V&PS%7QmZR3CM7%h|6-=H za7$u`=Hs8`iH)qyWRHBhSw{aRe{q^7)#2l^v0Hv{&++fLF~Cul1R?=EOH-`BzZK4*kt{mOo92Wi)<$mfQZdpqpMSbzesbMKh= zxvaBt%+mMR&s7y=6_+g;0jygg33G4QA9^sG-o97I&3;SZZd;Mwq%QPjebDuwYyK5J z=Z{~F$@|W#9%^{hUzB}W!;-5=Xj|eDP8-Eqa}VUi#&P{aHJ~Pig~n9my}9Qf5mguW;;S%uqs;z{C>S!s7Bx? zJ^IWF zZO&ZVDy;jMsIc0oF!y$^9Q2oZzj81B$Nj+^K7v-o7y!QLq#r$H(;cOwCJW8X`@vCL-6L_DPB@GOrRwRh@)pukkJv! z^0Jc^uO_WFcE%>$$ZReGRFbbYl1znHDT7b{h{XtQC%c8fZ_PBNN@k=X&9kRuoN&cz znzid+o&H!^{F=$15K0U|HPA&+6;`Xx|FmCdYaG}aWk^E2%jqQ!E|xVm$+F+|H2g5Z zEg#vru?LuP;@bKqNG!u?Fi`L9pAw;JV z@`@N1X&K4P2V*{G=-%9!s-pAEpMT$dwlMgI^fT+bLHJ6eaC)R>taVim2>j&Q*I$D<>;Vn(UYq4|p;P(qPC6&c6GKa_cVOAe+%c)nj zAdHQV9IImdBn18J4=SZez@(#1 zZHdFzv@WmVzbjwDj+nz>rERYD#E16(*2VcVd(^?;B093Tu%8axVv1yUZcs- ze$VNoEk?o+hzl#ZiQ)}m+D+|ZGE2}XT|8wsH=Ja^SwMK=b#i10v9(9oN>>sj!SiB# z2irw4H{^!8A$tufeq7kXXK_9^TDVYH(O0b7Ekl@g{a-2KD{x8((wg zAcPAZQ-sGI6k@zSVydO`Falp;I%F}eItv8JWi)?bY0uwmpo*8 zV{h_b_iN%8TNGZkx>uMPhSX&j0LSJpP%}+tX9=#`1fn?L?1uNcDWe?CizZ|Mo(acA79t)z0rO z3?8Dj!UC|=l&{L18C87pE#XjzJ)I@@Z@de=JzUW_?l2HzY4BsH4fDhaCNev(WaSD` z!ncPPAw^Q$9-w-U)#tVClB)M$FBSmKSa*pi^)B`{jP&G-ju|_!LX5X5tdN!=ClpB~ z8wl{#u9d0vTH78WT`<|I7tl%gL}}{o29@R%ct#hD^v?X^d~7WI?C4I?2amnTjrW8Z zxEDZqb@na^-0}=W+c&fTrB1|SIWlI05`wr&uV$}4rCiw{xd=v3iBJNaLkCgi25ehX zw(cuokcay#p={=T8rz+jQzaQDEeY>+iq5yPcwLD*Jr6U5W@Q$ z$9>cuz#8PYz-X3I(VD=+^cs3BL=EX_&^<#4CUnW8uSN!4n6cIOYX@|IhS|7kXr@E? zLs#jIAyxBx&t7Pwm5&8m_t>ZX_O#(h6>M?8%(uaRfm>BAkrnW&B;(h?8YCE)Bqxp2 zvNz(h<#v|DjvmSumZV8CUl`R#QlE05i$=@ni#; zp~d<>WC$lAKnAIMEmB- zzXemN5guwTbfs*FDPVU(*uQalVnprC`C8Veo?!O1sIFkr@o6ZhQ)jW)Vn2_5mbD~B zfuagw4K!QZU_+u|TeD9vdR4Kv`$}2@Y2OJzF*AOE<+kIJjL$(uOT!<~hh+&J& zmf`C&C@*C1&SwoDRqH2t*1Z)X2_kx4e%>tqAE$e(cyC*e!+OerOC+TC_V`}`egkTlyV#CTt&jZbV5hvY;uT3B9au~=m+|@q#&6~d9fi`;7Jl5vp8Oy9J)kX00 zP(^|u7#g?WpC*!&Cr%W=ykNE^9;4o`?VTpgru8sR46dV zbL2lp6u;Rm6D%KvZ&?~)ul8EBanw#8Ms_V7%{BmHLNdR0gMjsu-hWV7PTB*O-n_|G zgdQ|-;gQn%^iE0Pc>->vvDOhjgQMg`TmR<-idaiI zg2a>cazOXik|?C&2WC}e46VhCll2=1X@Rw!6SfnR6YIUoUzNPPN69<;prPQ4PPREg zp=syC2INixR1daF4B_2pH`)-Du-E`s&q`B@z|((ODwI5x_7^KMim`{K^fuY)FZCl5>v z;vx; zE!B43VzlR8sKdMDn6u#_P8Zk}o~oEjtK96%PMy>2fDgF>qNDmUi70v;f`Zw4_+GUS zr^iCjKuR2%kLywR3aGV@SkJ@a(;Ih$wuskvlc^=Rw{++AO(ErpJ3AvH9G>%!d_MO0 zd~UQzBotQN%DV+WvJ(mv_y6}_z4tJs-XPm6ns8TdhE2D^^LH`p- zX@Kv#qBHVGl=Zk}AR1UbX>m$qc71g5$OgUxaqdV^z}n88E9nZOAml{|4K}2mke58P zI9`$IxRF^U^mjPAxqiILKLYkgS#7ivNzxn zXj|jZcdu+~U5^)pJ5%n(+;4jjrYajfm*HG>JNnGY&b>^8Zc{R%r9je2n1MQ8s@M_j zj~Pyb^@ysrqOMmM>)~Hh`HJGi8g6hl<@XewcbICbN|j5D!~0Jom*kpt^FmpbgC@H> zD>Wqd3g1)q12zFRte>vxsaL}Hy+D<{5ezt~W@)P0CSufqV>Bbeih;g=qTPtLU94Rs zqnY%xw9_rl1&yBiN6{XB^n>to9-$HPx@`^4r`}CO=MBCqDh%=qsQ<>lCFYFr^ak2H z4Ll-`q536}A$O$~h%DD4di*h_R^7l;)w=<9@*I8NG9%{Gv6d!~cc%+jy905$d`C!= zzfli!;Bk6fE+!2@bpPksE0}?H&>G!>g!>)tVJyaR0U6QDq-tm)by` zNLHJ(m_S+CqTWOV^@`_=e9^J;YN9R0zaTm-5+mi5mm&M;pM;GVKhKbZHu^)h12{Fu zNQ`mDp{!=EXlRwF)AnxHRC49u=6^i>cPt(4_b$}c@Oq!p(4(~1{bSns`s%sbc4^sE zV%iG)`~!lHp3BPnkiDh0^OtV8tTePOmF+Uj*He2pFZ=oNO*cso+b$$;%mndYW_N1x z`9RHc8%9krE8^rAomIv-TSAy-X>U-~bVRZDYvlIsnxv+!FHQ^VL`@H;0^qcnhkt8S zqX0u~uBr$pS&PuDr^sURZh1mC?U;vgGSx`C^n-LEIV3kQoYoh-4o}Vv8*-}d<{qi5 zvL^MC-Eu~pxv6BtL9sL+o1Ohgmv{zc2ogMnH)mavAs0{Su5cniHgEXE+Q^7^{rXGe zRg!!v?pUmyyGRZ8d0WIT#Ayv|wG6L?`070XIMpr=hx)oPV0kVKiqaxV2`2@08QT$0 z72xV%3`WDzW=e3A7&-%tgckcf@ zy@F4j+?{j2If~rUe#f(<%Ic`7w=zuvq+=}jz3Q#zE@vtA8re09zZ)Hs6*L)Ju^sTk zveXS`+wGMLSSI!UfG?1`y|QP_wJnLrR4I@3PVGzZ3~jOi?x#m#c>f@-KY0`mh-6sg zeh38JI#eczBga&!nxZMYc*Kyhl+(uf{uLtLx+dEQ6Z0_hMj(H?U3N&L9zmxsO+?nr zExCE1s;9J``(v+i{dXh%LAPY0%VUQ#oBL@MR2i%!+{r`rH|%qlw0$a)uy;J2&)$Kt zIp3q^31lls|5(b#o^0J}^u%<7X0z0-!@ZV6o}sx<*4GT?!G6N0;i z#O}NiZl*GEJZd89Ehn^)nG?nP-^mxW#)*STyh6O2Yt6z_)rtUL+N_ zH`f}>E@b%cNI)v&Z5Jt|4!bB%O6Rd&-tfWeMfu73Zw8wTRV6bK*ZyKg&F+lI4rnf` zm@_)Q>nG71zr~V=5`1Gcu~4b7n}FPD)@jY3h>RzhMfsL)E?I*7T<0W%1FT`fr>Sbg z-7v&l=yioQ7lvxC#ajB-cobKnA*@t)jB>BUm&QXTYyx(8zij?z`oyiKrU}WUWUmhL zhcU4WcBk2Ye9^gQE+c74h%xd>ED>|xO(_(>rTuP-If=(HH`SZ= z+QUGXq;ux%Cm+rw*0wXr`fy$(WFBkHsOmNI28*uKo5)^g6gF9`8!cXkOBF7*p4%)eg9;>qovNKc3i!qLU_QzBD6 zPxl^_bO;UZta^W_gD>y8Th?tq@C@v-CYa;ze|dCOk7#DoVJGCk85n3!gVF~kE5<8Yp9r8wJgeQ>P7k%!I+S7(GNuI&C3BTnA7TdMxLy*h+e zMnZSC!PBW4(j-9wIi#a0hf&E8x#&{u4%E^W8y}j77F??JIe{6#Y@`B7<{8$WLGO>g z)V^!lG(p(i#~Hh^jC+E%7^i-vgVhn+)t(iVYWvJvZ+Xb4qr+bvKnT-@7GU|fT~)#f zcn=YtOpH-T7KUqA)wHwCv760e12p00nG%{8vN=nR%*C1BowZbX+BeVMJNEC3yZ_p6 z}Dq~i@(iz*wx1If?|O->~#rJ9slNxg;) zZN{7$i`xu>p>Se{IVqG0Y%f%LtT(EN$p)%lPwg_JIDS&Fa#t_0tz^`DMw}c4=b=e2 z*NrNdLM)QCEnv?Dt~+O0 z3%^Bx8$7jk?yhwoMr!y50oTTwIO`vG5$-##nn(ZE1ain7@oCvN^Im(bD>_@G-Aa59 zPdU_&>jNp4KnJ8y$~6*bWGc;qSi1opATf>yv)Ld_bdAtgnB>1{`L*9E6&N*!;Yv5m z>{8)CW~y0SDyP%dVW=r>pH;;)6jht-m7}sXrn)#p}yaS;Kq87L32e*3m9; zR%j|BgS=X(X6#t>*;Kj_v8yBT5_vVsXg&vEwW}T_S>h8s&X60Gstw?}4$l ziA>NUUB+p1W;O=YFs@+SYh-gdDUmu4h}shj#leEL|Dt$Si+37k0nc^s(58gWA8IkP z^m`vNxyhaWz`b6Jg(gXI<^wNE@{FrBfC|U#$MAjbj?&O0N@QStJ!OtYAl?G1`Sa*{ z)n&X+?E;m9DAb@O`#U^9_M9}Ey#&9dX#3_vb&nuF?7|nFI|`B&>z@5%_TFKt>6i~! z#GQGW=XfW_?Jf=jLxL-{WL<5=k3M)8=fzjz5pLu36xuuT^^Hf>zp^=aF;m`VNjFAN z>2UP-jnd_8Py84_tB?Ni0cz|ZcHuJ`cMTlt08rE6T9HSV%bjhsS4#ihMr*U4NsDer%|cn4+f!#+lDKUXH}e21LAtX$q+`luV1Hy*mij~ob&0pVgoK zg{U8nCN>WhOnriDHipzkvZ=7Yokd&fTW$8@JJvS~t|C&)Qiln*r$ZLqQ8nrFS$3Xi zbX=)@4Q&a-qBm*0Bq~5&m|Dm5sO+#W_H;b@+h@-+XY9WVD5JJA3AcGzr}nD8+(}C^ zUW$B%!GQK@!U98G6z`ugg3)@2$PwA%fM$0>H0dpyMU zO8fW`k*S}HM;+Vww_Z3950S=S5uO$G!xN4PB1f^rFFM~v>S|LeeLjCZ;@83vA72T` za-ijKzGE-KBu^yxVV)&eFL-J>-;7r-lKwUTlb!Sjy*#247UAg1uwvdUF@$W0T?8nsZe4(dFU`MH-5=>&b)#qve zVSFvE@HI^u$Ppm*wGusw9Xk|0W(}t~T4F%xDoD$G*(-37oOhM9#KCrB_Z+UwD*?(n zKRHjQ5}rka&msy&E}W>HKIa_u=cC5~e42k(WUIkGk)$We#?mrf<#zuf^`GK`h(+J% zB72wUe%7C;LDkJ9Gs(}8+$!Zh4TDy-yN8v@k_!D6Y2uJRIukrn&0u5$A?zv7jCHob z-+9!z_1&FZ`b5!~|E*iU*L6o3SUGmRTraK#a;e`c6)&_+ISnA~2!x95Lu0DG^ z>Xrm1B>3}s?YHg9NjnqP(WYR;va2)_+VRXrf9nEfL{V7($Hd~kppnd~TZI7uLnG5F zks0^UUh>v`66I>_tRgyIJP(9%cd)FMtx{JR?+RkfreiM6CiIMm!qG!`z>{FhuG(XY zd6AkO)=pw^x_igzp^jD<@;kG9{%GJLVh*c9YkU#snu9EwbV1eM3QSdYOQ2-#^VKk*t%lFri45VH@7MJ48wFNZwEiE<@Kq>G~uYZ zjaAxJAM;)_)h8|Y>mzLgwtN?kX+Clv(0lt&7vvAVc@0bjOHfHpR82Xvz z(PpA^qD9YUw^m{;_9pwV11Y2bP#+@rHWDIZHVuYq=P$#Ks;+d_>ME9L=d(;y=dj)e z<`UvS%#T&5IBQsG$;y%sRe*_IQ*6|x^+$UJmFY&|{kb!on^u!E-aJF$yV5GNne5wZ z-_M+ZkEMX)c=ls_85S)Aqlajfszbb)rPoNAU4y5q!Rr>6%k)b6?^a+u2;+ z>1vJGMmSWSl4*+}CbDTTxC@~6j3V2q7Z#K}qngt2Xdm;f1N51Li9k2@>=G&$vEqE| z9zuEQ9=$)Iq67g-=!sk1U9_REt=*$qW8FuZn=typyhf|Ad&27@?ZwN0t`Y1Ef^(7? zYfJ&$)Yq2@(Iq%0q#{jpMfGjt zAE@%5?HWF-L*1d;t=@?TOwjrz13a{7DXRouKMw2sFB=~2meml~G(XZ+dzKT(`wY37 z@}|~QtE~iYZTS~Or~ZZn!kj)LP$Pgrf*Pp~>+*BiH9lIi6L-%c_SaQ;P>mS0MJ2T- zz%2C$nxE|OqAZZ%oN)c_+$h7-o?H7rw(a#8+-&GQ`f|*>a|JwcyZ^Dltu^^n*;J4h zoS!Y}Db{oF+3KaN=oTmp)RGj%P?uqOHA@yz)*@qKBj5(g+m}U7xcPOFx8>qA|B80L zGAbtKzslm(MS(#rxx(vRvz(e0OP^515Rz_T;^bagXw#sHc0LIx4{=@a*cc#}vniTv zwgLJc$ZOu+o7r|n_~&}-6_~16!JOwF%h)9CtDZ{WDy(ddBp&R}DSOmy|6i#cV*iI& z)G1tMO9d+59V*HK)uu3gA`Nv>{Q_{%HDS_uwclgYEy2t1-EuoCHW|NEfAgrE%pf>_yx5_=QvE2=BsYfRlUtrxF)j4Box%!5WsNp zJ{hU6LEa1ZN52KJOSO1^d7QHbZ?E-Jq$+2z+!{E>y#j|=yM{$07cy?=F`}zq))a+< zAaCv9<4RFw=lDd*XeT4wt8a= zQKO_U;1IqAW2)j%+dN@Lj)E62HDcSLpNE6*D3qqakm*#O7X0!M4Zz15L_7MGjegTIo{YcNxNMFUX0YM%`<<8^B$TvEa@~HeApG-Fw9KJ6$hAXjpdi* zJVH==?@vK|$)6E7&WFV3&n^Eu$uDvG>i}*CXSU{{+s}ULAdLQ&xjf#?sz1J zL@Zm4t{>LFq}Bi0NwFf1qv}SusfZgk(1hqllH#(RdiXwGos>1fgcbbAL;-Q28vVn3 zaehv5Sg>ginSP&jLD^)cTHoNqS)bZSG4FJUQ7*(?nr2SMYn~n6Is5;ZtY3d6nFAGC zyOgCI4kb*as<-&}XidXsa9;LlV)mJ!=$aN9UTprJT_NqdfF|VSJXw8N4#_hWBm9qC>b0htp6T$E{9=yX@Y~78Gdmj^Fr;(=|kFo zI!=pQ!q!}U6L|g%Gf16?Iw*{Bih-hftiK-*P|e0;oD$4^x)KxDVfxL2#Q}_t54%1c z&voYyj10}M>6CR#z0@w@hh7f`_BH*&I_cr}?$VKcIs_DW<6O0K*g;qUVajmDd*$gm ztVD0z6ZMcsH{z5HglkIoN>mcZ6t7~f4nlh`P2O=kTAeYpY^rTZxHQajwQFbVyNalg z0*3R`*J+l!d+f%MZi?illpAIZwa1yKcfXNb=HWY@!=~M4f+H#`9A?yY@VEec2U`1_k8l`ifjT7R0eoz<|t zZCT4j?60VnWdbP`U+F|Fr{0GQlEzt01mXt6yh|SFw&Cc8wpnl3L9b5LZd2GuEpSp3 zb^hdFbiLNFw=}=k!OCf`q=W2ppCxJ*lj19Hm#nSmwoePTd~VR@;?X5PWc(R=dzT9l zVgUpdkP6rDMi{V9vQI!<0nMXtyR=lo7gOELXY>@t`Jz7QM2v!D%5rM*x1A~xIiD-L zi^uKi;xkV<*lzte+V%KRdB%R_xF90plSFnK7%q04R|pKV@ldr)a*@0jGj77zk@$&W zgF4bqppb!to@9^GVZ4dZ!>@UzH|(5pRqum20ke<^SP^irZK)k|4Cb_6sqr~)G{Yxqaabc604-n({5+!4|_E}M_N~P zQ!C&A6~LLD5*Y&m29~tR;?A3l{&VT~N1n#mIuCg~|FfWn?E2tC_OkYsUF^MJ@*Sj9 zZ}Gtn!wrMOJ?`-@no8;^!4;&urNygn_kP?pkNBSP;l7oSb z-8^5`&3!#&>fI#rmSRt2?Mq%vvG1EVLG);@nfZ~vXFqYv_f8oTW&mLQxg3^=FQ?2*4R50 zmu=wUvC=lL#CXTkH{&{5excyA_!S(>xLv9zKWt3l^%^mecLiHT90eS6H3_z@H;lt! z`5zZOc$QO-$g&vAruLM59`H&~y&LKXD4QSHwq(;MyJ?AavdU zfWzHF0}t2pMKr(H0n2K~o)^bN-uPu^a|(n8der4C<7UYMB2*zsq%9;iD2!*}pPz_V z^^n&N18zRY9sCpAK^erlppCT&e1$j+u0E7tFtN?MBg!Tq)lzNw=;udOj}!Li?TuO` zxzEHM^dRVxCP`2~D6}eCE<@C1g~$uWr|uGAm|JuD^8WtZU<*7p7gqyWw;t+Qs{;VT zA*yl~q>>{P5l-%K=p~i}D;q6J7Bcz=vu|$((e0A(OzKQ09GS?!&_&jFgNByCUmC z4rKa9rL8+0>X>spg7JWUc8IeGnn@v;!7vFX=|TVPCzCGxI&srTS~$WnL4D%LTk_w8 zXHFx@<>ZlU=j{wLC^?O_TBN=^85TRLDPxDI zSFx9A{{!*Q5T%gM4@Vq*l3~W#+&dYxzR3cF6fO~~clYLXkK3|c`1FcV zi#&P7?${`mhi|S~=QA3Fghs$v5?JmH2Gyw}VoE*Eob!QZ__5YCKLD5dqoGZ03mF`n z4z`oSi!-Cu*DmFZiWhc9PIw(DuL=eBXN{yXxNde;0g~fh zfJZl1n^xV-c|FqGTb)qzU{ATd1#kr3*nPHz>D{O7NEmq1!9C%2HZ+rn`a}8??+FWi zboH=)J=kQckm5oj0FQ;;8%m#6RJzofeGWftp3Kkb6dEsOgDLJ)=M;f$beNl_-TLz4{Tegxzh;ZXix%WQ1J zH;9tR^Au8@54?)j^t8!zC<%98^(aM#!gvcg%YoOg1slqnvY^AQ_ixCYomb&T9(K|f7{1{TYDUbLD;?mh1>Xl`cI@OnMj<2aS~@% z)>Fy`2`0HhAa3$>qt@eeMON%dpu==Tx(u58J$%$%Ia0FPBcRwrO+Rti{_IoG5FhE% z)h?oy7d(-qN-NVREK!Cm{fQ(|y9&RJ`Lt-s?e@}n__l^~#g2JJ9wFcN2pN6(k1__l zIwHM{KRBNR_em2x-LLLzcL*`0JByNt-9;mrh&8v6HUP~Q7*XA8iiwCNCaMk`fyV~Z zq;nj#uX!eu5z&pD#kkYGu-@OGs&#npx(WqpAzsszseh4ees_SZw5t2VzvU`%>@K~x zIn zzG+<nGK&;9!(XJCSp)G)vu6uuKK{6&$hujTdbGkhvmxU1R0Mn!H0irhdVPJ+(B%U zZJ&zO_eI~uTjGo(y;C+3+kCog?bg(E#Q$rNj`W?Z6xHN&;Wt1zf4Q`q|W zqs^6}6=uKs)B9tSGGe&ds{So_0359P^+dRn_fxg%8$7UTpJEZKhi6&qJMD3T@y@Q_ zu-sGTE04Kk^m+b2BbM%!E)Q<()Sp?`PIKb?O{)B&b5Rmu+{cwkCOpxnZ2_G?h!WL1 z0`1aXx~Lyuko4<;o@Y+PdtfGD{iCX8FRVX7kN(O;8VKr&DBZLl55l{1d=zQ_ODww?2EHV_!;=` zE=Y~QA~jt*cS^xDB6eOUeC~(wfXK#whM9R?&D`ph>l!QHq5oq!-U_jv0pZ>*UN)K* z`e4-BtXkN<5(U}h(R}=+mCLd2DdjvyMyCENq!PR-VbF1{dLLQ4QtP?er&nwSW{m|2r=|wN)+UW!SdjU5PvrTah$)7p zP2Af<`eTEp|1DE*LF!pl*-dJdQ!~|&t)t1No&r!-r$Tx427!A$wPlMvj+6XhAdR7r(6aG!LlkinRUFZ_N4Ry^x48YPmjP1~XEK`aI#yyd*6bOdOX357W#z zaf)ivhpz?ze^XmRW|c5Etl6v^$!hS#LgdqzN@|Z5j#PD89hlyQz8xwm4fs%1o$+|x zU)7YGMN)qqPF_1Ln^e_Cn1WZ05*F%6FVKLIfz_%%P-(q#343qHlx_6WU^D9_csgd> zI9+XaE6Oy!xehEo<+M2V89wFM@e&c) zJ1=~tG==PXkHzrR)DYiC6ymAAo%S{cyhUs?O&)utTpcs$uQD+3o06KbDr7HH7dm( zmuBUSIR^ttA~QCDe}A;cJo9u}-e^_EbboMOo;2VYqmVvxEA8Lk`h$Ig{XHD&-txPS z1iT4&wq{#1=Ca|74po_o=tDH{j|VeFVq#nChPjUgb=2-=Lb2r#qo4Lvojza}@c-!g z@~Eb+=xZEN5hEZVAjA=z5UmW#kZ2K6DMm#>K}aoChKQ+EBtpnjkXfK=m5PO^2#5?3 zA)pM2%vy_KMuQNNR1q-pA`o85%kw+_hPA%o_x(}paw&Pa@7{CI*=L`scEno_p ze)utVu?VMvMLl`O$bfbkXdvKL1(+4R2VaHUZ8;Gxt)(Aoe_vMf$1F53W0UPnBjjHQ zA~8$Z8z9)6x0R$x4B2k*vhFAa8Op7y_%;stjfZRv0L-OqD|GB&w`Ip6n@UzbO_lzmd7n`}YPrM3YhFkl1!&qp{tNoql)!R;03j{UUW_}*Q zx!D&3lkI<*k?+RQk-(C_=Ay2i_R~}QpK7XYZ*SVT>GQ_?>kSY+X^-C2^prX4*WN0~ zV=t=d4BxQ({HaP|XUXRx=8AP^L(aO?T>DEBNdUy-C3=In6AQL5Kn*-{TW(D{N)fsowV%SpjjmxEauKb29fS- zY6dnvPAr(O+(e3(@UwY2ET$h;EEQcr&~~q;HAFGP?sG;7d*>5uK6w7JeD(sd^5gi? zkt%-0vxt1G!-YVGCX##w4h`UZ^`s){ic+y;0}5iCt`Spk5BHam^+y*U ztJRDIXX{|mQi%~JI`p`s*xRaSve18~zp-nSV!dwQfhWk*9@~dJsy$VatvzAaFrdKq;AniR#5!C1p~H0meLD zmT~~x?#vE9x$ftupb)QFCDudnyS?4Dq?ZrAIWU7hcumP5_VF4)lAE#>B)QWM@un)h zdCzHLZ9*4l%rUM+2Yo3XrF_*wpXq$?9_y8Ce>t&A>5|@%o+Q|aYF}C1yJ>lhi^ri( z_8O_WFxqS{*QYAk)90{<4(rEf-Ao(i?1TKe9W#g@sTr@wW}74br+_pe5&lfV92mrW zx_U8w9&J&Wpk9OV!gETGI=ujgSUXxGkp9>nN@Gv7gctG8BUX+p6z%8E<-KJUdM0oG zsh$5m4tjz7ccT0n<1oWU`c60pJ0Lowfo28`Um`h(?iQ+#ve#58$te_a0}>GkH@+#0 z+9(!Y!ULx5KKSF*!coHFmU!xD+udiUYM*q5YiF8mXt^FmU)#F*7m{)}Pjv-WVgd3O z%tsDpBdRk3*hn23Ujg|sY^WcogDDlpJw6SCElrHFlM!JTteJ!p{(W`nGyBff^#iKz zN!<+l8%w@?4wJmkM2uK{@pc+RjQpG+!7UDrAT|XEDp)E zjJ+RnVVD84$Q4E}e(US=+i80LQOVKI*LPnu@4w`jx%Td#Gih4C?j6)}ybc^;p&kJa z&n;xsjTbA_a|YA97H*|+yfr%nP=`jxj5zj@5UCKkA}qH`pYXGea(~M3+~3^NOeFz@ zid$Fp`iC8T$D+4S&r0s>>q&Rcd^Dg zjybZU4M%+LF#s5)r00QMA!lU= zUzbF03%|1=+G9^od6;*PpE+$`x9#ZD_k2O;q|UkzG4*n1t(j5cAbBA>e zH9}5XhL!nZaWWUPUGaP0S9jpn1+43gf*J=#QB5gwMtAGWjKgWKU+dg9otL@JR>&9w z$`h`yR_S6Tl(|GVm7fqVFk>I1k%l=|0(5uQAT>p3VCQeJrw3Od9i$`>H*6SN_6G9< zEu<5sS=q5Nr&^l;?yl;N$*#`*sfEKny_d)Zzx&9B>gd?E2x49za4Nk~a-B^ay+Rak zha^!A*seiR>a34~m7r2SF73Hf!n$*4Aanl}XMg1?DeX#)8Tj3Ii(7I(dS_N9Zd}uA z_o~)uXNUJV;@dSbs>2gIYHdIPWIYIy+bf6xuF8@PFi+1V%!B?c{5{#6?SKY0N#??6 zYm_Rb6t%dAk`JI%h)mKm4a(ioD?6qCP)Q2gS1Dr#p_DCM;lst{sjGH&oPWhg_5P~m zC#-&wuq*lR5lPsSs5MnrW4T*W|dIWjDPDrMHdQO3~X4H-I7k|%l|uy* z%uT-Z`Sg`?jbK5LmD4`lXuXd*ckl7Q*~!FpWH45u3o1wgGc==xsSjJiqQ>(szN4D5 zvdxy7%30g%C;NfB)YgMhY{$gc-KG2D!RQ#!Hbcm|{EMgUJlz|nj7*(5h>p@KX@a}N zJp@cjhvMw~{1?ue*1j)XDGU)}HpaMqpvnnrkufik!N)j3%AJEBM}K+)Sc0Ea*G$^d zEJ~tDtFv}@+k{QjGJL`4qO)1BtdrlxzhyF3Lk%x%DIQL8dDc~~c0D_fB|OE&Y-qSX zO(nour1VSFvMEH7D&;O^`zad+c0wqe&p>nud)y9qa@VK->hXAKo3$zc%SA&szKv(Efk1K&3#zl&s3{XrS&YHF+e$0{ zE@ziPrw#qa%>i4Lv}E1{V@?TCFr} zm2c0+aJmsI^wkj@&PF0wn8ryaqV2g@nvf4?a9gOBX8b-BIrFi~dboNknhq5Hn#DzWo71ev%Aoxdu}0=$b@mUzH@^-+@$XT(L=W=9}^qP{>h$+4TUJR4=2bZ zu@oJi;vfk61H)_Ag`p`mYCq;076lXx1P?=zDe1+bVkAgG12_R<4+O{D`{W!lm~J>qCE*^Af^XCu@*AeC>1J=YD<@Wzc05RPZ_nxayERYqXv8V# zXzrJ;S-8)rhK~%UAzzZlFv~H`ZL3e6P=4H~=l{#Q_bqtRQ8xw2w|%l4Ta~fKb>_0jzt{(!^ba|A={%&_l&XDwA!ug-35mocXQN+@wpFIXFNVC6TJ>zm9*;LD5(fH zt%kjJ8Wu_f6B-?Y+1Mz(3&Blfx?39~FQm$8DTIU4=gOp6zvTy~YCmK#>)athG}X7$ zl=a)Y;ogU;-nD|`FJA2XjWiTUEcyT<(oiGD0HyftIxGV=8CT`Ylb#Yn@~pD~l@s%Q zctrC)A$o)%oQsiFph|wV#_Z&v#NazDWxrAwk251K&dEbk?y)UmrGguUxO;W?q`yV| zv^mninb|h!%}fx}lk93E_E$oHvGWYtP3)ygVPNSRGI5q}C`1iZaA=3DcWC4KB;v?M zc$VkLTOUmxGMR;(6pl-#N$@m%J}Oh}1bpvzkgmp@XKB}SODDPYHhORvsLnnp(&F1? z&8K|_EhZTHuT|8(=Qf&0$pksFhsAcsYJRq{)%ek{Jj#j&=+xH}E88dOi$LReM;E^R z?1K<)3q48j?MGd|hTUk?8{o8MJ5^SDGLLu^A`8>|V8s5H|>-eyDA##Z_R1gdWrK3{E%$&mkh|yPw|yy;NHk3jcSfC{uRPw>)~7B z*LlH?qGrjrPg{Z0bMReCO>s$FK2Ga#_LUflny4h!nW@$=$3C}GOb$lEn4q)HG3%4A zO5@((dwP0KPbt&z*1d<(RZEL~rgwZBF_!cCoDgEc47CMP=fa+Im+he`S#FduIsTDWhtLdgJj3uM3Fzsu&+Q)c1ju?ucUG6CO>Zyao6TnJpw|B-Eaze0c@eaH72Hx~o|TY`)(tM06k&%Y z=+AV`GGOB1vo!+bU*wg~L?3LAwO9oFdK5&(Bv&1QjHst>jMPrIP2rln2?1xzBZbt) zbrkF2Dal*=%53wt@IK~MN9}md>X3jr&yqL10vz>e`;Tbav!XuikKu^v?1Fot==*W-?I)28D>EddSPNZgM5qmA# z3o1#$Z>*_&+v+`IHhg53n0*oITj0;*uI;u@d3G*Fy?bi~%CK9v!%7%cls|uLpVV~) zwh+)D01SPaHL8+#_SOGWOARh8;DUxSGh+ih{yA zMij0brp~7hzMR{AzA07}kEL!@JUmvkCA5v`ub4i3{mvhaXGbDT)9YS^QFL7-IxrsF zTKlJWvwKzcTEl-1kVZs=?gT76FwiN03MY~@Kh)5C=%tfG$h;aVT3IjA5s+Ds>mm0dq(F|U7+ zE9PEw(a+e4%nU~s06_#K`UZ!{|L)`|;N|~_T>7dt-G$8FfJZSjFR6p~kfs&aZSPu5+8N0d{dRGxCA#&3E zk2C=H^X}cFvtk<%vDp5m3|TOe0d%Dfp=b&rmUCe%0SeImH^5rhVfF=xlzNk2wOq8; z!mj%r`Enfy=T5}m^cM285DD@n=%BdyuM_-X8S&af&LxUK55;HtNBGON5|l}81ohg4 zMqw(o++8tCSzX7~Bfew1q8o*C@HJAKSZ?19d~2J)M8Y6+mCd!l2-ah8>`-pJvsV!| z3T~-aY{MM(bWF5vaEE$4j!k_q5)qM^aCMSwBJN{T@CZ!>tVL_O7?GtbJeB#f^iQwU z`|&yCc>5^MeROmv*~rTYqN!FW-zW^CYK~hh;!W-5jepR@QU_nt#mqs8 zA`EqqTPe1yJ@NS&QlEMf84Hr#9Jqm6sAY4@lsb)+mCzf@$GG*Gx@GL0n4grpD8zBo zy^`UI*hbUxve1aO;VtjPAyvm}YEuq$?iRM40`+|$3H{Y8{6`zs^38^WfSPOk<KQBV*B1{Rx#J0?*?gPaEXBr+gIoGqK26L}GBY^yW-Ud$l$BE8 zfxEDm0*7fU*j@D8k=g*vyU;lrdS+NuCS&^iL^8*h=6qZpNH^ zlwo7dbX`{A5wzSl6{%+-hmx* z?8wyT+V5eiN%B=|UAHDd_zu{{lfO}@KGW)6Wf%lI*vGNQiLGEo1%o_`IB@>>#z4bs zX_^M!G>KKw2hJRv86sJ+>;9e^FXgH_3u}1aNS2@M0@z1Bw=jIhhiwX(zO42~7Sb?a zSa#!Jud~;qkgA>>Su|-c8rK_Nb5Zl4Z3f6d&Y=G$MCp(QnKI$b#UXENMm`&40_=WZRRf*D zbv1wAu{H|^(vZPRbOb#^ZnDAF&=fP22B3+3AG(OzK(hZCh;6V+jx zq=>pc^cs1=@Hq^?KneflPFwL<5djsEGgmqXnFz{j0Ep7vKLe; zEsHNk4fgMTU!MAF%PwIB`nocxP=awlFCeD@Ze$TyXR98e62&Yur%_U#uW(oIru4B5 z(3X@5p+2Ud%pD=?P4bc>)40-q5x(L)eg00lQf|X=Ieph3pFXeHrHkqO#CDKqm7kHoX(u=^&!A1v%lbP0TM-lW<)P zjF8Kt>9Sq)1?PE--!TI0dp3S5e+w9dF*e#Ho@xYuU^vqM_dQj#_?F}vT494Udi`g) zSGVmL$t~~gh`6WhVZ*tC{^+s)g8)5;B~qBf=Ua{|hlm%VCbH&xJ5m0);ivAuvEKU3 zh&0?xCYd!d9?%y;`3`+uEA>1i_q;wYe5vi2UWiYm+ov9b<9!2uZc&E(e|QX#&F`A; z*xb=4S{nSvEc}}VBB9$wRaWudkchwXlaR^7_zqqlDM^U_EK=laH&0P|iw>IXHGHgR-T26YegL_SxO- z{GE2{BsmdTgs)q48%jwB_nvSaURm&FN$7ONgVQ<2V!PrZ1$zeGh}5-9zGNYN8HD1_ zSFNMts%YMG_&hM$*<_5HzE|D>MP_vvC_;!d8Ny(4JY{h==Q|csZ~s-x1mvne%bOxO z-21FTsW1j6g*S%vmCtt_9g7l+dfiUdrf(!yK3atDr3orI?_f z%TR88P7J{5!1kZxQcisUd@jISX_N=WjXBn0l+t z+bAe#6D}8i5|7)&a2J64=BhzSJ^eDFSJT5M&&N%%lY`Va%F-_9y=Mb2%FMp;M4i!+ zn^JS5@qp@1TLsy(s;8v9-$ERHNDj=SYLl_-i($5-8!n`YZ9M{YHh|BH zNk-mwvk!gM>g=mrgrW%gsYpVF1 zoTLa+Hh_&`3nT&aQl#!pA~NfSEIREb&<|ycQk6Ol#B$|_GDOB)^3lN^eqz1EfW`D~ zpU|8;RO3I(J3jh5RrESVG2eW>Q}tdZ?;!#9_NRIkR1`)mK$FWym8r?+%pzam>HsBl zEKw{#vQSI4%m`fA{JU!m9=!#B$V{Uk43LiBgIz|0k8(#oen5Vrpf#$_U&*sM>@Iaf23>4K`nG#-cW7!cp`$8_bBCbXm_dXWeSp-w{q-f+aM8dX;uM z`$SR8{bA0@lGq%7;Dh!BuVl6)Cq%FLb-OFfm9hp?OX@uQV!@ryKB%MZ%;yvr0l#sH~W zIrnPEs|WrJ0lDT$XUz{A1C`4t=IFIq8`3p@F6NS<;sS}IJ?L_Bj1g1Os|ei8}@m}?`)y3O-y=KH3s)XFOz4Q`oZjmydZ_|S#meZa%>{ZuA! zzo_61_3Y5x-7x9U{2#%;mnkPL@dGbdpa=e3iv&L3jK%t-)cNvKnS#w2vl#@it`p&H zO!h&kh9spXm-`zx8aw`2k>S%Z!8Kq^hu>7wm_dajM#JV^+(Kxf?IL4#EpjGqP^lV3 z^A%ZY8mEn}N1?)ZDf)%V;^&8I}!;o+O z!u;E|o(2s@Wud;Jq_OhWLu#O;-IS&jYd7y8i&MW%4As#xvfXByAiPR^*?ib`zP%8Z zO3AYXB;b#6HbPX@;cO>F0UMN?N>L>{!h96BQl7c2T#dPVVzD`_*P{*7AR?sE@hL4u z(^88ur$$rrw9-?XCN#7LM@jrpLX$UitbM*f_Vu5~R`~xO+cqwO?)eUp>5H(9z;$Da zgT87V>s9$d*$5Nzh>jg0pgv~ei_`0oC%w4-EV&hKU@Wt0PS<1Q_-hJ^1 z1Agw1FmP7lo@h{Pjal?bG<|=s?co}39cOVns6l#c@Zg%b*;mWW_xCnZEHg=Se6bsz zC^ObWc%$EgSokOG=y>;sBYNgj~;8v zui!?xJWlf~G>i7}is`t(TG3^*%I(>2vxYMC5@#6FS0d;RfM?}09NptVX+}Qny(Z_i1%J`c_Zi_2ka~}m)32uk4)^Ew_ zKbkvmcLuTFP&2^#nTuWSl#w;R>#P8=jPFnf!*QGc?Gz&a*D>t1kAnwUhgtv#Fq_&< z9ZYi&n&HP)(^GNHxdm(nzkMRmj@LAamF3}_SnnrjO~u0-Y=;Ug|H0d1ucVCB@2Nlt z)$$KnC%1NL^x2GEe=!S>9#!vyGGh%14{}$W1C;U(#Gk_0=Z3GoB3g|Ga8?dVrnht7 z0IIbhJxzkkD8ixq2?&jIAq%M!4glg3)Tge@oXp4OD&F%Ph^y;V= zzNL+_OdKkKDo#{~KSMZqPs}KKM6T%x$KY1GC|N~rL$+Hx$7=W&?AH6qVs|&CEoBCFP)1!4s{c`(tN5IC zvLwXk!u*n>e!e5;KKkl5bp=%&$%lYbx1BmD5{YT5C`~ywiu!IaM7Q~@Fkv~i*Kn_*_ssVPnj~qn(WR-guJZe^F97|YoFc{_b_AZ54?Nl zt``wM;P-0Jhil#r&n_ST>k^2w;AYS`Tnl5@G0~89EhbOjW|as7N8dBw#Hzed;U_2c z8eCg*!`V!h8r3W?$4myTsdX0utd+K=(RqB?I{DM;F21Ml$g6$vX66*LR=)#+TMeFx z{S?xje7oG#=G;f``dSP6HC{K`PlqGqdIYyPt3E+=Aak@og`lgXq$Z&;z7@Nublr#D zgWJbk%rf(-K?PuXhCGjKiM16iv>r;F2>CQwnj4~Ir5yTm_+Wth$#bl*&u3QMROGav z5x`D4tM&#A?KP*3i$zjaBjbDN5-Z%oMZJLNM#|Yp=}-&(5;C_22~pmt6V9IyB?)FI zKV+f3-*?vp$vq9cl`7t#g4U3kfUn3Vmx9h^|J?KFdrk3&!V@89EwV1C3L}53a}XO~ z5S9}n2?;V0D6G&XR>3?C@zcXunuAo3RuTuweH8E)V0PGAX)Kqd@`3K}*aq!=vh+`j z^*ythkRO5mf+^b0-eSS$JhJSb+~Be$kk9q54Po-U*A9$?toCo2>JVxpgMG+bFM0YW z_;Ryzu&t1qUO~?jT3AvMjHg;PwxH4YBk@3j6aae1k3F~(L$<-~fOvn+`44Gf?z{is zzrM3NB`o5}`!(^@f%X4I<4e{8?N!JDVzYt3k}7-xkv2jWlq#6X8ajl9%gH9V8z=$Y zIilR3@Wec_w=UZ$I<&7*Rb`o*UjDHv{$9|}w*tL82h6`0iy6OHWLy66l(=|TkCY|-=RUc|~ zv%d5n3%(#6uDZU{*=U{1(6z8_+93PeaGcXEoH)lVw<0XklgN07&s08yr!X)eE$20J zXK~&E%V87ls|;!s&So2wVjgl{{594PV2zYYkYAGhATi8Qo?gsKKbAS>`?P&57d@4F zaqhM7D8%k&>aZ}oeWgR^I>MAH9S~MZG)u=6mth&(l{sIv{&@5a#2XY*-?&4el?5Kj zXYdo)BpT5KdAzfovPUGPT_(&zsQTyqed+{uJ>jJI>-eD?O{Te`*znN}x)J-@^bxZV zZ{IVy55;{*=<&W0n;@OHeGTEi&cHV!>~9AxiFik~psZsJ$w`e=ak@OZbXVazr3v>$arx#>v32*=zGpT+r-+pEZjk zp;p+$!ZR02o@iew1`sAad0;upZRRDzX?0PqswcK7d5P?^cUdXVkpUZFoSlb>EpJJv zV{UHogqK(h!X^Aae*rZNMbXy2ca|o8c>V(`hu$ zCU=GN65w?Jfq4hI`$JU@-;6OAF^7;YlPFWYKg{sw^P^l&IX48BLOSfgMJd*4oq&dV;vI z>#q(gvtu9B6^%}H_WQmKaa{EK8NOL{65gApV-{82_akYN2g;+f{yhL2yBZ1Qr6XmK z?&M4P>Ce*>#bXWI1=~4onLhQrL|+LKS3>ttu9>IxN=0Bk;tzL;Gsm3!wpZHBS9%=K_rAEjt)wDA!-2R*hi}vs7wL;Wu5%*f&r;OHoSIUFa zZo;>JL+PH7LLIy5vD|$fMs6sktH)kuM$2V1QN2R{3in=&iplkg3h$kv$b*=X73%7^J_yt|sQU!^@t9 zbTXN?qrx}mRMmckI)y1t*R(%Ll`&uR&N4RCZI1sesv8&k?^?ZE@13v6uI5$Jid%c$ z9tYTu4)OrFL7(a+=&ncL`!#tmtu6w<9b|PZ(fsFh6ZZN$ULZF`(qnJ$u@jnrn{B|X zUfV+QLYN%S49F-Py>KCu88?Oubx-;+PUIi0QF+&YVBF4)*CI6la}|_>wN0(#ehif^wl_p={$e#C&hCK%xZ4}HyCGi|n_A;zgZZiwp;8xm(+W_nnAzlGG z$e$;}4h6673NH>FKhVwmyyj_Dm~b7@0*A+@FJdr4;evt*1M@P>d8U%lYgm-bAwICO zQVA_XL#&}8Gc~^KVcoq^-~G>|T>Vh-t)!x1--`5y5|9P=>G}1+ttp$wEo;+usbdM9 zbr6OC35dDGav~N)tCk z#aINK>o-HHc(j$^D-9YVz5~dQUJnqiF0OMISjhJlRTqs?)+jSl*milrnUt)&VYt!W z#WQYIR|j8j`Jr6URT6d@83F(+j|4hJknwPDlE;rFJp&+@RLy}fo>%tJ6dll5?PzwMYVl+odWd*|#bvP`P& zALsP%YtdM4z~#ch&pgbEr?{c?P>o|@YEOi{Qo|+I;DMN@Z0r)T5c6j~Vy{M#L}FFw zw9VCT%zlYacM;oDIyQeeGLLuPw|{uR@-cUB_olG4EOy&~pC)g^ABRkKP=AYp0RdHI zr%e%m16iD)imR;c{Hz!&8X}u;R&I|U>G=6+#VC)+~O?XyjW)wF!TnqDY?nc2kMG zEZMIz&362=@3D8xo2f{;C-mL8Aj4cBITljEeLSSN&>I=o&%gPQt2&DbgJwLfzPL{n9SFf+V{$T`Rh28z(m2-Pz(J0YKMfZv>QT2r^$^`3y7MKj2H4>%r>Hz6{j&`57ClTYm^TmbqZWlu{U4iP(QhiT2nhtnFmppMNee;v8e6*S7!AWS)D6ct_{Q!|s^l z+D;9U7_h@ZWhMOz*Mu;^_th#zS85PhYEZK(!<5^=oy*`@p|^@3c*+?4`-XOQO(@(> zC3_TGGQ4X#$2NUB)7bXcO!gZ1SrZz8@HBHN+$=uA)8qxnJEEOx2vf-P7>N-`%AS)7 z^w06Kd zw?aTyGg#22WIh1WjXux?l7;g)`nbtAdFK5fd@8WPRyC0q<_f?cisj^s3gafy7a$?o zn>%c?TpOo425NdElR5@(iS~9Dui+l3jtTi>&7%NV88Z$~y9Fl0{mqC4`Re1?=x)tT z!XDp?Qd>yL*{eY+eYBZAPhg7m$`a!FbZyqFL(lHe{91kLG8YxkP3OH4F~h;>%e0`p z>-6WazYCVAjEy2Hmy@q?=OWPim4$m!tDVXuN7 zp%Lb$WB{&->nJl@WTG^|s)oU^Lt_o5k|aA`f-zlZ5d9r-uvpdpp}gmA$3}9qY16wZ zPyRpl5ta9wWy6;C2LKu|lFStj-heA(ERDVd*G1Fo>7@w@@_j6!Wng(Zc#bh-q&i+A z1jiauj#=uL<0Hg+B{#VS{7(bOZ4=eoN^B-|`d$?iZq*GZnNf&!@y!+$T)oeWpG+X~P zrAo#&Pa{+Hqy&lKq?w^I7`<j{^nWY5747Kh%dq9vYc-$bkh2W;dfjIkf{8?%(`RKoC+XwJyGlTyVYMno)FpH8%T z4|!%YxTTkn>^e;e68d3HIxtA!RPxxwNNT7l{BAJ-#%jFVw;i zAdkS35c`OvypfK#!9E%GlF*K@_tY0~(Z)n_B+2`@Z{WMWSiKCeH^7J^@Whu5T}2LV7nm{hAk+$MUP|F>y*ZgNC~4bz^|I!$iOn<(Wb$ zg3+n^l*I!h^P?HF=|3sY6b}mL4PTh9Og`PC>pSk1aj^UE;b_D*AID*=ri2;_NV-T2 zm^#V5F`Wq6pwXcQ12IRaxKADI5&|kyYA&qrEZ}sJ7t8@wMMwW{ZA?i>`Lsyk=Tei*IDymcclAXML#xkiGLVdh~{1CRZ&1vyQ^ zcc-V4@umaXQ)U3e1Oo0>p3(e8EfDx7(s)sRuc)s*p$IT>%x&1^%af$;SwG>o{4`D!=LO91s6ptu7g z!W?6Q3JJ2oW`46CMZ_P^*1 z`0&MP_n!RVUhrZ1pYS{w@TXz6+NzD3T;WOD+pVfJ1TYB_TG_~*H;8GJu61Hj18*TF z-(QQ4rERjM&j=Q*L{(*ljO@kTVa0abMZW!^l``FxEaktwYYy)53e=@ON< z?|(}?sTAR0A=ivpMYM8J&#^vtxv<_mjBWSLiR(GpoW-lNSk7=_0G$;Hh}JRsGRIN(_wOy77ziot0_ZZa4+H9e6~2%L4~Clne$C(D$7^ta*Y2ja`Wh&n{JN zm5H7+m$9gIrk)xc@jY3RF9T9=f8dEV_+VXVFVpzi!W&^Judmx(lkeUnl-~mY;9HaA zZq@GGhanCU$nDQpR-IJza9I_SGHz3!I=t#hwg)3FX1bx?!QSC#p0@cCXZaA16i=mv=1>_8wWcQ13C0K=|lF>UM%ANfH%3%Vt5ceuHoBEIciW=b{J5rbGv%L`aY*NvO>+V3`f3 zrxNBProtwXHtU8|P*8}KwPXlPs@u;4=oCz`?kF$z35W-8A(i|{*G5GeH^0zt*ObjU zhu?(P5cJp>L5kgswe#q;|tVk~onYzZWnLb^0s^H-z4u|s!GB;Wu zGDubEpopv&j+GdZA7Yi&3AVR0ZQs;@tVvTgVuLC^IY5>hI8x9mBBkK$%1 z_J@75vmUwE4(0pc|2& zTrQf)@EKUr9X@9KF?%h=to^f{&Ek&{M!sFsoUA<2FV^KbtcsZ*E}qFY{v)1_{F9>k z5nkj0q;9vs0a`ZmIL53XmA&RZdwUV9ti{x>CSV121HOP&L;8%5Mv-hupJPR(jAsfL zrthh^dm*OY*YfG~cdh5npDA&B3d8Ak=Ds5>v?@Xfz?G8s#z|EQY9VLUYlt@POwIxT z+O00Jbq68S+@K7vL>fpW24%Z4J{E))8(SKy+?s}BYaC8I9Oc~|c;olZO;gDTamg+I zL=6%!nD+f2v=;tXM|CEjRu$H!IEB=GdAUmlBJh^TcHB6Yw%CTQ$^?!tpPKzw{l4WR~xreJ7P{UMSeU;^;vt>U6)|$W`Qks zPnt^i?X_br>IZ(=lNIgk6}iWTy?uHQy&aD6{N69vh%uZkytaYXr=#MRR#nuNYx1G{ zbM#En=M_~Izq3za$pc54YLaZfn|-4(%xuXuy^5+Q_M89OjkqB{anXnt+(52s#`x-A zK!7Kovk0FF`{ss}B2>Fz?t{Z!6n)ISk*XKos?%bzK$+19;;r_a)o5>=ZLQbQ_K8sq zRFLPzlrp=*kjfk45&P@M`~2OQhP_{CA+Q5RVFz52`1>96BO17T>C@_`@jWwaKJ_b; z#mFkV#*`)Bd!%r+-I%ja_D8fjm%8e|e}7Hy*d=ZC4A_p#2p9GNO}=mt{J_&&(o-ob zna1oxSPB~1a9GJr8t4%J6&-QpaZh-bZn)Fajq+rPqW41Nk)q1+2cc)ie50Oe-*LJz z?MJ^OLHAJKenXOT;gQ-jX-JST)pWZXXlky5y3h4Q4%Yj9S=aLJmF@f#Mc~n1GY_y^ z>!U@$7;GH_QC+w-k9YrR{JQ)!yW3y2_5^2K=FMVfpcZrHAiRHiiA7|HjR^vnQnBQG z2NHvs)KlrP)P?w2CB22bu!yo9>unY;`^5GtVY_q?7Ngngl~b44+ohnj!F*0}LA8@? z*e9c8w1jC@aq(zmMQzCaV8=Ci*8&3{u)dM+dNZ{Zp;B@iL_o-yFW3prSLKf@4DBE>(#>(qj-SBAL-~)g8cj0sD8{OYF zzbizmRf)S#ivBiCSNtG7di7G%vUUFy25L{g*W9HJ)|lD=5WSLm2|lYhTNg8abeths zTApgv0HeIFvamB9V|!ruD2&<2S%+@rBF3!PeRqDS=@F2FEv!&ZK)^YFHDpEUF0)$OzrD;oP>P`rRhd5=H_5xE#`f3Nz?M+K_vaFmW8?H*~$$D zt4ph1yxDzB@A8VS-$*yUYRT;A;AW*+OnU?2L^ntQDR#RTy*nQ_gAhNb2}8a?<*}%H zOYtmuMm=*5+Yx&UgO!-WfnI)k+~UzHq#lGzTxExe4Jq~M4?YIiD1)xn9J|N5;eMyC zsx>3gn(4f=C?uoZe5-dSFD!r<-PIv{NfFl4uq}e^+NspR2V0lE+cjXUxg|fg&*4^w zWwCNNdow2XH`;aTK*V~aN?+^5Z={LU-&&6t+_NzgNL?$r+y*zGtLjr!J2f{s+K@C` z@&}Q*SVSYx=4#HWcMMbP!2-94&QkB{!1qeS$Wm1TItmGUO6{iJUF~O57}KJ}N$dUo zbg!}HfN(~hu$9wowSS$N25{G+?4h*r#kA$VbCra zX(Ll3Z8{{{m!ny-wdo#ex-~Pm->2vKy`IdDU#^cWx8av6|Juu-6xE>DBy>Z4$(}$xoKmVP#YiWw}>qWIjKEXBu=VDB>QN zG+BKM9a^K^a|BEHwKG54EB4142tRY1Rxss2`i&^J_O@@6saB~J|-SjCuK{<9;NTxw8Z$(zJ1OWKdzb#t?@*o_7KP5iIgwFqKyN_ zsD2Mgc$-r(IT0oq6fNY&$Pt-NaGiB;odrPGRNZKpqnw_{@O*AF*`(&%9m9`vWAI>@AUw!DM^Mj0pI7i9RsaI)Heq2lIGrBnJU z_15-ldV~ETJ&G5NMYJow-Fjg!K8&i5AU5)tSdFMOha7Ih^tc* z0Y~G9k3L3w;=;~rXH%yVX!V(zHS2|yH-VY+}_zQY%RcoTJSB3 zyB6k&{+qj*VE5$W)M@9ut|fLHQX(Jhe8^n}h1@ZnqGycv+q@~gPbq!>YD*?!EABHP7k&dHpy2Kk|x|KIkebu3-VtnS406 zIZExMtR}X9lM56ke=0&SNNT>lVn@z>>4^?X14# zW4bd9Nw`@{(SOVxC?VEPy@0t>qLa2o9(EU0>RGxHa*`dns88;Ke!3z`BkYw}CFT42 z0p(+TH`B?&UbhVR*?gqc_4uvp%e~+CZ89uAeL5iS_|c5ljhB^_GtTYeel5fan(-%P z*6_`=e&719QGzIg8Qe{xB_!}2;M`y>DJ(L>I{HOhilNERhq69d#0z0TL6@OoDt>$%#?Q zlF8`_r=wjJqy<=|U4&@x%VsZ!O5Gd1whU;uWPhFr;>6->+%wI|h?O9UyrXq4Mj^EuWoLY(4+ z(IvT}p(}T=y^urb?r*EUTH*Y_GVQdh!N2N=SNDWDz2DAE#9yMwCJCnqgH(UTojOe{ zLVA7f`U<>75wer)PUd5@mMqZi;cbM*C2_?>_~bGdMS55X=Hw-rY(Wrx>=4+hx$(-o zKfkNIHSafxdfN3jaR$|*;_1aa#{#hb0712bAHcl*7F^z&yaO10fSRjXedYH92e!4z z_R9C=tWx?-)%jF-mX(aFS5my`IVZz@NsF;yrTvPg6|~KdnFmpumes-SMr9KFtoipg zY<^8v+R(at!vcj9UkJoi%$(cA#*9xtMa`iW7@t59(PuVmtK9c3osc-oNoHC2FfzR` z>K%rPOE;aMjs)~!7oOu*s7bOVNoTa?ec4m|?GwpT&b==+$FthYsJ)SVIY6pzkgzn* z|Ml4WG^LP@Hh9bS{ZyJd)>{rQ@$0r%kdLIx5Z$(q{0!tYZTY@+eM8>qtc}=}oUK2V z!k>6}nBJ=Qb`~b%Ch(|C4z9)(O~YNFV=mXWWG$i+X~+^NEV%UnT<_>K))wT2kk*e4 z5H-(!XE&rvHXgojnf|-op&^x~cIo`F#u3LKs{g~$ME+M;TvypH-!m~k`Zm;X=qy9F!?*8{EYeN-vYT%W?G~c$gn@Px!lrO4J8D$~&S8 zZU8O;gcoK6S1hTJkt=K}OtL`YYe4pCwhG$Oa$e8ccNKo~O69hJQDJxLs>LBwBh3Bx-5^mh zGRnmDRHW@UUm+V|48NB@nP9BM3WbWfUxuLLo7+B=9zCiqg@x$d-b8E`d;tXHIRLOtawzE$$^b3?6( zxVq6?kICp{Qp7|ce(;VLQ3puz(0msV+W^r|cvGGx(S4PoY zX{-9;+?*!b5de_h?BOhqhWZt?Q#1m|eQBkY3AVWhg(!*sjld+?w+FLMojI5HcQSp4A~8`eaM(%UTc-y5>n%#~bh8D--0KN{~4&k1%$5tB!nm zFRnVax{T*$K0YQ&Yc+gq^GSC6Xq*=#RgqboG;?87%Lh$#yP3| zE<8-l_nlAl-p>=&1Ks(R34N8M;hWi8V}PwT3+G*>*2#$Vn)v9y6upY`Oa}Isar)8Z z1HWTP!PTY0u)QCa-Aw$?VZl!B_oO=nDZIUyW$-wjmF<{m;W(|Bd0x`q;AaA!g0y(f z@@e8#(uyg8YAZuPsBB*`g6pG`DS*qrY&M-nIUzB~z7y(0Os?Qs@0Vy77Kb7>_9fbP zf6HmE4pFZwq04+J%^dlv-K!yE2Vw8R_rr8&7GJP{mcH`Q%ZCVqvobR z7UXWU{p*S%3-rL}_|k10`DzDv?=LaPz?GxXpy!mOApGwtY2Xe()+NG#D&FSkDN(u1T7&s_ zj0+fa<|ScvdAUTpSU8sS@L}azk-k5*h&$!GJJhqs;z>c~p}MDCL#^~^N4dMH0y2By z+r$r`&-_tD+ip$XAx*$O8b`g$W`-37&->l=Ic@Lp%k+Q$Sw>J+B8+M$Z@5GF&R6jG zh)_ZqG!dCh&?KEA!Z=@M%X;0zQMK0qD=N|p#rE&Rz43tl<%_LqOEERyE?`ekmI?fs zyFtqn!i~?_>ri-vb=4T8(DDR+{UIr|b$B_QeqmkZE%UwzoIq5R+ncbuIm#>@$W+P; z$6KP&SaAZIw+kKieS#4X5t4j+8J2l()Ze;$oVV)^b@n)90)%S@B(zD9mH8mlD%++x zB_g-Ky=df~XmjN)`_Y~9$q$;+scQ$3vy{Q@y8;;hz%QuSw$X6=PJ)Ei^yX@b(w^S~ z={p-t7p+p(ZB--(WKcj5_}6P2hMWmNp4f0TZPkAh(6TyxN(*r?35{vdS27mP4_=_? zExMPiB!3AzycTx2BOKN!nHoUo9)$6f1R8ReI|4>j{q2Ql-b_q`;}c1H+9c5#eLn}e z$$e7Ir~DEU!54kz zE`Mt+TO~bkd-Kh3c7=Y?Q@wvg2M!!;LozFskOwnzJ$SH%@38CeH_WG7IE&>B%vp#e zaCBMoTSa~mZoMoG2W|-XN*))s6aA=FEQ4z;4ia_>k1DR!Y@FRMh@ZBNKD|rsP!i&k zR*{9+#Qi1Jcy&JC2OzzWI)cI4k z!-rq=jOo{2pSd>8U%+Gny;G0UJ+P5ZEj4YJoj)?9@B2l*Tf0mgn_rzY&89*?)X&Yt zwOiyXtSDykT=cs%GS#JwS8j-9BH|K@lah*>5>qXea~^I77IQ`Gy*|V{ug$Se4!$MM z+ITcKW0%n>?&`$n+ikXP`tp4HqcxYoiR(^+Tq6@-+OK$kgtian3WLTQWFC0CpuEw3 zPJG)tPYxQwiTofwf$5AUq}r_oi=Kh}c#g@I*s;At|21@oxJ_IeFI;7 zK33?a`mgPz`~I-7MynH9kWs{7Nc}5*g^FC$T3XQonGn(+AWWzS9awgQf$CekAb>9r ziBr(*zqzjq`oz(7C898$=QcWE`$t(yd$8zw;pUDpXp_)wzZdxrIh`yFuG$)sc0W|m zP$*w+ziZu3L@DM8Y_(lkJBRjy+EM++m4-X6Uo+PY$#7o#i>j)wsqNEwM^}_m}#9!;D)zqS$%^u!<^C>DK$0|7Q_cguPo?h7*(7zkbAR^d0 z9l2)A#sEe>Te@D6lX38ahqDnKvWNAY2bt!W3*vT(!kdn_wpa)yvfaFs5R&oCRe)XueTj5(KG4#I$lvcF|P4V`lkC~ zUeehur6Ti9k^7n1vS_1MGF2QINZ-dc&S1476@NIvYCq%G3s!A1G>myY-ni+0jMtw^ z#&1=Ae8oC(f*IWVf12b(ZZ;1laV#cs?5LIk657^b$o4 zpgLFL-Te7=i-XCM+&z+;K*($vpUDN!@K++P7IP=6lWK1=CZ#N!Vgs;g1#WaJr~zM# zW?!}R4I{dv?|U+fzW5teKv@}{7V)b@{Y|lm^!?(R7zgEL2iL?bHi}uid-3l5EF5;n z!=rG~A*dg@kDg&oPr!9OoV}0UIF*XOM2LnO2luR9_G-etD2y2mEeT%PQJd`yVY?tUHSddmeXKEG# zOrs1Hs}mY*JQBDg(m&cBwmt~Wsk`5MEejd2%uiKum;Z}hfbBik-q>4ecS)qjPC?3P z<5VtlTS}vD0QMaUMd4&xorVNJCUJG0D|^_oW>7*zH)%pRya^SZRak7}>gGbQ?? zSik#5(f|}k#(d$2xsa$vm0h}F6s`gExc|y@4jeTO5+V)C8V9`8zxW~gjt@Qmc>HnYI+=3QEPqA7C^VpF)7T)@t zAor7Ps+VZK5a=ObX`_v6p`TOh*2uS`4iO~}kt;BM zGjDYD@jLW!`k7jrn$O!xM%ba2ODBth-)SWr&6F+TeuKO();2etFXV<9HczZZ9t)b& zjjwp+1x{0xl$SmabNlGE=9G2V&Nu4F{p(6Aj_y~|LZOp6CKW@>5JIEkQFY5tr9tcC zb3@j~bTq;deb-?}I9cR75~9**zezHAO>Tz1ss|iR={aNk6)IL%-neahxF5TBg%;AJRtrPetlqP|))TOf<1|q!vLO!1SS2(uz_Fi=ZX}vi0w{9SLuc{&Zo9Q5qc)!^AB<58`nJb5xNv|9I8 zw=_wP{P02QN8vmj+gDQ+HQ6tdsy=z_lIZhos8ht9Pz|K-zW=g7tVeC-hm2vRTh=Ao zI9@PI8$WmOmFd8VO)DwsuoK#H(-GkV;?n%!|Mr`Vd!y+_f95WJFNPjq-^-#8VW(4_ zSJB-LsklBr3rYDpi+T7L*Z|xKzq_peILKq-bMt}D2G)vho_Zeh0=h6ERfMvhUJ5E2 z_1CY-82$ch*HkfGZ;8+7R^QMZ`lwf^ntEP#4>7Rj$)nDP>$92#W4dy*(8D0?Z`1x$XP?oL;lRroeSx^B0&d^55mQSeUFx{oh(9knhi*`;%#(K|Mm)0i|QM zC}{jUTHQj@v$q1rRcI91uaH~5-6AIsBYN1q@8dpJak>5Dj+-Bpm^Hnh_}Eo7dzPIS z(le9O{Op0hS8$D49A1h929mx9WZT`HuZzIKz9D^!r3{wi&wnaGFKW8jez7MeeXAn# zM(UxxD=Z^D63Dgo4vLqM9#^xci{*Z3bCfspI5?_ri6h3;8SWRo`cUp-zIueEAMN6% z+aB|=>+74?3#B2|*(wKn4U=2^s^CCX%5ArlODGeL5HLuvoj33Up(pF(_PbXP?>L$w;wM=6`8HCtKndkU|A`P?=y<jVEqZfS;j6Dh-GM5;n|88b+D|Dvb)AKeY!J9dKMswniW-v*5toR4M;W>s4 zKMyRMeQ~#&C;@+sED#0Z6`m8v_Yghy%Yu;l8!=ul6>;{PHfpTbB`Y)iw-x`l^$np# zx^Q0oZ^y5!)-p2W!zLeCApH|IG%W-+rtA9)!{jFJa;B~X=5x;)Ig1{IzqfDywR7qj zH{ten>=M0T%XJZwaC<&_{c7YT-LGSd?MNby%7}K&;irgbcf=vzqbdpJyj>TzWaGxu zicVG!@-2UIE_MXYsfNtHB^i8C0?cC@Y@+Re(rc_Uh5A!dfV8Rz^sMAC?0Mxpv?>;D5%J^f&LbBjnxbPhu3xW6jP^+mR1x~*&ckW8{=G~WG2(Zi zz44+Ak}XPO$^5#{7kOsrhwOr1^Z( zd)2mfnyW$`THQ5mW(-Lx?My|j&**G}3$Lc@;nb@r+caPIjL8pN0(>eD6q+a1#s*B1 z!r6U9kykf!cgaZ2r@!fdZ*}Vhe~l39KL!2zj*h*&=;GkWwm+X%vnPB!$_ptYKB)$Q z1-XIr5R13YGROT&th4ZW$64n1bD}bQ4MAcJplPEHqewxmZ2MS90Y*K7MLZbc4-D|x z^v>-=!9XcQ?syH2L^gd+HTzH&=6>kOTTAsVvc0i~pY$N0kLjX1xfb66FQOa~zUI$v zGf#$VaG#t4FX+lorCqbbQ&Bh}5M*PbS+9uFOS&|?n3U3u2SM!XL{R6+0V1w?eHan+ zA!JFAT(gv?hL!^YC(JteA=6!CTc#6BC&lTe<@$}?Uf;zsYXSLU7Kj(V7;N$1^|WgZ@^(FROh{==6Vc$r`tNR!9(CA} zk^HepB<2(NfXmcn-th4zpy0r%cDgpjfi6=eZJaA4qzIN(xc)*~(ueWgY9$c~^p>}G zZoAy{vvsRQDM@x#vt3qhuQqfFyk*P*rdum5&{+HHJL2~_yqPbU^4JI-&B!jdN0s6p*Xkk$D*j#mLG+8Q65vqMY2%l1|CL~xibyt z(0FmZHA87)cM(l%e zFOctz`IN?eerIt+MNE=A!ZIiy2IZYab4hqy3&;dqqR&>MB|@EgS`2Za+)QR8-w29Y zkO4E7qCr=0KCd&t0`h~UlOkJ~7OOYf#Ainp4yBYKrMKu@^RIGO>-V+~-rmgp z)#ViluP=Tp4Kou~RlAlsEIDdusHm0^;SJirp+ow#JDWX_$At0kLQag4SiPBZ0U5Rx z5MFUdXPfK7|NfBjYbCFj=luZ8nEk9;-rV*LTQ4}0;F3Hl7E8`F{r4S`=a6@Mh&Pam z$vK4~)pS{)+_TGm7psH+Sf_!T#Mh>$nH+pa=dM06>TiX<;Xd^GVl@$zTlJ()P2!ob zdAf9O1NmF{6MC9k&!i{b`nVIZ$gYdFKGbOzwhOsJL>;-LCnB&eRVx=xkV#8J0f zC2<#h%A0ME;d#yS8wo31MQwqlTQ-Y&EEML=_S+!53IAt_9> zOFujR^5XZDjGs!9r6%&_ws@+Zs2$fK>XLqZ?44*@G4ET#c)Dx}-ika*h=H3Xf%BO3 zT%ZCbqMYPP2{1AGu;9^|nE|{aE4^>o+6bhw2+ z1Fc0A%a(7uWYGu<5<@iPmS7@U0vfEQ(lbx7&yqJJol0&KaIRG3M#E8)^DusMRU|#F z;AvFhD6Woih3)ENv-!0jG`twujghD04J9f`VRXM!uI_%frG9YL%QCp=K@1*pou@cR z@fUFwNOn0DA5ocgLh7M#KLc0~6?a7?a~>j^>j|E{Oc$n>8S&sBFO-dPvPxSVg z));D=z+xZPb3h3<^>89+QD@FF{UnjI|DEu8o!RiYpTgY#^f52tbsBTf_Z8T_YleF+ zd!<=pJ&xZf0HneCn}uHntZoJ51<&qJE4Yz2ViTCBt$X%HL7#6+NI>C;)GECRR3QSX zD?EC#vfk~dh0#`=YtTKAd~bS%($W3@=g+(!aD6VkjP=6TAI5gbK+}zeZS2VuaaZA> zg4S!{y8V^tc5Xr2Lk1t*Xv%BjzaLHkJSw|jG}1beoM>^7H*x5@_dClp16LC#*_nvQ zJgZrDzd4`H#?F9xjX9p`BIe`ENji`dtwG+fp2)0mLKG;?{jP{7jX#sSg1R?S;Q6EZ zh>C0ulVbsdd(1>7h4dRojrptMswF8wumlO<#@py58Fy7AP-!ar5i|^fD85$mfT(r1 zhi>~!uyhR6Ffu^Ky{09Z9@Rgsrk5vKJ@6(s;F9)T^N-buB$%9IM$K_ts*ka~vEnYz zL7QC?YIDM8CIMHwc}vBYP3Gmivy9dj%bI-XIa%D2OKHD2P+NE7 zlFXIzxo~do@>2=gRj?UE0P-M7?|>9z?{n_=&hO*%Uc)swmun+arx2~VmR1tR2MFE* z{K9n0%v#w3`B6m%%N)2H%5qSG10(|jZ_`Ju8zgU|YwBc-^2@i_+ zmjPx+0;?C=%7-Pxe9a+@C7$gc|IF`rAV&hlAb9HGb8qmK@x_`7E45;;OV^~<7dy6H zn7s}W_ykB^kc4HFN1(R$LLizVRywh#i{Ok~`4;m`(Oywc95Ib$=q!4*g6Gn{I`p=_ ztIMx=GkPNOR-ts^!^&cRNO{gsGh74i`i64)aKr2a@&}NWy&wtMobNJOwod7M_4%I~ zvMIguRw2@vx#=gB)VH8ffRe#27V_2A_G{%^uymO!^8n^|zG!r4O&t|vq{w2>iF8!1 z%EEOz!jqV{90Ai^SbSi1U1400nz`1OI^zI*z;5AAH_K)3;GZnNP#rjCZvZy+fQ6g#k^s3c>t0zq8xr0<0Ah}!LbXuhtzN2 zncM}*R`YnfunW;(Z4}s(iQHix>SQ{WG!~x}vsZdyu@ng@iSBCdTTu>fI%A$b(!xR~ z+DZHL(>>e0+D{Vl_NrcoG3lwvb5WWVz6>4g>X8i+@Hg5n^ zLeKQZnrly^@)%CdkxOH8s!BJ*bNTQqJ0`x;OZ}tXQ;%nS_vkX`o+V|_ux*3*A%T2_ zHQw4j{{5#?v2{}-8;WlT_RgAR0o8rq@r_3C0r4a13lA^xlU$gy_l|;aQw?4y{3ZaH+XC8y*ju}e$sdU6^ z!-8qfb1;4_l&>lc12vixUaTeq>$luHk^@&PZ$AZN6hw9Wghi#fT}b?eM;DnV`7eTAhxQ0-J8D&10Oshr*Ky>j#I&(0hxIh? zME<9n(Ocrg?A25d(ZFBPS6^reD+%%J2{R6EKl{;>_^9HD$_KJsJ9C&L*CYsg#}j)5 z=y3u8ix3m*Nh=wGhNBE8k;Q=D8@@!X*?;F-U`GE&6)1v zG@}38u!_lFM!~i^f$rxPY)2s^FdgL(k{V9KGtWf|3Pg(kJ5bai_1`Gp*27P&NaLp! zC8VitnZ4(auh72woz=jp0JTn9Bm9%Y6z69~o;tN|U@3)>W4z^dq%7X5WYPLq7`z8qPxhPm&@r>%a>FeIk z5oyot>08R$8ICJFw9ntEPSL?tRemaM%0lXYBaN5fout_}{KtYHlZe7}?&_O{oIOU9 z)k#73vXcN|&@Q9_Go@j-t&q|fm9m@YgIy63RtDP$5^!V2V1rG>4XvsYk&L5*H8j`M zm5W>#ef_Y$xG+SQr(JXQ?j>4ubpE0z$a38FPLTq+U;4kh z;BOcR|Am1Td^lNyq0?fo|BmM;omU~D|Gl;l&zH<#K z8p*j8_gWfJ6>-#gVH2-oKft=R-ogG&{2yzHFU0XAnJVig|4~q^+(k(0*AaTg)-493 zP#?L!2_o_1#^3bnvLr00T1mH!^tg|L;IdWmLq`1UplgM*QAb7VH0ZQu^SZuT44V3; z`5w!f9P`;QCt0*$m@v3t?zCvL1U?b575o92Gz#@>{$fw`AJxIto=TR~NHa^YNI##& z6VnGbTf!`YYP0A`&Dnc@4mwBYxfHeLBYu89-u|w?&s_A#Y(S@h697e%vioD8z*H-) z)ahQ5W-t4%;WO4Uys*pujK=1&A63T@8WB=Hf zeHDrX32P}bx1QBO7!@6;n z>AxCd3wOI6sIiM)#q$+u*VaAl3>U2E>;Es5rPXulNlgd+6*cU@K*!W*&HCEH5G2|Y zh5fr|8;JfMC4Ft>&wMqN$uqZR&=CT}xiEgenus{fuzf{4gn1y z&eh6w9ZZ+If-f~VTRf@8VDIDQs6E?JgwdLY4Bl3bQFkAHb>v;8NoV}c(vX4p%!o^Y zBoyk3Wv=xXh|i$K5{t03ka{D#6=Z@6`cZ5yX7qORO zj1gqSau2bQJ?;#z{S3oY8m1_+UnY-&JSN1Sc@9+Ji8@~d%chXYcw7@RrK34xK9cOK zm8Py2>c5Aw%rnJ;YlUpbPRl_(Kc}M|rdaY8-*Zz{D-V%$5wxhORa3g50 zr0@c`$fz!oR53yK`2K6p^lsA=9hCtp30w=p7wMdxiaj0PrLO$YDi;VGE*9Hw#-sel z$7jn52&uYSl^k)N2SD z`|Iiw)t*IL`&XC8w;%J}Rr=}KqaA%Nrrz)U9JZ3$adL?ro#eoFW(?h^t$6dpbaqb4L8TjUi_Jf-7oY;L1f_=!iJ# z*m{B{qfUS82tn+3@v}$wS>FEQQJcElqShLh-S)lY&NC1mGx%?f@FlmgHVN4aMS$5T z94Z~=E(=8vwYpUyl!fy1=uEx!O#GxH80*oY8n#I zSy1~p%xOFYi9#-f&r^)PXWVSKxTBRCC05sXGpeCYrIvcU7-A$H5jrm^&r3Y?_|cL( z*MDAGF@lMxAe|DbnLY8116`I9Wt)|Oi9@i*_sp6SI0H;LQT$eYhKi2de=6r@A_ zT6xRHIWX&h>gw~&+3RjQozhzFaQYOYr2MC;{2^aBNr2~+2f*D=l^`z=O@^RRx21s` z3-(r;<_+cx-uqX}jacfLE&fXAOcL+prM!^yizhnWfL6AlrhD2uBka&OQ$L$#-`v8H z$gooGx1UOP$8vh(>7_=ExAnRK%Y&sf%;v{e-x(XwR}5vA2Iur#kI+K!?2-}P{Hs+n z180j11P%*7#wq?n-}k?7&}!n9wdwAJQy4snnf_FgYm|$sFC4mc zKV&wpvbGCp{Vdi2Gnvr+P+}8b&)?eqv|* z%G_U4q_>nm zV(u@ab@Q{G(d+~Z#Rcyts1+wr*yi*3a6FZ=Y}Ds;-zXO8_!zQ29V3^;!U4XQ@o9@xQSKond`whOX9hT zNxF>8MuTmV_-79og-vyvdyr|02A116w6^Q)bQ;p=Z`eyK?6xe8@a>*f1^p&hif7lu zhqP(41JX+mCuv*OKYg0zoFB1Y z%36;}a!(~gPP!x#jM?y8J+sHE=X)TE&?2S#y%AYC(Y)-48IK;1vNPY$p8cFWFhttc zWAT0cjSLHcQC`T?_|Jh3S-+|0;M#oQ3_F#xLcR`-1#f3F5nG>;-~fKhDNtxskP|_K z(Bx1p)l9EtpzuXFwvg0NAlbX8_sG8NMM8G(z||cUPjRDud7z|*A(Ni#VOcWGF}>kcbW#Pz7I7LB zI#9VQb1MjHj3C70X|nIDupJG5V1mX!87EKYT;f?44*87OtQcTr=YFY*Jf||_He`@b4b@ZZrUZ(pCzXPxXKlocc)}+7LMiG)OUAk2C{(Y!!TzA{FV*2$=b~REBGFuUsBIu^%3jjd2 zwd>Wf*0gQ2`)>Pc^TUpVug9HnKs&!?!!0(2U>gLm##d%(X>tcDn$j#N4`TD8#O#EW z$aYb*XY*}!3Ay2xc>`-hwAJwp+c27IC)1&1WcbKqnZ?XSPnR$zh<5hpKT(+t6EI@; zc6`#FMY1aMLVDG8|6Nnp^ml zMAO#~>sQ_`4!CfB&AyP;d0XFnvwzurtEzY)`SrPZmiT6wB}+eEQa&_hUW}i;ISs>9RHdD_78w-o|t!6yaxB1R(aB#M!TB912ruy1fcEom*q5 zZtcVC#RHiMG2ah&gOt>SiB|{r?$b_s^d>35R7%f2I9nLAhcFWYr4p1&2v`>gXXYBS zvD;Z%4B7Bx6LGbCH9XPf5!fX5`izz)9hM2CRWCUDaszUbzae_C*TanL_70$FOe(tj z&C>#>k`n@#JFkiuk@{`|$Lz!9-InTS@u6X#Ivg#L8@x$PP|C{6 z($11fE+cme=iHjrb+O~Jjd&kr5h;Y7OsZa3J)swbYBmiJnz+jH9jMcb516-EOA}$< z*sN0`PGrP3vJ6wipBwcC1xpwKjaJ&T-GT(xLBnc)20c30nbe-Ej$1~8gG zl`uo3nFw_R33RVfvcgjobj{8|7$wlhdj#d}^^>}FagjYJ#uX(r(NgVI(V#}c0xafT zkIQb~Jra|IY1^AoH%-Z@yJsy6rg-$bS7z-Y6)ywJsPxyQ+|o%rn1W;UUG1-v&Loy)S<8+8&D^ zr*lHQjL+}aII*ME_*(t)x&)YNMg3m55@C_1^7&{lyBo`8G6i z%d>l-JsS1V1)&3n^QmD-VW{?n271sXWozwkEt`>G;et7qU^x*zh{VI`@gns1HXw_c z?h;LJZ2vGoUg}IUn>oMOuNxU~XB7U5O3R7{9r775y^Xh3!vY#mtPnfmHPY9$$NjuQzSWO7$F`7J7p^NGHPDkFSL+BE-yi+->{RxjCk8BsGE4e=D86QGJap9^ht1p9e;|oI zgT6$268)BMBg?3}ayQawgBLhj7rnfLcR3bji23)SBCUQL~RWbU#!KB{Djf@m@H^_5*Pn6oUrf<>b@ zrYF;^`Q@LRXjf_qYHllIH|*o-_!Q5AkeH;wo zs;gSl&^lB*7cJ5zt10QuCtSP_CJ)n zi>ZD#8^4BG=$jm^iCLTb#mZA>Ye>!+tGBI}iu`Vt?wyo7j493{!fxW+)erh5L|i8n zSs|x!vG49ExrqShatSC)GcqKV`iPS=N{szl5rwERPTT&-o`}%B7)zy_-TvEr{nMPD zMu&jaFFJY#7VjbD&i=nVJJLVx*cO6Z?;SdZs}Y7tpdxF*y*A4qpOmzVIPp57th?e8 z_M%o0UmMzjkr$PSkrgAii9FO(z7AiUeMqt;7Iw&r(3)%4#c9Zn#^p}u^T%x7WOBdE zcACTkj{@5PYh;OW@C@I(h}d|Ps8|lezwe!jMZtFn&6H$5x=+v_(ErGGJkkS)?vQ@SO2y3i6 zaR0*egfrKedP3W=o|?CI=3?$DssX8!=U(!~M}2ZSl2tM>8sPPnYM1?#MeG6yE^k;p>J= ziUNZtKBf0;{hD~<^4(j<{&}SGBLWtt7pbp8)JVc2PK9{_5^AO*&8PLpisNSD9@yzg zmSNfTsuJ55_^wX5DU}u|(reRn=D;wnUv6G9g6Kw(14?#R0#Q`}}MsdXlI^-mojrWL;G(S)Am@uKIu6e9hac6pLx3wp) z{QJbzpsbeQT~*FQGSrf$N)K!p>Yz-T3(e~(3s`M%mDXvn+67BWX%C=fr3F}b=v)q| zRCG=DFZHo6E5V{X0QJ7$ zSXcP&2*<87m1#OY?{M4&!^%0f#e*s)}j%je3G8Sp|*Smt^l8_wg z>+I5h=MN(LA#OZ^owQB96aEI;9d$;xT^Ljop7pZmoeq`gQwO8IBEM5w- z8hmECDwZ{(`wFK#TD&9L_m?2Bmz@x5OZk~!USu{eJYLncjbE-G{uOYI1YHG0{==pn zTzc_^o^pa=vr`qMn{?w)@D6B+cjy_A0d5&|C} zOKQIN0Hf2xtL}rcj+?6Wp-1;phDLMa;a#ql2@}COG97r+(O9;0$uRg6$z`9g0<@qR z@yNT5Z>14sp;Ek3MAY%S5C802RZ~}2`~0ezS@BfywmXsO58mJL z|9Jbuz5f3D*N_54v1T2v@$Z7)v1&esbdRtE`+rg93S}8sSw8I;obVV9k$_n+bZIUP zcG~>+rL|HUw|P|qTkJY1A#Cu{h*P)g5dOurjMIHN(A_kY7h+QxKOe>Z3@BZUB?GdY z4rDlI=m*T?G2njMloj}9MmuYbjn@bC<=Uo}4==@UkfS_wK;s6OMRRxytFil6}0R@e=EMvze^mIv@GL*?%qn{CaD}8Abn) z`B1}|r&I6t{jI5Smi^@wJpY{?JiiRfmTy$7EQ6BI)NVDEqCxkS8hc<-)QCQOQ=d&p zF0IN>*Ei9E`qMr{|HwC$qg_Us?ap#)`?AbRos<2%RdZHr|5=1b|6kb1sA|GLMsE8z z+Hd~z`|pk?H}GQ0vncn!|H<%gG55tzZs+bsR)w0xmY~>}F+yaXpa)Cm=>eTZ=P>&m z#VM~#F4(a`JA-$O0Xr>BcV4S9aV3HTI09d`XPUlcS*;maw3So-Ah?L*7e+hRvsDEb zBcdNrrc%QWO5=th*xxTtvwiaY=O1q$apsow1ok;3=9N@OPp#Scw9MgR<<-@Po{s(- zTe&oB+xa-;#WC)4!1CN6lFO9Y2$gk>4LPS1dYMir!Po@egJ}XNh-t+&&@5>j z>->*Z-C^l$O-kG205@&I)*!{X@X-ZvU9}IHkh4Xs;)eBv@82?n6m@V`KH_F zkvsU*(3?Hm1fCacn{F4>cX$Te8LuF&R5>B7)EAKjU_nL~%o^6>b zo9;mmhtFqjJn^5KXZrth>!6#%szoY936cY+@(e`2-|1%X^D*A)ZLUmZ zd`BxggGCTeKf_`n{}7ucSG>p6#FW_3!2mvwVca{jhixt=ucrkK4V>-0Om`I3yjK1c zU~Q`ubWarZ%uVzt#&0wh7iga{lR6J7ds2m;dq%Yu79jG1pSRKd3d^%SpyzQ`ll7Yt_C#8*f*)Qn ze>x>5Ym{3RrZ7et{zNw5-vI;>E|H^dP4#=E%W6>eFwT`*&{(4I=rAYV)odE!3|30^ zi^Gzw;!a+?I#c7K^^QhpoR70#Mo_-o*iCom)(Y6YkbrU;h0zKWaNU7rs?s^?wcR@Zg7`a}5rQ~8|i!xrq7B`BYQ!B@;Y()z+>Wq?qXL+Y0|X0F0w_54|@6>ehv z$W~}=8G2(ZXH>E8y(hMNUpHObydc^40xY1jy zbCSNN*ncjy-lALK@{o3G!%R`ILsbP7i3Qxm$*|-_bfWTp$D)H;3nM2DZ9>$WGN6aA z;K=R4&;2j1k4Qv_pXVpqX^fG}jc+l`Xi2n-MxSJ8q_ts8I>Cp!of6-mC4raARl zzXe@B`5|b^^g=4OmV)h7$JH+tKE^xAgtN<3vl;C~Rpi<^%_Dc})m<0Gs}?L%LG3?s z%i0BwxO$!xX#huSe&a%_nHtSlLnH6UMx-sggQ)fc(SzBfK}v&xid0qt<()3wrl%J1 z@r^n+mnOUI`-EnE95l6RHx|5mR&EU}=ZVbCQvLe7QFhgf@F+4WCS$v-l!iQ6>Nge7 zMt$yAGj0V7Gyj6GpvuoPc=bz^&+zZ?UW%q!{VFd;2THd@!Ml7DR`V|hi}6=${K_z_ z&le-BN72U}W7cNukdXpv;0mMKz#~%gngM?4%~z2trno87sV}-f!sWCw9%(3{pzdrz z2);6PSSaum%}Km2iUJWac~TmV&QVIS5>bY+6PhT62IJ0m*pNC2WQA9BnAhkpZat1> zv|pcU(w+LNFCQkNaxyqmf2Gb}d{a=Mf~;a~1X?)s*FH76-1Odq1^dW1(A}}3JdE6= z1I|Ujm_&l&hPHwAZJ_nm(zi5bV>R!vzirEd{QlP(jSZ3hz~}j5F1V8COMT*TcA#}0 zl2g?^u#tiPaLmROA~nQj{0GHF<$XmsxYZc4V@J-!e`c{o@&#KVY zxI2cCA%!%yXVvpKp>ww=_WaE)Ay0(w5D$Nun-$d5`9|v(;h_i`QmI(O#8+y`_aO}j zXUT#*Ss(;@mSCNT)#4IAw4`ebQ{^jE zF27XM4F)E8l}>;4m=8Mi3BQUU;zJ3AtFMETd!S=0`~?W!q5DhqM}(a_Djm<KEZ{w+$(VV1o8!Uf zRo$7N-}H@1w#DWl6>%p!**z8*5NOmuM)5?wInn_A&Kqb9tFl+I>iab5zA1P_+kF@#pk=4KY7HL;=q9%frHeAG zevy*mW?5^-U3E47?ih0opU2rxsctEh6v!;j!#00cl> z$qf(+jAc<$%A^#{lK}~$Npi~VIacPoJ*JTM#TCmzkw~#yqp2E?LYS#q`hLD5D{qJf zC~uQaCFMewdGfkLcl45A9wmyhuX~m_bB{et!Zp4?TFpd)*#pM-O5!l-?SQPl9(8OX ztzdM3{@Dzem3Su)Pp?q?AXRHb*3(TvKD|BZFRfdNvc zQ8ycx9Rg8%^=P#+w&HHFpTRC%3H_k=r*O?KSOp)v56nv&xcydQx5=#GjrC^Z!ZOE1 z25Ahp#JohUR~d#>566}-Xk6%)d){@LJU!SWv z{t>f%>B=zR1BwU1YQhj0D$zWo%ZBWaD7FlIQ@JxC2Lv+uH_#VBsN8rV8Wawue(H3` zZiVSENkEo8zbjtQxLt`EH(5WHT3zF}dauR2axhARa1GnE@Ka?c-RpO*=HutRmqBY+ z$m^o^=>~DaeDmK@UDLv6zfkm$2H08wf%|3KU@pfW2^Me9Dtf~7sGc?VFB{g@#eJML&GJkt@G~lp{`khd>$3{svwb~$-&IJ% z`AJgnE>9TQkUI=oWb`&?pM8tj22B?04*A|__YKz)-5l>bnR&6U=|HRbt~*~bvj-*kkj1T zB%2Sb1fRtqYUJ|m58mH=HAJ0WK8Sd~)+oIE?4ap~wzi(zEk(cCbfw8OVqlnAQ0i=s>1gM`@%Ew^16aHH$=hrwN$j{%4(Z z$6WAvy9Zj`5jQ4$o{r9WMD+xRMBg&mghAfD>0_b4$DHRyh=Me6whHRj$xea!y>i{ zjeG#nq8atiMG{}9KzoZQ-Fb6v zH~*Js9OjuWi)5qe(AU5rw)}6Sm;2+w@JJ;&2^szy`aSJ}BkN?dr)k^EkbP>cNEqk$E-O7!jUoA#M`?_>oyXyOu zS;2TPH}#nq@|hs2B8p5UVpe(h%0YBQ_9ymQMAF1tx$EtI#3I^T(`NA)>0XgpvT-7V zh_xQ%2MN8pIDtz}-=3U0a@gldgZ^ynY({SO?J3ky3d}N6{SxcyPn&_L`J|cN} zS=|GRWKxr5bR%w*g7Z$soIZTnxSPyQgWb3Tlnr4D3$!@z5)|w2%B;aF&qd}2cAz@o z)Xo&Bi3k$)5q1JmXXd$$m8cixH&7A{)=Q_dM0Ey-k|jWPBWP*po0ByB5zC5yi>^k5 z=^u&ISm)epq31Fi1k(oJ{r~=bTkwJ0L&le)JRy^cE;95B1JCd;b(T zeXHm(Lak-C?1!6Sb8<_15M(-0o=t=>?U-Czyx5Hw zyq31tf!mi;Qc+vSJW}3ck#u!>sHg6}y_}wLzVPO&#KLR4G|NgiC?`M?9Ob*s>iBNQ zop9qa z#9FH`Ex_EIx@(bjkDF!B!?e1NmX4?4S-@U&OsJH%Q-~DHLZ+^W;UwP1V}TTbqNQ zjtUOo>Q&|Z6!nrP?ixql%m!{RiIYWACZmDWil}H$W4{p<+IskHksRKGNK>dtrHnx> zvuqlNaJ30JN5Hl_^2pKTa)2fjSK3H2a%1yU5!`hATk~Tx8Y^_@7Nj; zH*S47{7prLI zKNSQs4_4OQ>FfI})$T0LPVw6hs=PY?Pi*2Q{9(V~0%*I#JlBe^a&!D`=1%n3ZFIVs zve9OTc!lDGh(s7i+^X~9CvBXDnFd&ysF)l1(B(->b#ie$dwfWvR;tO&9=FoXfr;rE ziR-|=I)3f`kUXd2aq`2NMT$$XA#$MAz#Pz#X?JsBe=m(IX>)Y#v*cwp+wc z;tA;cSD>FxO0Q%;#nRn6v)#{i&Dc7)tZ(qm)=NSfq9SBg;afX*35Kq6nWSVE8q}`P zni!sIMhxgbc5{7#Z?bekg=)0uiNjsEG2OUxo79-c*7px6dFNkmgyymh`v;x!0)Fvvn(xtkPj(oact1lX*tFQ#Lw`3Z-EV2 z?EZpbtHcF;{5U}5a9tXaw~Q|eESSjLhC0-sBl|00K#8)^f2}gbNiXW0lNKKP#G`I7 zd2(4~h&c?mxJrK|qmDvsj~rilLJDqJTJoRpb}q*lNw8TW<%(P+8Y9yLg;q`1WuP1p zMYqa^2ZLK9-elzZu|TXMvcX_uSrxY$9sDS)z{1bOeL2hz^{hMDta-QKWeyg92!J+C zbJ*CiK+|Nsb&?H~DQ}Z~%q7BTh8~!rXl|I zKXwGqa955QXqUN^+H0okzsY_|IXfhz1Rxiw`aix{^7V%fNWQKM(K8w09-T9X;1?FR z+~C3XG9b_X5|Q#;L<`N(af#YDrFWpN&%(2&htq9d<8a^QNr29-3YqKewvgS+M=p^0 z#kXx({V|mq zDXQ!#vNUau#UDherlj6+UOH(Bp97u)!$K0ai`YampgF$yjPoeEwFI4;wf4DwA+*-k z>ctB>svF{uKfkqt)4pyZk^syQSvONIihZ{<8W92FRx%Q&t894e*~HgAcYjugy34~}qel2;-1#SLHwTYpP)@O+ISB<+yd(vi|w|~}qy~EvX^^rAM ze=boT1EXw{<5(aXDRSf$d5pi2|AK;T`eMxFWPSI)pImBtY)nK^q)!ACR0iM>7wQ-&`O^h zp)`;;($+P)F$|w)w(nvve4tpQS=UFtTNEw=1+?^tf-|E_E#_N`QtS;OnpP~X1|Yr z{PinU`8zil5d_W2eo+2__)`RV&S*yEEe`QIaRqSYb&jF2Q~74e7umwmE8Cq+)CZ5L zJB>*$4}Czyk-5?Rxp!3WY{*=(&0YLW<@+`lT=Ny2gWjQW>(~#et?W2~ZN{Kvur2ql zubfObs4DN0#cYA8Z*%5WC|`~| z#X%KGLc)yPBuJJ#5G#;*VIX|6Lt)Z`TfyzON?`=8vdr&MLbTR2zGZCAdy90D)G}R6 zGvA}Vfn96yp}~w58SWo$)D}B^TUOvFnK}o(&1K|ETx@SRzOq%+&LpqoP?8zDu@LEN zku{BXv{X9Xm~W>|{|O5tB|%kQHl|Zi9exY3(HEISAw1o|YxQ0}_Pyu4_O%E-x(mq_ zGa${88V25YfGG?=6V5OR0_a*S2l~PLSGdFm$`jRJmBl`=DBiV>@XO1p zn1(B`*aubTNLBU!%!XoX;p2-IC{LB~5?}cRx*^OAUu=&C5?`xO zl>*vx?G0WbJn&7A@@&BgQet$i8<^9v1Oi#&cNMLGUJ`V9fw|);y9Wz(<5I)@^NrF6 z{e@KgjD$QK1DfxVH3igzu;(N2PSm2e$xe&K&L9b-^<|k)-;oXNxDj`_R6F z8+fv@)|R(lsV*!cLBi`$0sVZL`sjV{FXM6 zIw7%)#%!=t;by*3_wsG-d#14e!@r`E;CqohI2zukYato@AY;Pai@yFZ(gr|V@c)Li zDIWuc?Y%Lsu>-!S52Fu+4oQu{f@yKMgo4f^H>_C_l9&)JaEZ3l!Tzcxt_e*h4g1h9 zRrV^J#DiPS@9+Q{A;c1@`z%v^IC?$A$$S`HYz__(q zl8welQMe084!m=#CPiB?lOZ&6GK^cSIge9J>@q}NryPMvALg~U$?7)mvG-WDEC~s` zYFeD{1EnR>0FA()#V_>;*cKA6>c~Ggj&MuZlScOw&K_u8250VtxdRNsDj+Mh(u9P9~Q_HXqG^lqa(d6&@R)7g|3&3icYb_ z{^m9BQRQ5MxfzxH$9!fOVdsj-nuUe0%WYK$dfZPb&>Hk(47@FTydK(;&}(8 z_GRjzSMFu|UE!zQ8>s75{%g6z;sdXIja&yKk@TYEd`u}hEvG!2;nWzs@2 z62ZSR{LcX)H8p~fEL|*j<5qM9K2E_SO?SSUK2d#h@L@?s)$=>w-CqpdI)efPqQy9b zuVc)9p?g=Tsqd*Sn;mkmQR~#37GC6|b#>iztGod6N;~6G>nNNX6{wn0ZF^7l{HcGn zMMhs1@G`86vwZUQO^1i8{)jx#LdtCQXCbU_k24!)_0g|1j8u zg=?mZiUM$o0Tzo~6mA+V%A%R=_1r;ew(gqHy@jwISEObY&7wWig@vn#0oBpQxVo2t zF)hdO)}g^WhX&pC^;f|7*P?s9*Iivz@>N$aXD>|fM3eC$Zi$s-Ol~~J*r|-SQG2|= zn?(7p)u?Nw+;%K;1zO4DVy8Un+M0(-g0qoEqv12%4Oc8ZW^~rk;#xlONR}B9d?fYv zy-DwDPE2L)T(}_M@3W+biIFwfKntvjOYqys7Q5ZjTXh9M3m-C*x=m)FpvrFyu3`%N zCuai$a|sGZ_Bg(%rZ=oD(z7>cYStF!{idck8cxTbSJl>4rG50@c8>4`kh&PxS_`pl zI8f*1TgKD2UoV=l_KiWzi2}!_`tN%}i{J|!2RE{KGwvmF5>DXo6;9#)s(tg?i6LU4 z7UtR55m5a&rD8XY*;KtOag!u}!aA&|eSXTV9a)0>huQBUJD1*6Ni1`ld?~lZ+jnDc zY_}6PxS&?UTpsgA8Ld=*DMzQ{k#tw)BAC1qdNB`1QCyKIBMXOWuRR>+gN04oW$#~O zQEjR z+2f~oY)QCc{WP#^6a!D*~DHW`s@Z~oKPchk_3Z+KeqJgtDlgP z5<=ZA(g%(Am7t$)%#hZM3M$G9CJ^hK*m@;pe^;Pg=ToYyc9#mH$nE`{LdS`hxdn7e z*XrRb7d@nT?(3sF6$=^d-o{i@ou9i&VaXoS%hTqsg>nlwaq1WV7G7~0I~CV^D~WQ9!z zR@ga#Mhg3BGqU8Ze`Unm&RL(wp5E=N1b=qTuDf(sU5{m=R1b#C;T{V;s&R7u%c%w4 z7-0F&ytDHItlV@2aao9*h_{EQ^7;G_+vdR!9|l`_FO93KpJyK4UR9OpnC74H{x*<5 zmFnnz*+peK_fnv-M+b5VisLWC@ssi_EcHaZ8zR+CLidXtui@+ct?{DZ%N1AVb2)i5 z!p|MMX1yHL&7BSf2WMq|H2Ke&wWkyJT_Z^0-i+qb!kg^0;XeR2Hyk!^U+3r19v^1H z8c3DpNVO_h#bc?}2PLiX3W_wZg<*=O)GW=-T3Y*g6fU^p%4`_9um1ZtP6`KvU?@lX zLi?veC*+=O0wO35gq0=yuh`ACzG*RKi1{xTWGs-YPWvNjxNmzDk6)w`tc0{DRm}EAiTuE9>6E<3ZEHhyGggqJ8v5dIjon%5ylD~X-eOq+~ z%TNUBJ|i+U7;R0~6Nfjryp56BW3~WlW*?^z6#FnwxdnX#P+XE%gaukoJM_9aNPTZ8 zbRJU$=Xz$6DS7_IH0V;|3%mM9Ts?r&hYKmFC%Yf;Ua$Wg& z+BE3R2j4t-R!H*^xi!kYbP+A)a{uWN|0meM$>&nDf-FB*U9I|pRS^fP+wzs%4JfO%PQNxC|_Q&5O`36RW>Q z)Y;cF4eQH0&0i0Ce0rj)f zfFkiAY&eqqBreD_byjx|UOAvxgd*|2(jUjd+eDa+{Z;8YmmZ1v4$kdG-PwIXjlJQ90~3`9ZWtu~cpgiI1%lYWRY0T_;R1qi;lg zMXfRQI@qxh3OBl|PwvU$)i3WXw>9wcmI)eLhNe&6X=4X%E-jDq&)n;G+BTpPpIw}^ z1gWir9+pssD0Aq$^FXNN4@?IQADN2@jk-cu?{BacQ-=f^-in@iU6KURoHMs~@HCS#9M8f9} z32*|32S}P&Oyk002ERpiAC+3h7GmKitALmZ91HuriE6K@+fkM=Vj+{OZF5@W6{P13 zIe4Nuq?;6vPB80L9yX-f`u^NoKIn5S?bES(PF3ohLA|q*1NtrP*IB5Gu)nO8TmDJ1 zsL1)DVh@H~3|=enzY;g?4hWjXGRsUoG(4_weXD*;B_dXr>c?fQ?-~#saB>OzEtslW zO8P8>e5D+3V9oe*Ieg_86ryA1X%)opG_Go3h<-|->jO=y(ax+RjHOOvHwwUqq&`yPUU;!q__QJ5A@UnUiODzAy&A-)6q8}S8H z0=iLdGB*fG)ilG_p)HuY?+95HYZnFOUGyIj#*){Xh86^G^s{y?=R1km>#o)ldpH>@ zzp3n#E;$phy8rCk@w-kMy|IHi@4t6hacY*_#9q+HTsJpDPM)1kWNkps2fzI!<~A|4 zoT(vVB!RpZ(uC+tYalQqQ@cXt&f8x_MK&|OLml$j)OFx&u5Ep>usT-B8yJ(hruA7E51-WgkSS{q$ZGTGyB2)p1A!iGBMBji&UMzyV z7+TlC@odxlbh0b7Xy3@;s+v2EREd>K&7Hx#vdTMlsUytEBKh}>?h0JL9FPWI!ZmKW zDf`Cuo**0JKxykR$gT;KEODKxzO`Mp9*8Cn1m`BCqjv*tzdh6XYjAZQIxW_`2f+bpfLmzuOxvj!^*?lnk>YkUS~aA`F5pB~)Gwslo>tz$ z4QU6{TeVZQe`ptdUvJV^j;#8DpZ#n}lXXSg*;MmzZVpF{? z?4>Zgv0h(FjORF$&qA@5#rOw$jpiA_`}=0ntAb~Wd>ng2VgJSV&COrR&!u=afL^fn zsfO^2;i^`a-Hzsd6SIJI+o%mAi9-Y_fhSC~TLq(zBo(aYNOcF@eHa)K;fJIkOXiX# zbU0tvS-W3U6ak>!!M^f>e-7WAIM!2Ge0j>y$SAf)e2x6#6}NK$UzG^HRhP+^f(K%n zQlS*QXanQV^Z<!)$; zt5Qv7=T zQLav)%?=qQv6WZtozKR~9MJl#;psKBTSKPdyiDCLl8w`Ec>$91MzTcT{2~9M-_6sJ znS>=66ofG;ialceCAd089{6seTD$Dg0G8gw)lfK!Ud{&oICm_*$$*$X{D~*osULnI z<@54Wr#nCNnrFBqH^SQ&2d@rbm64kKLfh1N^B1Se3Ild;Pcx~$xZTwh!Q_o6geI53u9r%#7X^vfO*dBP##QiBdp07xgBNPBQpoFNp*wdIw}nP>{&1{ z@%q8Vs+t;5jF~dNb2#_*`zsd~;a6CwuW>vbsP_%-Vazpe{XE(r!Pz7Kp6*sD)vWQ& z^?o-hSU>-JX^h~e@5TX$n zketSL7^I3bmUyCld13Nj8QnuhoNkVCr`)mrIhZfuKhSDg3<&(;@}6@QRp|$%lpA4# z>zeI!dwr4%!-SetYuC!0_ORRl?bO{!nPUT?KVBA~d=M%LcZA&Emvx<6_VKnjX&}%E zdI=4L8}*B@6M3|x6Q$RSF3%zwS6W{%h#jUJ>VT6kHIiC@6&pt{^Sf}{(8%R8tjN-Z z7dU}5sMqV@Z|kk0CG7xpltN2>RpEn?0*R1`Y*L)9S7wlXiHQXw!fK4_!H;{6Ug0+j zb?*??dRtwycwOggkz{JvhW6bP2cA0H9he*FJ>c;RCE%|+i;Ecb_^2^}hyVskv zrH4(PvgXR!qDUFc^~A&=!$XkH1`uN^aCl7gIc%Nmdua+=?8#gx%N}q*4dNnYDbk1( z)=M1{pJKmKhG>@$@0!W9JMyPcm6W-wZpm9qbvyB5!@o z&)?1dkndmF*fG@Kye!XW*9&5qx!Y%7`B9`{7;-#f|MUo}DL5Jt5E@wmwUK*yJl5*7 znD?~Um*?pcFTjha2b!VhZ&C2tmUP;w&P49u{3B$PPF6MD@2g5vRtTRf z6A|EpzEzjCDRb=f7;Qwfh+$1%38sp5m%1AbRHaC>lgkokk%n8VpiPpSE^=Aw5(4+W zHZAFRxBje)PJo+#j$gQ2$`Af`#@qdK+rl;4l#l*dq&(c2+Ay&6IzlIdw1pL{V&FHG zE_(n+0Kao0ToWpU$JO4gce|(9f=vKlG8*lj{sUjExB%#JO~d@9=OLnGr)Fe1V++Vl zxW$zNfzthqe_GL}cy^d@S-^Z>s(-PEe?%WM|LoZAK}Te1Tv4vKQ&7sBtkmhFa6J;p zdfPsXBo!m{MbbQ3mGXZ5YTD;lU9HD!L0N#vL{`==5>Gg399tBUJM<3pBr{FA!tR?| zs9!MK6?C!mP|)z!GX{s=jH}FF;r_c0C%A&RajpVzyfT%ASf6}PB&ypXTWFlr)wpC@z_dVpG~8v7fbfNlB?bKBwj^HvIzTrxGb;+!hA zvZc9_Jg8@XIdcztT4$K>f_t6vOgPy)j6@UFC9ooK+(po~(4FdbVKt>;p88985pJ^h z1VE*jS~Qkt)y?ICj~hRacmV1*TQervwWX@2hjira+ebVVwa#Bs4OTDFMaU>mENo;a z53Mc(N~I6<#;23&7xx4O7s3PdK#+zNZxba_pJ+4tFv&3i^D^wz$=-5mL5B6`J*K3G z(X$`QpC=Dj9s1z<^P*967@J1!uoU_b`gRmyUx4V*Duax;_n;^ZzdWG~gQlwx4V;_& zZ)n6sSq!9;@g2sL8V&G^MN1BMAMi9 z!(fa!AaE|4+&yk>CF5ii4EZC16n_h*EE#!f+m_0b-Pg)Vv{%6*fmRK&yTCA1=T4r0 z=K5@G*f_p=0veD(ENmNj@_{@IZ(^)a#@13Ejk5@L8|nI8bgex4qF#ko`0feHvOFwY zgkbyVTS6W>K5Y*PEVR#~*%sSa{C1w5{&sq%Ccn?!bu_07*-MtHy1;8#t>RcBKZuCd zpxt2JG`g6X`rEO^8<2nDyn?saF0yYswr%=_9{fsNLb5q1&e-3~*otT?j$rr1U$+<- zN#_!LZaJBnjel`@W|BRooo4x`Pq(>TvywUJDp~OT%B@c@!U!BPkB`_v@A;k2aABK|Kn;1hWpPTHJ zJ6I*$l|_OF`7%W~vfS)1e3wfMMVr1vYFleoE$wlL8`?@yrOt~wb=14*%#BD<@6nyY*S}ddcY}o30C^0nNJinuG4~Cw<7gqO3$$rx%$R+^ZVenzeXBNx#x)kt+HQG z*X!~#3a2jaMVORew519{S*yr?N$neG!3_dRqXwyoUyrOt=O z525jK2lEn&Pf8T~FN*wU(mS6t?V8<8ke0J&H^ct_f%{>da4mOIhQk#;o`WsI;o#YS zRR>%H)LK_?$83lz5mbS`^9Cp^ z%wCY~7p$HaIMoYW>eO4di*yk#UyiG1!6RYeqJehR@H1TptrQ1K?#lO#3QJ)iw$;^O z70LzB1e)^|?)nXZ3kASvDPZ%@P= zgsUNHD^{oDceyB3HiY)c@NX(@92LYthz9T#4@YGCzop{#OEr+!DaTR&=%`c=IFxiM}&?C{>%%4CC zE?AU`v@nymNipCX@nRFAWPNS5XvRZIf=+{MyX_yC9B5T4_E4f7-xr*wbyN}O!jYni=N7=Ymxax+*f+%8TW$LNY{ee_! z;QF$6XOE7p|H0w&p;tG7KDY+FU4#;;R0mmTu3e&^SLP|go>2E6kXj{L=ap4Q-3s)0~?lE>H%aPS`oL(|CTNN9p=0Z2({K z8(dl2t144GBa@J~I7)A}c;M^da#)80F{Iv#&zufoR`U+YMf zb}lwTC}V?gykK)S?6;xHo$-H!+Q@%`Zwe%U)zzMt>y6g~yc8S7rdt2uW*wJ@?ic@J zXUOC+^q3N>kbp`FI?!aN$q&sB5^R`JTJgLM(e25Lz68E|iC?f#UPl`4t^0Rjc15sw z@Z!C|56bHdW9MCq-tFJk2iwzevI_arY1_|}SI;}8-?-^;vNNaYRqTP{4NH`^|J}B1 z6Rb|kA7)O;N3FK0KYun!AHK^6CB1y3+ z$JAiT$rK9Dot^Kg+*KI&z=JdU=2`Kxrm25=dbipp3swXcXZd~D#eGFl*sqftfu`Cd zqmzOipu=%_k3^_oTzu6PnCGg5CDeq&5F)oh#+{|!+|HF;1M=`~E_#sL&j#K5VQxaT zV}o!txYWBe5tDRpWmqv8JF!uB_ldNM_=mP57^P8GZJGRGG%e_E*5Uks@a5>w{M5>{ z*nj7C-}_-daR6BjB-w8&ZzDxIe}u8)DcWS@GP~bJ6yHSDc)B;Xm@`>TN{bVEDeScE#4WO<;=!C_>CQ)ONy~^E~Mw3rzcW&0*WG-^?FtUpZO!_=VbP@!QD=`XLr z$BQGc8C_v_fCm8>XjsYNyElA~_>C?M)e*!c5(DQCo9^*5&ho#YBS0Iohn$EEYOl( z%7GmO`ztyOMo0161-Ot{a_N83_3iOc<^TV>NF^o-Ma*`ia+$5j<}y=jBUw$CjS#bC z#h93FBNJmzkz118+Su4kg;b154COX4E?c(TLWsH8O+wCe87Fh*^n3aIzTd~=_j^3P z-#;cjx|})ZecqSX<#~a6hHsUBv{xD``g(rzwEMMuGw;yR56Z!E0&c=-~SFr{YHj{FP((bNuaCL7Y}&JrX~&5G+w9a z>GBd@`xh;9sy_Erm&^7zHjq#bVa`g&28LnOq|W}DbEN`%&2e>u|8P}A;qXv^^^w5= zhS&TLI|TM8D}EFq>tOwX^#$e#=0E=`LshNVIc3#|3BG)5gZO@CQ~RYoQezi3n%IPx zfH&p|PHr>Ly03NzVTo1cLxs!*6nwItN?M6EqMk?xqcYE0SIUJGE*{kE^)+6niVmMe za@l8Lk)1(9H5oi}uu`hKj?_mWi^yd{+zb-b`7{I>h>>fzr-)^DhUaU_K3yj_#aOI| z=7H* zxl%Z`l~@Q#+x%SkOD@8DY+-^4l>w@4np+i&L6au_vgmCnDXpFs#&br=85MOV-*&F; z5PhZ^y>x8U1pI&|CH2(jQ?DuFPaJ8p6gv&SaSsudU6M|oCI8c{-hn*fVEqtzo>F)a zxBZ{RL$Wo$4$?))F&do36En1*_!@?kG_KJWGsW0eBv(ZSZ;(L+y@jTYbFpsJ`a3LL zVJ$RrgqUE_8_)r_O0IP%4j+sZ^>jMW4A;$cbc^(kc@6CuZh3bGGSJHthb8vEHkesV ztKbbM!XbN1{BlHBD_dPhzPo98t$}II9p)Dm>G2EJZ%~ZJQ5@#9X9| zq7NGGhHvF~`IM(0?zVqqb*VAJU)L49kwn6m>&27lrisF3(AIDI*OjYZ|D`y}`*nga#HRJ7gL#V|jIVumF0mor_n995FsJwx7c~^hIk+ zCYF><=wAP#<$e^xMa3TCDGgV@nfEapG^>=p7i?*=8$e#PO0ea63IjNlAWen{F=FK* zORIVv7IG;9PJSc9jHl1EFj86yZg~7$?UOI$gnBALNwA$XZjyB++;kgXAvN@4_8IpM zsEW<|-hWneu;OM52TdiyLY8sbnqsHZxw{ax2OfG!%}{PDy91K;6R}7WWht}LVCZeN zpn2%i)5BxEr?ZS%_U|fteJWMR{qqS6^BGgqgkCI+)Cyt{7^0b%Xk;WjbI{Qp#1>o+ zHZvAeXrbT`jjz0~^i3qzaq; zv^je(8k1Baa?UMglg2jtpM;@CGBJm809)r1u)&tkf*`{{aMf6Iz1{#g`~q2KdXNp4 zR?PLqHY>LTv(fIR0s2MTTFNbsvmB0H11x2KANNp9fh52G)lh5Uo17mUO`LA$-6pbR|ON&*5@NO^gI;3-frTenY4V5-ey20#UV2|+orLAHeO6?(;IGI?7{>x9o zPbz7Oy@^VSr_#5HZH%!Ssg@m|uGO!7%QNo7sg#OL11x-2#OHN>p+6>pUU!}2*|W4~ zm&Ek|V^$P3sbM#yslMxtgm?LV)E5#0t}Z&&lp|Qo#PM0 zul4GI27UViKUU6p-bzS6H^vfb2(?=2Mx0wC`(bF)Fw2pq*pI}Lt`oC>p5@FSgIwPi z>wv7Jh~SNkSX?c|(&-&R@`_^D7|`F@E}wMt=@Cq>6~91E*qfU^f3IN0+dch;rXyAu zGcdat&B|mxWPaA_4S+D15>6BmdT_=kd_(krMk_@dYAVYRRvTL%)JDmZ24~PINj1IU zx>+DPn(>LYki(YmYQJPzsidXiY+3u#@nO=VI%Sgdk{!_?f3|q)2jWRrRJyf>emIbh z9Io!L|B5ueVAgB?zdj^5rvdA<3`=j|-$y3PMo2p2Gkr-e0djgjS@o;ZDhX(hbqCbn zDov8{T{pR_lzDe=v1S%Pzk_x6%?y-o2L{8Pe2R;X3^JJ0y&ZuAbiJm{e~INO_Fb>M zY(BKD`_R7jnSHmjr%hM(f}3-eBh%@cg8HT80Yg>7vZp47u<#w8;LtB z#D*N?J59FB#*rZtH|2uz{4tB|XgfrG&SQj`GB5jfxZ^e-#KIcYq-0PxaOf@D{H;<2 zU#0tY3BXqknn76uE$t0^nT&6sZ+@BAgH#W77iZ(OrKWw=kH~O)@hG7F`QfX$%t}{i0Sbk&u zD&>5{XRJ6ZHe^WnE7I^6brXmp!tAPGR4uVsr-c1y1=n_@eF^wJfLM9`&Ej2h>5_h~ z6B<96of5TS>Ybh8?hXOP!Tj5IeY?Ax>-p?c@n>9o#%%wFVqd2YZZ^Bl$6+|WgQ{sJ zwxRVbasnANX~+U?0MTevZ(R3S`*##XVSNJ6&2*9IEyfsEM?+$x!Q zOFC*-7htXXmF!fg^~LqnSBopq)+CtA5xprQS+&*!@idCy0o z0VgtUJfMcvA0aX`H+}bPZLJnz{xKr-F#1|p$Vrq8losA(4H)N#l!WCBXT(FBZH6ki z{i^v)QFu;JVTg@M4ln=cnI(=szLi(pPO(lcKhkEO}l7h}_^GiXtRbpH78>h7p1BYZ#X zIE=GY>rQ;p4f@Z;_c)h7NG#@HMIidL0y{g_n|GpehMJiFYB=2STit`}e_z%XAEAkY zIYC2r(FdT=Vx+bqTDFNF_}rAYr!pWEJ~jw8ZPXFDPfgq{&fIB*G^u`lm~@fL)?%1i z!(S^y(@t>B<&$v@F^I1Hv*XA@toVnr`+c=rOF0)t_9`#()}xh4a)eISLb4)|yVLke zu|l|<1CE1h6|Rm=)U|!0A?F5~Xtag27~3N8>vb?c&<6a|?8joAT0g5~d^0QVq>HJ3 zqSb}kgJ1z&pINxRG!OjREb2-`k6uK_f2$b5AhAmILu8MnACH?N#5&;wWD)K4;*NAe zZw%f7n~NNS9}JkH&K2}?V)7?GDG(yjbft4M%)*m$`Vvp$!rNTi7qutl$Xx97p!|1Y zV?}*!7am#)Y;kl~M*9U9w;LtAmGotAb1D<%=~G6WIJa(>lZvyXR~POSz;|0KLsRL3oFa+I40ZTbVOaK~--be=z6;9vG~g=cYt5ak^Zp6MlMIUyNZ`EFw)Qo%$#^-uX{gwE0(@ z1l>9x+j*Zi{g1>f_DF#PcRtjki9#srCaq3#@fUf)ceC)~UvCPQS%t0;H@9+?-0=h7-x z&8C;22yB%6r3o1tCcd(05(yt%uWY80`wgQdo>8uKh zQkR08fN8jd4z1Ixn%|Mn(;YhFLVTqW%qS~>-G_5lyLbpGiFVX^99pxZg+XBOKE@Kd zB94Nj`j~q2Q(t#zKkh2%6WAB4C$|jx#<{=v3Yd!%iicjTYx&5>XyHf|p$EH5f)($j z6!p$DB0BkGfX78pB>W{V`;{JzIUbGdMO@Qvu9uva>V$SyWW>;wAyb*3pbDy^}jE zVs`PmaVV7}LQzasE;X}>6p0 zbTS?Yw2-19_|N3m(3R5x9n@wM1l=d?k9#~Fw5|_%$2GaZYjGl9_()lJgk^eh*rb!Y zUomxwY#Nnm4C+59o~>6fRt*u-NB(>c#Y<^i7RuDm7chqn(7LR`lcyqXF!%c-rVhka zC|^+Ur4rRkm^SHnG^V3I#B1@VB=hXv=9yq#!D=e@LG}zPBei5y+BBn=oH9a2QpE-p zl(YfI3!n*HB*|L>g7J5ca7GWJ!Za6`fvj`|q91NdHGWYMKsFr0COsq9WZtRM` z`zkQd;}%A5G}TW55-O9qp{%2Jdp^_OB3rxkT9LoMM4iOQBcq;EFKe1 za7J5lbeaHDZH{KAu2CWvxEq7-yyW>cQ9Wz?i_#$w^5HH83n!zPQd%M5n zlWUrZO}!4}rNAGeTJ}wi_5ebXlga3LM{x*b2lk4biv=D&J)fLwiZ$-U>$tYIv{K8< z?q9UxbG~R<3NMjSW+rZIEgx+blQ|SQXkRTRI*=u>whRIXIHVEkIc27dE@>mD1{t3; zf1X*|E~BS{7EuLQ%hLQ<+eaz@3g}?XQU{WowpABiN>R{E!j=O4rFRF7ipY|KAd)(d z$b>LFUed+0xnT3_V)cHm9a?%fMm`z5uQM{5vV>FKWIRk((o-m--I2D1wH;i<>&9D? z8B1~gEBhQ?c>yDcS)EbWeD&`RvD&-u^F3IRX0V?7h1e!?BOTkopBPk5scwU464Ku% zCr>Dq^C09h6VVLbk_~ywxCeS%LhoU^e%&Hl6VR;VI-tc0O4=vg2u8DGi$k@Nlp5fH zPN#5bp@Q(nQUAV(g`mfTWokCv$vznc!STHJ(B$${KsIefEZPohTnm(&VJGEyG|kW^ zo+(X7b}Pxr(eZ9O`=W}|?AyzqP_Gw{`5IRzU2JFDceqsc892My{ARpiq6k0Wx$g2W zp4->G_jL87iilBi)%3)Zd%S*W8Dr8xZ5@_pb04)6DW!5!I?sH}!mcN(4ao@g>WFrY zY|g)5|Ds~htJu0GUAuaBey!RblGWs4PrKF0Z+B-6u-_|Qz_9cPF+11f!aIith`(a> zMXZqc7-mGt7{4NOqSZLJcqiNCSIONmC5vYA@p_Q6HP#s^SlbgN*m_|1`V4l3+W9xz z+8@aWV2uBS*XMo>Dm2=RKJLcbSY}W+P66SGbVZ>w4R;JKeruegllrdKwFI+^m^&KS zDSMbn?|EmxU8MUt>WpX>T>XR+`Okwe|JM*+Tx6h!+l?_{3n-Y7u#Nh{5wamCu{lSO zIVl2>!98sUmD|eivIdg)NiI6dnC85NsAF;jJK8H3Db9t>erp53v0T>5y-hb7LK!02 z-h1tiF58k>nr0VyqK(jl>P~_VZy34HoK7Fch_5SS7W|+F@N<41sLTc8FBK= z3tTIuufikwlUpgr@NiG$$6^pEUB=>>D%r`IDYf5c$1bS5H%`n$IXdrmp{(Yh4X9(Y zG--G%N{4Mq2a_O;;-$4_3eu8UPdThZ`X>~LZ!({Wqu?ca*Ws(zKk z6utG2?-Bib{qO%&;BP(rq80o0i`E8ye=ec71D>FPQ!_H%7_U3x2nuaPJ7aOFOGx1` zI8WKtDN|(ZrU1%34$I?oFUY75J_hNlO)O$FqC8#`Z3oi9-PxDSeh{Y1+MjA!bg}PzfTfGx`i^ zpLU`q6%T=*Ch#C9dEKLn; zlcI-pT@`$jaO#386fToG84=CIDG>rs#Ri1`Sf>iQ@+<=<^T8N~8< zVN;zrf{vJ zi{F}9tm=psF6VkSOd+0&~ zo}A#ZHevhx)54EXwoLp}EK8JD(eUMls(j2_?MJlatwNtcMjK4>dA)OL#Wwsp-&f;uQ4>$Y+1HF5M_3Q~$Y z_mX9Ir)*l z6if8zl0E8i@JcTplXpk4a&^rq^b3Cd;6Q5aWgUHKU|_|SPc8LpU>Zj9o|*y&T&>Id zoBtp*4QCW`J~Z*?Q&)}=%K*M*uk zuttC?ki+e>FrS@C@vO(O{FEr~AmXdevFV(itn_dLks9- z_)-(qKNviQ_F@>QP<*xK2H|?$Vq9^V1q!AS3hx;AO4d5%PkjAPCvOIDg!{{NCREkE zRgrg*=;GF6WquA*xem3PZEibgZU{$dgx(P>P-!Bk;RfBZ$}39i1_(bB92kb&omlzE zknZ$uCn2nr+OO~4 z#3@)z5yyw5jhT7ImobMeI^4_ULkE5G1{%9ITxLzW6N(S}YaLBKJn;M(38r`Qu{Bn> z$uzMzZloQ$rW0+b*NAk|P{`{5pPMieISi%;*IQ0cGd5kA;+E2!{cC{*1??G4iHb0N z-`13~zPXltqPy)>a17ISD5{mY!E*uWpJ8j}P^)EuTfb=x+ z!ff(4u%PZ1OQ1fD)BB(U`*Fyxg1(d)v_RYueYIP7v=+eP!6r!&A$462bC5+9F9gBW zRhJb{nKwP<$Nfwug_yHY&Te7=bR)3#6liqv^pp!SWuyyYT`K7rI^JSZ&F#eETIRvM zk}Al0oZGgB2OC5+)g2BsOdE%Q&1;7GYX-gSt=Tb58|mM6R}0_fyeldS%dyNwN?17bw zfdO@&>QsLYTXYGdA5y$;^P{GdH?o5-_Mu9^6)i^KvSGBbuNW&~!!>-&z+2?-( z3_uigfq!3^1WkT-a^Le`Y)w`?+k6nS_-s>v3`giqr zZ&p$}BUTA;Kdg$--vUGQ3~2H}>y`GjDyt^GuA9OZ@#{MV;+z%y`AlLZIuTo+e~Oo5 zl5)Zh9_D1oO{VqFHi&(?=ak*+%KKGS$zIonw^}265cGt2;*+ul8jT4XkcI%;3Em2< z(o@W&%*BtOqmQGqz2oSZe@+|mJm?$rfIsG1I-vccmu?K0l=(5?2cCNGIXL%7*@a*- zY9tKvmnMmxg-WRFyBq-hktwMz&A-W-(H8}uy3I1RtU?L`pB25Wb{E_p_Rt&V16;lL z1BfvMx*0tayx7v(Z|k8;s;ASy7oAe59RNBT z*7Z3PH<1#$0AA3x!#v>EEg%xQop>%Ke-idj$zvv_?~Yl_=>)~p(@tiuIF)hIC2y!3 z>y`8iQ`}tztgWxYyV^@d85PFId!Igk+O>Xnp69w)Y%QU;3p=Q^Y9*uojQ%QU^41zD z%#p`ZZLX_Q+9Wl?{fd|$ypbCm-S3n!6Jtt5-j_7x;&E3VaU!6J>Hw4kxyH1!z_B`G z=tUP$)L2Dt9gXv7uoqg-Zb;}fp}SW75Lk|=Qhu`<=nzgUM);97s)jSX}Ee4*CHaY1f3ta^@Prnyx*H&dL z_g=jPu@5VJa*TzWa&={lmE66`>|~w=?utGajZju`+WqgNQ%PKF`QV~YuI!-RPo7-^ z5^wMz`t9|@M-Q*{96EIR*1OB@yMG%(hSajG%)3~ethLd0UciIgy}ePiWO!|#;;FJZ zCy(A8%Uj-c0-9Tk{l4q1+UGU@rr+799~Z4cI&_Nq8e#1pqz$lJ7-dBhy0a45G?Kl* z#9!Ikz&;;4ebL3bh?8cO{atQAvX__c%IFOd1s7Q9ah}2~bkHWX$~N-;p2m$9^>2i2 zpa#g0fDC33tepeI5TXZ^>VtmD)=Za!D5(EDz(G-}eFg?Mj!N*fLw z(sg#ec6s=#Zts7Wr&+lF@FVH{gcv4$%ItPN-Pf0$ufniLKT+&T?UJr1^JU-IL})h& zDg7mK_QIzf0}bM*RcQsk<);~Vl?C=_oMBTdfoJmjqBYp3l4u@K*YlpzL@~`Z?U^79 z2oiLB@B+d8MyzcSJ86rBUN6_6l;gNFp1ixJBT8fWd~|1DM6FMsNoAGlig!d@QL}@f zQP9-c_gVF7@JRoWSAN3*?aUY^WcxlM0{0=|ciaoBE8;jdnIO8jnR*L%CyKDs@EBvhBfi#&b^#AmNRy`t zGjy`|PD8{Otx5-$`8?m+s{3gL4+8==+F9m@={|MqzH~_kxWdQuN!OiLOJA zpFE{^y*w~r?6h&2-&n_1ynhyJ*zlJa^<~T@x)=+%ZeW)sZ04_WS(WF~5hXOlZLljv z*fE94?+n3r$i&w%^a)G+7yvP9wMT4?J$0_!4YRO9 zhXvsOP@Pn8xv|kxVEMJwKghq~J#B(Nd)1cA0bdy?P9|9_QR-yk@d|z#p(pBa*(lGT zM$iv~TvVUU)VpD`Fpnn@!ADuqps{(S!Y9kvxXQ8WN&~{0U;dTO49Ls(A@Ib52UUjH zIki2no&3PJ=?rzTn^O02byf3}3GRUoT~F$}6umNPYFYU3();K|LzYi$-nC%)3$HT{ z0aDAypaAOnpswlX{xviuY8tq;7(3qi`9D37lw^r5) zClinH(LbT1HHA2R-PiuIRy-jd@=)4F-6vw2`L<_#6!`| zb%F*4?*n1D9y^UhZs*yP4Edx9qDxJ|H}x`1<8vW48CdJFecLga*-7Gz{T$*7TsWMJ z8+EI?H8n_WwEBBqJ0cP`F{ut7=y-#vV{`jLe7AD_^(5gX&_JgfjMXm_qZv&Dwt2aQ zQVyq}>}^(1^jR>Y47lo*cC6eV8TCXch-v*Z47xN5-OhH^J>Fkq$k_404~+zn4gKKP<_OQgk} z17`wMvVI5th8Jz(G3`XACX|udLwwakNNL%7_s%mx{%=J-HnSlU__yLPep-+*=8QVZ zr!Md|a_P##X%&*}Vk8^d6wf%)6KU+27vsfoZ;P|G4*R$Eg~#uDlR&wo?(iVCuRS!FaN*ZUx8N9FX9ZLOcu*Y$Hcb4aX|84K5Xa4`%mP|}W$Pu65 zY-GzB=?UYk&Sx>F*7Bes(z zTZfrHKT}0~4;tGix>f_-S+CX=F|VyI>hF(cr1C7wQksT0J~;Vaa!KQ zXaCR{JxOuDvbUquMzrx*WlbJycl09Uf%F`$++#uwT8-{~kHzJE4Z;+f4%e0ya`dR? znvA@7j7nyW$8RB*zpND9@f)ytlhq8uZp=LU*NI;2&3zceC(S_%NJ7aqeMxZYC*vpRVoBKs_88-~x6R8~3W)V&km81xsWfmxtU zg!dqJYtT3A@G=&ss4C!YKa4V{_Pw7y!6Zn7h%ch!hW^qc05QBSLIxPBJ>^c7`S&<# zM3S?XzY1T|S#tVFVQaHkcT90SP<`D*VLfUWEU=r&)AyCqSvM8xkC#?>fC3)tU?#QIi@th46-`E~BHIXouzj8_0 z(UoK0?P7aps>8mjvNLygcgb1Wum1nlnq9o%R(50LyzL15=InnyU$n@i3_kixD{f3u zw9cwn??PKAL!pY3Fl+{86FZUXJULwwPiyStSUry2e0cI>yZ#$*AQv6#wY&MCJlOzL zV|=Ggs&C)ui~arecDsw4L8~VoNuV^-lK2Mo3-Mjb>_&Pz^mcS4?P&-}G(NQiL)sg7 z8@pe3eUg$`IZ=6m&#rzdcx>?7gSdb{`~#fT+XYi4k#;9aBSwFnHv@}10s1}-kNM~i zH1P1*Lkkfmip`O*XNW2h^Mg!A5tCG}g9#hN%TCyn%Eq=KOC2U1zfW(WuBt`fW#lss zLvJa2jkSR4cl=>ljfge51yS|L>Y1>{_nU$k9u50S2t^U`am)``ffTlf3d(N@zrf8?U0g@n`>*)kNBK?d8DwNttt+~Pl4>~68ehVtb&4|Q zz#-jIsGu+QA6hrSR9aKcDmqIdqn%kbtc9Nf(y_^cn##)7kB1mf$hd{??Ag~u$1HUk zH8GFt^=2zjd?;LtG8609aCg^rRD2dK#LZ+OU}c)wc#tDJ;`Q_Ufj6gzS3~#Z<6zqt zM9cQ(gZRB>>6OWyR(W5MNLq-#h0R*1W-&Wb-@n*O5ojge*)Zoh;)8zUwX zi>=f)coc}I3k?Uk-zlpgRWg%MO}nU8uLgF8DB4$q!Hdp@k~u(Kb1 z&@G%OiCCBGonsbPaJu}G4}AYd*wLWFwIxv=)5KUvJmHJ_n&`_RJTlcupl$?ELM+H? za=nY0AN-g>_9+d-N~K-4f+f^%|HkdE-^-KSNAgr%(fZXs8#x9oJ z!gfY~Ly3x8t7wy&t}|5?Mjwrd%5lGDdF|M0kS0_>&hWnv)K4%_@4zT}4VDc|ENi8& zgjk|nAVNpw>`V2|Z=?pb!MDdg1cmVyzd(3v#~_)p#q~_m>~8AY->Id?$Rkd-f>Pn( ziPtYqT-ocmC@$_OJDNE4aSl;WMkAo)YCSTAFOMS@H3II9#tfLGG|`q5OSwNo2KDI< z=Y7kI!*3ZNcddiXlZR$R@mF$6pKE9CO>eF;hNC^#y|<_vt6z+C6^P4N8BU7l??z_I|1ZW(oQ_~lABe?6>QJJg z-cs`iF_-s*VL(P?4Pa$QUpAY#HStM)FNZM2C}A+k!NMhtrw@#81GlW;o8Mc3D!7I7 zg1VYVUK|{gC+cYmd_PNPhnWGx{0a}!q}Oi=n2o8)y7ui!fEtLui@n#QGHV@fW4+TY zi%R=B?0qkS_4Kmt#upT1_wIZ3nZZilSdTpKs@-x+3xQ-oxm1W7gKed@G8cfiF4|wT z{*DoUek^9MwJ-L>;r4EZ>sZU}DN4^`jtU`(Q$&C>njYC`1EomnzNB3uYR0SL5zS^ZyUmYQ?e_bj(^hS*DKS{-7xkAD^`WQT!k_6Sx?nue(R+$a(4-EU%p| zQa;Z-Wp#nOS$5s6Vd~^9KW0t{oS2cvde-(#9(dh4IdD2(yZy?kb*2}~nu$n}2EMl8 zGGBMLCNA+N`uL`#M3)`>#fCDw>u3je#}#@GO}A}iT%4Mxue1+ZR@t*oS9`d!dO>bX z@sa+)m3hyf{FwFOFB2r|*w3V={6T9%ACmfnM;wXj7AC*P(NW!1G9FK9`@wEVN#RmZ zvAmKGw#)0~5r&O3*IQF6)U1sTL-Txkgm&*DDqhs}P^Q=gS?U81tE>AL|5auMjj)D| zVwoIT#sct?ps2i1ofgUfqlr)Et>m(>F|;fyD%=MRO?8E;gujW6CvD)^rEH2pw1SCE|f2i1FL@74IuF zBni*FODM8+C*JM8vFm~}XTYl_H{i@!+I<&*nbLQQ z{Q}&PY-c+5%_%~c#*B`LLfGQ;>2C)VE1!vmQqKM>dbsqj2WLz+lNPS^WN(w&g-LF5 z`hVZ@r@;*5B04v5TP*!HybpBwXP$h-LI)DdR&-taw^Rj z{NJ!3z5Q+hw?&0xRI3ir$`h7hMOn`^k+xZ3@<(Av=I*cZ-f|)YH?tJ46^CV2RSAw+ zXGX6!opf8WKl!p>$ZuEIW*A)`O1QJ`+z#?viaHi)2t~q$P(?r^izcQZJe-fkM94|n zyiOQ@(I!kso>vU3vxPZ9wUA;h62C4=WyiaKCK^KG>aaqTg+jpc#jl?iG&T@)jeHSzpbBk%&F->|cBp1^i>->xaagF)|V8Sn4mudwZRF<(V}Au}R$ zguN_GyefA@uX!|u>r)+AcPTT@*j1F|a6Q-u z=y4NW9|z;=d`PcDv+ca+86$@>F)EqxBIAaG#FL^)$lLDHC8Q7CA0~0DeLCt?q~RM{ zWP73*-7oDolq0}SaP#QpQsfllfuS&eeu79%E_+LztISL)@V06W(%tID2H9~HkNhH^ z<-Ikqb0m=B+r6vspQVS}*3Crohm7ziNJBp3eKI{6Q3Vi)EAX^K7~K(CM)*FWpN5_h zHZuALRaY

MX08@KlfnNDWe5mxFgX7JxWP<kWAqU}g5gIj^{2_;e>A z%>OuaJByfLH&^N>9shdJ?6bYA-NvJr=##PH#!yuoFtx;>VKDJe{dV}yb|t7Z+-OB6 zcEKvU!dgP94-SUi%II}G+rYL>9m^|quZX8uD{0~1Y9hsOAhtf1h zEcAub?l%laKJicKELM($HTacf?yGxYPqG#1)VcF$kyFMkQ)=DN`0&&W~uoIT1 z*nEm~-epa7aX{c&6Gfd zcuteCiQhRnXKCwbitX()&3!~sy@zVE zdP>#!Ff1K8 ziq;T|V&u{Rl3aCN;e6e(QEZCGE8`?$J(q(@idVNH`646xt2X#*PGb+oj+&QufH(W2zW-XN)Bl$3M0$;hs9`U=dgk+2Ee zUKHU9$WDz_V1RM;()6ry)55|I5$haV>(9qa|NbiF@1cKh5C77R$ixjp)OWpvL{5`< znT?yJMotJ%;EUueR*hE;a~TUXul|TCfIN3Ntn2Xcw65I+-$jLl%#RHO?JHl{$RH$7 zxQ@dlN;pJ3fj=;(ZpNuEcwY|z#VSj@1gyYTJ$PSof>@7@9_)5-jj#sE{} zoTN8^yz#dzgRa`UqFXFm^V@m7)jmn3;nUmvLt5W2g81lnE?m06h6~waQ#P6nVhSnu z@#2qcf&@X%@A14gvg(W}o489$?;%2?<`94J(LGa`&Gdv=jqo6wriEWOdF-DuF?fsD zHp5P*(7-%S5D~2yvXc8`%MlV1PEf0Rxa)L_$m1{07Y+faUNlt;b&0aB=BF-Tl6Wt zrHgw|F}1{>*Oup6fsUqqQj^Nx4yckMM))fS$4mu1J?E*`%H_!E@!}!Kf6)jJ{H8Ub z3mMN(MT%(xs+azRao9YK7fqJVK!3@efWt$3spjr+&2O(Y7bB2h$_0i&t?M0t3@vu~BEbUF@}Q_O&#~Be+N}L|#)}Nw{VI_~+bB z$e5d~9k=jQfBj4u@c3))PM9$VIH9t>gh&_q?Ens6Av_0E0N>VnTMWs6_56MyR=f*6 zK`S?rjAt6WVj74C3cDs+8&~Sj2?HyZ-E7_2Fa5)OEo`; zwP@|g8!g(MG~PnoB+t)^FDEsAzMbQd#Vka1lGbJU{dDZT=eo>->@((1LQk*1mUevI zhd_2RZZ0P;7<}c=aRU|&Dkyx_b~1(~uOofmG5N04Xug+glT)CeH1JkeRCkOhy;Xvn4W;9~Gx&gU(nE@e^&#bz!WIKfftg0Y``%b<|F3vYO!CtI1yFqVM_1n=~FCAXAnR@+l0zy01sgs-B zmU(W@{5$%B!|~P2^#(R%?|3-R8JVX}go-GfS2Dg6y-9dcY$9e)rTUu=k0JAnrE+1z zxjQe9#KRfOx!nS!Q!L**ZexLWc{Yzf^E!+0f#o#|{v}#Ya8xfoMRb+g5I;>qxqW&2Or9(>J z5vvw@GI<5QR>^3p1R0B{OcPK)43?dMJlXvwGasj-s?4HSV`YH%LC>{?**8vx zWJTY5``{Jw7UW^JBBHB2KOCk+!bX^56mb~o5=Fg>dsy=;U=;Edg4cI7kAy6C#GNr! zkach3u640h7oxnk5Z#`=?5;2$1z>UJ4Ii;D4=0~EQ)oBm4JhnMGs&h1vmLXXo?t7- zRjD0QQM8NFWyB&*aFDVA^oC0!YrQzh$hdbmZ_PG-NYR*&{|UzO3Tr)z!n^WX@KJWi z?Lkhdnc3H>Uge%IS~sKJ_H=z5B7NUo5okg$@2ZGj^R&;q>tmhMfVH!p^WL&^(7!k# zuUxTG7$*}o=v-R_Y?hqM?sNU{-vvS}s-jif>?_@ztB*RlT3A2f@bG zxy1=I_Udvy+KJ7+{sjzK^3;?|>KI%5r zuV(Z_&>P^IGNR_5xPs#FMiirqW3Yrpmjc!hWubH)|)3pYBW)WtQF#h8Da zUtJQ}?}dGx{=B$*&%V}qtH+^FZdW_u4jO?tbShn39@;!<{}$0FwqRyQP+Ds53j6*~ z=G$ETJ4fy-jgvHG06Ym1r*SFwY_n7Gt?_ZUrGsNLs_demC(NJkui$@v)QiYovzPO_ zhS&Cj1R(T*vS8-1A$hA*-PKlLKBV+&VRAHSKGz5xy(F}(A)ZtzZwpuvcbN4SXv@u_ zoeKo|Wh3lWTqpiSm3Cx=&G+u^Vx1PKQhd=G84GA%!fQkB_f3Qt{kdR!l=y-LEwS}r z_!XTmT8t&bCiU38`&kDzn}L?eg1u(QnzOXO<5a(4D@REEnNal~Qg6q1TYKs{w9vEM(#&8xbiZuik4n+&{Y&&^#@!jR7^ZVX)5iEXkZpfLTS*vV zZ0{@X3DXLwEhZHzts4KJaCeYY)N9}oAJcG(b47kWr}=}mn?;66I^&R6QXI2n$L*6QkOgJ}Kox13N}XXhW@47)6krTU zQ(_sZg2rG<#o0-+yJFhv$Vr~y|97)Y-PySe}PG^ecE6e+857AOX z(7$6{M|fM2pu0dmpzlEOZ$k{>g61VDa>23-@u|Y%ZhFZktq=Fr z?l0zCk<9!)5X3JG>}L{WAgx5&3+5g8qP22{n4-z58Sjg$a03caPYXsg$N1^?b6_6* zFLA*v06oD5z{SLZ*(e4%E?_H2NF$QuDAua=MRS|}4`1&c)l~NOePcmXM5IPRh#i%Z zQ7KA`9hJd|jMB*{Duf6b2aq5kM^U5+8D%6R{2-zNA|+Bn4MYfK6p`M7kPyZ~37iOo zBT3FY+xvc=^{(~2_xr9{4*ugIIcJ}}ukyV<-vZHWB7UWTSR?c(Y@JapJ^QW`ebXeNZkoAi27X$QvI-GA=C+&3?AJDRw9zC8fQ+BfY5xNae`PmZ2alYlhj zYMiJ8&Gy^(RC6Ze|1@HniHNySp*JfcD74*W(QrV5BWVxKh^u^VA0_2ARr(o$e)pbY z3D;(ffjK6s6|M^!b&z6`X;H5bLW{uW-aJ8dqGmKXGVgf3j+78@e?&au#P{-JJ{|rF zPRdkj8<77DC{DV<&gdT{FsOiN6B~)jRmS{p>`114-HMG6NO1j?Af5(ufu%&rG4O`+ zr38iUE&gFCz7ZRX1u4A_35#{y2*|2FWC5=cYXDfhm2u(!s^}nUjj3M+7gKX-hdX;t zNNr*lG?yXF!#(p!KN!(lyP&ZxU0ijsGf@|W#=>^TeDEf|C;c&b>wyK# zgiqnW|5ApkgN^qp@dxa$KU@hmU=Y6JV}F>cd*P66i$J~xGtsqU(l`(WmwhM!Gv;?? z0Z4l`)oZFXOT9C$$sK{($0$aK&B;d}2AXYL#8Z^}LJcrbDZ3MqPkIqplNEP^^lH#g zJrnQq=-cSH3j^rpcQX{m%iz1R{mNoT3$ROrJ=vePf!^IOTw#B@Kw$|K!8k8u#N5lWd?o;ORy6? zFbQdj`z~kooR+TRtrIC(bWpZs9j_R8(LFA;yoFClCHB}`Sripfp`%iIW$qx=$p;^2GC;AL7ekzPqU%Frbw6qfdmZ9F`8mib=m1X*G>Cbnlx9%(;*Vc3 z=t4YUBL7zKXwEal`6SvuVAX!C$v~DYVyIH>j`Zj@_#4eaiQG$K5FGoxoBE}?hvx9Y z`eiyZ-paw`XeqDPoLyzmHWtJ545^m3R^I{Dyd!5UUT?BK%?x8>bJa2bv_lo~WP3|goj+>IBa2Uj!pS8EF z{68aJ0&42<9;-GoIo0rZh`Fd&67uYRs1EmcxGhlNYXD-IA{ovs!P`@|@Zif$U33Sv_Duf^Sa`?E@x=QXU`@p`Vp+(eA2rplKgqC<=`B#hC+xQb+!|1X z6e|D*qgQR*3QD`60+k3A8-N9s4vYbq{v~JPH|E0dg*|D}LF{Tf8EaFX|~#=9tg&8=h*PA%*U9bd9;(5F9HP6zbrjtI%Gs zL96u9jVHfNW~m~X@Rtifh62=muZfZVJ}{ag_H{lY)hisuNH(;xRCS0z2?L7e|)EaUz%T;{i##wk$xR#IWg*#SV+7Di)?pN z?l2TNp{uAM8hG;R8w)v<_3c|Lx;k7QLGTX`4LATKw3$#wJK4VCTV3vR`Oh>=&w~2J zN4rjvqo&j|GEDzov+tw8hY-Af$PH?E15a9)ed|Rg$VUn>qLe96~fkgnAep0{`3lJIEHCq>^24?ML@ojDRjI|I6 z%t3OKt#F!|XO?DNUibM|_I}8;T6p|GChYq3L&pWq%7(*qH4rdE<=AP4lr=Uh;giQM zC^Iq|nS#X>V2WBG_tkR5S|qHjw8m}#v|#V}e^U8==5TYHI$LwHf@JlcnPSN-?Yq8U zSGNstqh6sv$0eF2x3z&$u)IY5?#a)+e)LY^O0$_}?zcvZq8+tT-A7&@R}USRb=$kz zHCTMj98Z1%oj$Q;1>A}n<$zUv8|o&|O`)-AcoEc{eW72#gEH>fr=*L!la%kn&_F!x z?VgkVI94DeKNO4#r|ds4)4PtDKQ`Spjlm_L|OK{9V0_HEuV?{jkQ(P zotCvmnl3#>Vdgf|;1+f$bOun3H?WiB&I<(0OX@0fhGoUd7a%tZCN^cM!XO!l7vLWB zG@SSP=+PW0#Q^jTV=Rt8$fjL=>it1V5W;`INcySeuv{Z{KD}79l{PcZF1J5fZX1AU!JKa|9?-Mm+K~${c!O@`i~^o{Gfr1sy&_fgQ*q4FQ0{4(I;Mn~eLM05>DIywv#2~UCC ztH5J@2>{>R0y(ND?B7V=#XR51>DZvK>3WVt!QaE|*q8=zZm(u0a>*npk8IF>KNdu} zt79w5#*~;=zir(L?*+C(cLk~eVwfLx0}UtXwmxVV4>ONTWUa5?JLPquICI<=_?{)v z4v3_4GXNdPl_?12$IymjEm7bm_A%ZtFh9G&2IxF#1URGwJiBI> z+z*xeNX9>P&EsOT40{)XSAG74Ako;b5z^>zRFw=lSEhX+Nj@n--zMfh_^KeA@uwhr zcn!F6i}vG2!2E2!vF;b@UE=S!-rLU2kXSOj|RAnZ4x3dtH>wOikntX!U1S$l1T|8jRR zv5YxA0SCyOkv*^+*ch>BYo03(BBx>gF~(7t6&vPp^Vmuk%YmAd{CwZPy?{p=Cf3z5)^gSmcBs^MPO(UXj5?L>T3B< zUzgNaGWOl2%4TA$pUE6E=wFpaCjgBjN_$xW_+~X3o?&_2MzJvGw2}SwJnsXLQoRFD z?=~rG6_O#%L)XZqYE36U@Wt;M0M_tvecsDS%|IGuZ0gvhamu={{l8br99f}kDl~no zw;5U+WZT>lw{G8yw}EG%XYIh-p~mq4gmUp2R|SV_Lq7yX$pm+MVOoy}>lQ=;DOy3P8f&ESN}P z&J9_K$ivaA`o)dWCqJ~zc#pUp@3|O!5ui^8XHkt_sZA>-;Ucj@sK6W(f9A-qqrJB{ zxsL?+hD>MQ$LJcGMWAUi4azR1S88R7s*k(^xD@jzh$h$N@}A`3MfwI*kvQmEu| zum63lCNIV88Hd|lXz!3q68NRUsOCCS&58E@)NrFZ)`Iup&Ab?zCV(E+>wwqtcKzlb zJ!Tac2i==*3&u2#s>AMa^X#<#WVxQbb*VY7qM%|2`!IN$1=(lW5F28u=terE*nI$4d_B%)@$J;0TywLr1P+d0w* zJ|zLw?V^!IY;DixP|AAtn1ue$RO$Gs2zRN1_qs;w%(l9oa6@ljCzrYuG5;K=f?Juj z`i+CZv#J>Ns$HC#7}E^$UoR@FDyx!r#faBDe4f53`=QsN%W2kE9v#wq*!9E{I`e3+ zhLh{-ZcDku`xkCS&wRVZhbhFq|xiwXf(?G4fNc@QO`QhVj37bY@ z@~sqg6L>fR1Y)xnl=;t;82|uI#{+4_m)38zgB8l>I0aTT1>H?9uqa%zH{Mp2%)Le5 zBA~9HkzBYWU5BpqJ-Q>^uojXm?>K`ocwD8n5A4Zq;K%~OjXzef$Om~^6LIhhpRT85 z@B;rTYHm+qh>G?)=!i6vXSNO+@O_Yo=flMrBJx|w!^`D;3e|uY6G^}2`g-#R7BQ8! zh1ky-a!>kOmN9sN03gy@`5zt$rvlP<`fjTJ;@i3&w)=-3$f|DF*b7^yBzu#>j@7{d${#IEY zqWYXnw?RzA&D|$j3fSHrUXIyfe5}pl&HD~#Tnt-ore$2;dgP^(zxb48EfQ8xJ#b0o zUzIJkdp~C%$P%cWdSf2Qg}UtLRnZgV*Jw~FH+*;)yjKQHdm zRs&I4hb}Z{1F$s;cv+ttCViFkf>(gI%#HGWc@OyZ)PtOd+_%fgZTxrxj+G z5+PI6qxK_Z>NZP9~qcyWTy157P0<$)kcbB{1mm;6t#;GuBbDO>@rIWpqavz59%YhwtIx zjIY1(Z)o0Mi1rFTFXYb&+lomS_3L-N?&P#@Zv_Lm_}8<*refR)YGU8x>EcDXKu^oN zDtzEoap%?7dSfYchj!xyf@u-z*_%Fd`19gxr2idd8HkkS5CQ(Pw2m2PHC1GCw*J0p zL7i53l3c^O=WO@CD!GDxRZJknF74YlAZX$>Wq8gMU#loVOyF&HrjY?@r_8z`)z%xf zUX9cKm7aMKS1~KQxJJGlL_!LN={xZ z?U;1j>2;^QOQ>TJN!J)VfSaygj)Z(`bLzi(rH5lzOVA3mvF>+2(7uY3^DZG;ZJ`b9 z2HHvx!iq9j+MrjzG}}NNHgrNlL9@#O0dBUChA)-abjxSS=FWBcG66z3>$2j_=yv2p z;V@~Se^j@>JlJ9m)e>qB2QK+#Q;J9iIbZJ0%=?C(7{JX(%}Re}&w|K=T^O{8g~>vN z7Cmyo04jxFBTdqT%;E>qQmpv zp^j3Aiib}LI~QY%{t6V};)aiQUbl4Fku#E(k~($ken9~M9vDH-Z4{eUL`LGKXXO#h zPd8S8_6XVw!ES^>tZa`oBo1W5&jFdr9rkX?+R+8xe4AK>=c`epkf~$dsev^w(&?#} z@_jtLTz+Hg6;@7M$E|ZSBp$l-IJK!B3*xgIc+n%CE|)R}qKp;FT2z&cT1qRb^8gZf zX&c#~p0%r}Xfm~^n3dNYSrFK75T*@{e6&5LGwSEvcqQCzi`fD63RnlfU;Xq+sL=0_ zNt_D`GX5-;viSvemQ`7vY297)HM9en!NF-KFR7Mx+^}z3d1RY*hvl7!zd!NTXQz`R zFK4Z}5<}f_Iy)T``02jKp#AU7+w`daB=_3dZutAn{yz>|E<;CnuqzwcWddxEAQja! z$e|pXq4E_C`-(V>csBt$+1QVhW2JBqpibHR=nJ1q48FnviL+tJzpJ~Lw%GbU@G^N| z@9=8Epfzrx?cluRWVE@x^B8XpS)ev1{C~&D{~aQqVU@n0^FiN3Wtq)Qr5}BKXseN9 zkmcO_S+gD&9Di2RzO>CWEpq3>p{&zJPcjF~v`<^j%rAop(;#PGIj&5^UttVZEXuaR zCXKk&t@8ao2(if;(w&El`MFaEXzm%r?re^9fI;7n#LNiZ+PjL`8wLlxp&;@i$)Egm z!uD+3Q{>_R-%%URi=B7SLF%PANTG88)mT+2HRh?df@+x9`tFTYm9O}YaW52MT^8ka z27&Cn(_ux{lV4=f{T3G2Ef+7Amt{qJ|B_Co`akkLxiD0G6|DmLqs7W}NTCfzc{fE~ z3J>*EKoJO)q3KFiWS?SJ{fyE8xS^mrgj2l2819AS_b57|nXb_UXs-nMXTLRIxm&Uvpk_`wd3BjU4Q7!$edg0qi40`bS>5&4VA^c;i6gMn_%6apVu_ zB(wqSAXKQMk**3yJWvGeSah4hYjGfrDZD1lccrnd)c@m&8{_8QCs}7W(z7KLT;f8e z?Np1c5U@i*Z-0{z4!HX1A3W^topi9c_;MLn$pn)Rp9(|Yr410s1=KY~>7P|E=}NA5 zzw5Mo)uR9K{Z;1*sBiym{1XBoV2~md+WsO4+d_!rZF2h=S&-7d07&arsUr)X8V>+K z1MzGueQRIguPpg?REMw@oT}^W_u&)D>r8%$a=BrtKH?K-&^+^moQJS}9^l&Cc=gqb zIa4XY%P*tvu_ct4FkU>511ej~6&v7dmBlB<>I}W56*)Vf1qnV@d>hfgyOfomkC}6= zPcselBQrT=X@;lX2Zx(EhB*dwbI=<^II3OpMPa!(jn7lQ`ZB#jWYc8Y;QysfcI2jT z!=*dLu@yiA=2O1+^%Dipvgw(_r^6~MmS&g@zAJR087#wCSNSKN%pTg_T7zpfL2gK$ zWham_xDhba1s^I4NZ%CrUPoi||2Xa`leV)HFMi%_=>!jZcp!X!{Q)p*6^>ssb?an? z*k!wG4<~qU_cx&eu>0EKmXZPnvTsVP_jg~o*6&%k@~&=?-^iMQQ>p0MW1`G$E_<%d zL9~WE$H!*r{)6}B2T0uxsmFt#JlnFlRbl6hYJhe3B1>@*)&Nu(^hsHVkc^yR!5~SHn+lIQ%b~T@y+jSF zX}em}#eRLdVQE}bO`A*js~{+SzTTkwS9lRo49Gz0y6W_(A5Ky$_ajY(|8BNZ`M8k1 zvsCAu`HtGV;xBie#!Nc@I_mt9uhHJL@9f`KQ}%pL-navp0m&l}P!d*60n7MTdOJ(k z-W4W}$4T;G;@E66R@G;Ch+5W=Ud+xPnasl0SC_|pH?y+tn$J7~eHcaUqW*FjN%D;@ zZn-S0d_6pJ!fa3F4-4`JFCI7(IeAUPnsFx(WVUstBPKFPo)>7 zBx@7$>z}MD$t69UJTNqTs5b502TwM=Rj*wz%;Dtpz6K`>Fpa@PM!uwX5;xCMW{N3n z3t&2NI!!zSeTxhU3E4^6M7Dd89{J=`+OZTrr)v)XZHiIGtt|oH2?EY zA=sg_eYs$>h)m`zwi1>P3ca6f4GUHM^tib>r*|i}Hp%mIzQ7=<;Vba;`tC3L z0DSQRI)$m#~GU!6k*1wwggM8mvTnJ#o_RVx7ZMkgr)SR@N~2jyCZ7 z0XLTylU+w{1cU!x271k~ih!f~K$tnbY$bVF$F5B8KrqKaY~ zd|=Qg0`6&72*}QeMrea#kMc1MTj0quMDEZ|gCQ6~O1Oqy6HBjto6r+ywL}f-X&yCu z-BrFZBn&7|mom`|kx+w_ev$b!aL-F{hwIZvcSzlntlA^y&!%cYH=z|kPrC=2cE9>x zq04w88V3!6ph#1*iG}{Ihvt-@Q~L(?-Bq8U z1|HU!C;F4=PFvv#{cp+?`yGhl78N>qAft+TPH*6C03BSO1~Mren@!Feu(y+9-WYt& zyBkd?883DTqveB{gJ(s}~{(1i~NP{yu{MJ8osRU>#)0qEt zKB|J<4AhYJ|EM8JP5!Hd4bZ~voHvs!V8vJn(k973;(9YT-8vJ%ij@X2Tqa7GJ0gV=)3T4@lRT<(70Sa@`} z(h8KXKHCB6)jA&dK5-p4Scv^(ssmz_hh;ViojATQo=3Z)yN&um_vY;wLNEh=hC?t9+U z_ka}32f@}gyh6`<&acZ%;VeBM!$Y04LlFfN6#}$6mv#pD(P%iG>9-nmPy=#K1f~u!$9^{DmL_ZDa#Dd$>W)C*Hm z#3mAUHuB{#k{<#8Z3xb2g-tit6`ms^-T|093&GIo9*X^m{NX^`UcpF9c2Qu3H6_1X z|8%Z8nRK-lJ!kI?R>x)Vh|f&}TAepmRpF|9$UQBR<2I13vvj5UP$k$~u=lMpqxd(M zlACOeIR!wN4S3aCU^MR)DS;dfJv+iC|2E<7*5(Uq&Ap)rOR1JTf5>THTG0@xw}AK^j7~S@2jHoBJ>r*tC3!we%0adp!CSSm{|ZKE_E@w-{@0GscRy zUH|-2F@!iB?OnjfM=Zr9;m5G)t)2q|C%fi7!{ccOLdC(FO}}D?tZ3QtFnXI8MN%f; zf_6fs_ltlgYo#YG9HA5y3rNw`tTcyVAX9%uAxF#ICdWxr2%%*HaW?Ry?Ah9n!--?roH?zeD z=c6&_q<5tH^-HpnVio#YA=K~$Jd3|6>}@^h7J4hDLd)03$v^Uj6e5ZplAA};f1U~s zb$F!M#PvRucW+0jplGDCG9vKA`!V}|6o~Ohv4{;(2Eu$0DRtwufE|&51x$p^othVk z8XN}jONX%y5~8Q{Vr=L$YMC4Sxmt|XMxHl0?f{PEY;(3GgtjlwBXG$vBezYHxl{7# zbM~IJ(S9L>VCunDi=c*cR))jvN5|2%`{bcH%?l&V|4dz#d(dw7T<<35XGLD@IordH zAmqhG=aT)X;6p%D`h;12~7}x^eXRzw?cB#%7JJFH>Xy2f&^KE!!B7HO|!f510_A{!$oAmQF!BW>? z1LSYj{`lSL;2UPaXgxrzGXU?a0f(4Iyy^7_vFuQx_XDK3;R>lx6x{sP=JL)*!k>wW z(E9XGv8@U8P2?r$UIqNIV*~JZ#Kz+!T%5RS+{L*O-25aU_1slgEBjrUU2j8wC)}ja zkPgejrL*z%hBQBT*DD-%kC?HpE$N^1Ns|(8cX46XA&FHH)~B?Jb-F)H<4phD&;8|7 zwaK6(FCL9ZxQ!p(n<&K(v76~uQsML*Yy;$R+FPNrO6lSQM{U5M!#~*8(>GpbylsHH zCnqTV`7zx;ysfGp%}ky)9&Ff9@o;qHoZ02N(%WlMbDn6DqGck%$O%vb7Npn@sUK$f zOE!Sy>q>QQ!fO`-CHqW!QC^DU7AisH&Sz{ablbZBBbBbHqqua+{7EyXRqzjHwS4;( z2<;{#F<`CT;c6BG1?q1Y#Rj)=#~j^@qpAb-`{*CA>!%k^e>|~#3SFaQ&r`IhvAfca zmYmQ_NtNpGE6<;rS#I`;wf4Ya>@b*(2h)De{3T4R{k|NJuYQK`2SzS6iZ9spBqS|8 zBsu$Q#W_;V*~fW;CVf+~k!_=3v>WOkwbJ5K*4=oz_UXlDz@CMPVA0rt59G`XWD9sd zW`EUXiHN#%Zp<$U_Ltp95>Y(zh^3Xn?i_&>UVi427XsxSX1@Cl_8! zrX65Yoqg{MA~;pdlhR#}Bc2C6JQ9ip{Pi@OvO>4K4xPsjBVCDK_|L&^%j6k_35RXY zRucqHV?_{q`f4sRPLLk=+g@>(m@yuM`1M?q@a6GlTJA9ATQf;rZwx*fnZ;`)3_>h0)oTC7aoJczy7c+>ru;$ z;F}$^hTb7NP**7Z@W<$&LLJs9lKiJg=O^a^XV6Hhz7wCe<~cRzLfBR8hnydM2s>y8 z5xxOTDJ9Y=bZv<=Ld1~lk>dc{Ir5pp8OHIGh<8R0Adob|wU28xX4tWUL^qKN8DY4M zNJO)v5iCsP9OOEY*~q|!uJFti=elxAdgt4UOk7|0&Sxa+qXT{@8NhWkG!auL<@$eB zN|>lIHUY2r$5iPIC`tDrFlWUo31%vE_%cIf7SBHlXE;JTg;%AS7fvj(l4r{QE_W4U zwWzf%emfDMb#61-wx<{>3-eH%7LQyUI9ys)TUTGvDVaw&hVrR}>e`H%pdaQ~ri$}) zYp8()iC$qhqh{1u@#ji;1ll{gvKLr|sTbWOsM0ng3y_pNMrfQ}kQ#fO+)zn5`FAXw*w5lKv z3QDs+KQT~oO+1QDj(!im&PxZKkxGRgA6@I<57x&?>*~hF6IFH-QCKla@ zNQB#frHk7<23-%{kQf#UjpEhuqUT>^R}mAri^304g)_!%8fR6gNhiC}6^bKLR-*&4 z$qOU^!OMre2NnpMk-Kk}P(}f@Nb*nOXxp*ZylV9CF0<^2lAhUTy_nGP#V`yHG^**1$ zk5Kw5C=T3Xkn+bor$%{oEhqt{7$>K+LD`e(^b&vl61l4)NNW6W=jSI88WJEAG?r>6 zTe+<8?a2z9I1x{!+1jDCnlmO}BQ6$>NXez5H=K;CiD8iGVx(YZstsonsQqI6LwXaCGtjome8A|-Q47dJpLy3v=eY*t7{FL2> z3O7F?#9UN3*r01c8Sb=!l8XnW>p&bzy@19-aA2imSni4rf(CMgL~9HyAOlDNYUzV^ z@N~G_v#DyWbUcWlbrjB3o!qGB)VobyS*haL&1CmQ!9Wr;)??+oT%a*iZQjq$8}e<- zam)$L^&(l{3RGLD1;Z(7FgOfUIbezVKxEc%glrCB%6-vySUtyeBY{Mt%bnpuBgA_g z)Cy0C>YN%zEr8!c7+^I&>{Rkp-*rpCk0vb91->7hVr%MWnjb0v9$UzSoSy zL+~|V`^!Er{c`U*Kt{QPs?r`tsouub1!esjq*tq0-}^ARDB4A6;9Dkm1=k z#PwmrZADIJVYL{Axu`(n==aOs!&A7mUvJv#6@3!FdHAN%{U;A6tH#EJ4Ua#bO#Ev_ zIokrkjvIN4O&W;zup6xA%NV}WfyUW5bYK7Tp6o|Lf{S+(s?!}_cfu_AQ~GBQv;J>O zmx9+Zb>8Z3g|t3D{Msw?W-Y@0L2O>`jBSS6t5QgXY3M@mm7BMHKgKJ*{Ht=b z1KEp}ER*WUK$SWsl0HZ*cW?C0wCE5M8#V9v*+}zZaN$JQ`&Y4 zdhMJvH2V2!Np~1)^AO3)_u1R+>cRM*OUrwvv)2R%fKg0c*DY#``QTp6gC}1NtXUK~ zrZN^7RQ8qsqBx5Xqs3H5qbX<|9ZS^_&45B&nNzPNc;FvNHd))Gnu*}u(WVj_fxDi* zM)r^w$>8jhytAcGW`ihv;Iwa&)+ zStF+7g!pg6K$-g`2{lDh`Hp*Gz1jOkmbL6vNM1|L47(7a^UswJ6pQ-j14FKMUt~iB|K*xmxGbhjQSl^2*YRTq3cNo1Gg#MP15sn zw@!PVVg%RZ^_)CMzL~YF6O0xId+`CVJ%8Ozaig!nB>#L#_1{&mvw$pd!Rr6FsClVI z6HGEpNj#DJ1pxo_*a)aw(?57;2F_;yGdVs4s=xwjmL?*!hSyDzTlmeA^ zG_h33LLjZ9o*x(^#Bnv?jB|ao6QU7ZgT7!iRy@qs2|`VzYH{}FuY3^3x&Ha8uwfVD zo|E?hk+}#+RdF2C+@a4+1_3ybeQj{U`A5 zFzFw$%b{S_T`)NN1}iuqX1>vktuc9{xkhoV7QaGqL);Nd$4Lpv_T+KR1V25lqnVU( z*Q$NHPv5+e$!Rz^zRUiQEF*fG`moZ;mB;C&Fr8V>5PH@eFM0*aUT$*J0`lJ=e!*9| zxsYyX^qPhOs8M8H#r}qEZ5yjf>!gzA%#S-Cg881`T!L>Hf6cppD)W!qP?f5l;tB)| z1VjbciWE>bUiFUNCaiF}SHIm0F_Aq|zWj{bfb+!oX1p572@GlVShDm;plkZ8)G1!E zbD34Lz$C{0Mv*sld83}xI;G?zLihFd=;dz3#D(AMAwRkN&}L3!+y{J0PuWYA1V2na#V+(StUvh75fqv(ZZNbw?5pnzYzhbGeSP94xjk&FhJ~_d12n@Y-^J{ zx|LfuP+l8s(9&0Q{qnt0qMzTpUsGDfzsG|EWf%&cU9!|10)^gHcntn~`BTmL+-JN) zpwcXa&oMx358^cFGUGm;lN&24B8+|dk}zY{u@-4|ZB4&}?LS5duG-|d4gBq-;v(W8x20`F z3;`e)bbx75u&ig?J#m4NP?^q&Q z3^z4wxqU0# z@Ms7gZZg(F5^NHn=WsB3*P=R~NKI_@;bb)0eqzRlwT*{fv(L~t!*?}(*_J!;6x;df zhZmrp;x^RqJ5TgkxEb8${dlrE5jX}zN|wE$6xWPu!6~so5^1~_sf%V0eO*4@Y5@Ad z*wulfD@$Q|b)N8Sg_qIWYU%o-qW3XT@~_}1{27Wd-|OpqK|%Q;M>8Ii|LVjJd_*mf z9aLuE>@Cvk)6pH}Fj_0cpB*SJ*W<==lA?9a_0pVQkz}@90kw+Z!sYc?8Jdz??vo1) ztF$Els3_aP^LfL&){%lO$0&3dV`&`Ne{3M$al5xdL-{O>oru>lx8E01lg1wV7gAY< zjq?LaEOfhzwpy_Jh0>CKuFH#EFDx_chRUZl<@VvW$DJ*&sg6^$LSrWsApq8VNa_Ya zDYHqu@2jw}g#!A2l6i!QWka}m$ zwWEj(ofF}b=>`IyqIzCLURyQ^LqE_fDNl-~H0{Zm&~VeOsY#I(P8a9JAy}q<=86-L z4;$G^ROo%ji`Zbm)9C?y7bpzggVxrY(Y8u+cz5W#18*6<9~fQHiwjTM#1U!+f#Ks` zO7TK(Im>Cek=xn+W9?U84%ZF~M~6rDdeKMDseL~S#*26*ixR71z`t=^Mf)vI#A5(@ zCbo1ru3M{8gWGR6ZM*|F$u>^VQTX??6E= zE5HGYi?=*I%d49Fo+oQn=5e>mG4wXpW?m2w#H7zA*zbgG-War|EHE^YK+k_wLddQY zjk|-x+SGViqaW1j>Z>_1-qrtHEvt+yzK0>K9sYS4ij5GK&9-Llm(98KI|jk7m`|N_h!%EZGqY&t$BsNV5^N&kNk|bG)aza=iVV za=de1r2m=r3~lQe0Ok3+;Ajihuj2+Ekl8Dl;-2(L(S(#3B-AU$`IkZm4wa{Ti-Por zk2rK6`Bc=@>Fv$m(J*>&+ewds#k=Tga}XLDQ!CN6wbCzQ%=BAeC2kX2udg`UZLgY7 zbAxyB;L=5}I{0GV7*1o-KtEX=ZQ3M5P5Z+&F7t_Z&q<9_!=I0kN_{NbHmbfe`N${s0GB(+>5E2Ew{pR zlrI>@dC&M_o(6fv;l3-0@21Xoxx`9lGIB>ctde;nzx*(|T?VTu^pa3lP_j@p1Fy4+ zvJ}6NN#E{{+;x}Jzz*WU`vhuJ=YP1j{=f`*4nSfnDBkM73jKQXt<(N$jm``~kBmZK z_}PFt?p6Mvtn>|lK^T}bEFWV%IFp$WCU1B5V4kTsy{j%pu<<~s_V&vk-)`54@0)V~ z?@kgP{zZt`W5(EgvIZn#Dzv{e)mfx+z1qve?Mf(butXW_gOw@@YEaO`URXO*>4N9{&b;l((E!QkZxR6WAljW%(bK)PJfRA*-PcDLPtmT1qO{jZ`m%yQ0|KOEuIPvZSm9YnReVmg-!)-V>IwE zmUe+*Khk=`U~7i}znX5d=L65GdKGs5uY#Cf*>G-Nwy&_Ny85B}Y|RfGJF*p0=#Put z721&Z_%BOPym-KI3mDo?@VFg*_>B|&0Zl) z)#hHUkCT{-otw^`u8PRAkV5xREYM^cAC!Jc1b7veDndA2KcH}fK=A`Zz6AhexUnL2 z=gO!$f|qm^ukhm#$<{hG&#QT7yDaDB$grotyJgPDWpm(rd|Ls!em7V=_bedMGjuBt z*kfNAAzbJJlL)+3 zUj?SmB)ue(4Ei5NU3~=x7_8lY19gCz9O4&uX&I^+gv??9&;N+r4*i>_7D8X+jl@h2 zjBysBW{Gp_b}`tKGp?qL4!BEWEAP?VgU8x!F82MC66{j$YbvQ&+Kl-vP*N}z>d{t6 zRH1|5ra=|*X9(iJy3}LH%=_$3VLP!|I=)deIcp`-BQ^2qx3|itQjWn4J|%9{q&pn= zzA`R=2{20TM3>X@w`BiIS1L7Mk`16{H1qj{b>*#0Wl=frISY=knNF3T0`KP1`1mJQ zgK+Z{Yc-0a?Lt7+Q@v%>pmP@3n@!fV@`W0Luk{vPGiPIsvmGD4ny^XrwwqYuUUYXG z92uIl?rOgpzHWA@4>X`w5OqqN`6D zo3eaCxZA|m4Z3LUs97sr&l)Fa5eJEg_2-E%J>gw>ZvzS@o-GNz%WV@UQUlZT77rc9 ziiFwd2W+`XLU<9DpRV?%BdMmMmJO=T3C|`j64%p+R-f|dq^;L}ba_!l&+R4tyq6KxSd)2#p;i*2Yk$T%z2 zuM`CvzG0Pt4);c+)itH(B)1OI_C_ep^(Ng#oB;UmKD0HLGgH0Wz&Znpq9_}HU`qtH zyS2FS2&Gm>A`79=0IcO5NK~9aDssSC23%L;|2Rw#>!? zmL7;?t^^sIb#vEbQ&SI1E!d>&k#ev0ipS(4zub)AmqHJ3uORn}i_(ei5hKUBx9D4P zN4+`i47iaEtXQIs<4@F5qqF0UytR&4YKPc8p=jPY5NGv7ffo_i2l?%W`Y&|{TNF>5 zhj0#sNi05@na@xPi@wwZK1-hwSd;!((HY2|1B7&iu~2@Im!vo+)WrLjLiWm_A1tBUTDguus zO^HD4Q7Ob01ca0#(nO4wAW=e&f^;ILloCpKASwbPO;iX`x(K1svJhz_K}bW{(4V6a zjwCsLi|2jc``t0_cYoY}_lJ(EQKR6Tv-e(mt-0o!GpIYh@OWVp8GIgZ8 zTk#I>J6!t2-jb8?jg8_4bAhTChe}tA&uZtO*^He`Z#A1WlOhD4xsA+l7c!8-t(a$O z3~7Igyi)7h@!0yQn~4=4?BkKWXY`JgX8h&ddV=%D^{0E_z{H$^mcRhi2?2?*Wp|sM?~%&Vv?#Y+zM%AZ+}W_x8RBy>$INf9@p9o& zTPP}nH-t9y)c(H+4ZwlN8IbWKoE?)6%gWmKz23lXk)dP4d>jQq79x>Myuk7PWU{0x*7Sk}n5 zkgenoEh>GEw5cA(4~cmNN}5U+r^sGZoxmv27E<@OEMS<`b%{JZBYlYK$oxxb8Ggl$ zIp;%8BYZEG+paUI$6Y_a4T$-v?Ny>gK2$)oP{`q7t_RxeJYuK@MG?;H6+`YFXU?)r z7!_NRw?(@ zi5;b!cJnaBqxwHX%l{1&fmGEFJY<^xiQvMcF!WC?2)%sUJwj%q7h>v&@)Um(tL-zm z38P$I*!*uwM)1FAQ$IAm7C@0zI|A7kEz~hK7@EM6;f6WCi}+!=I|_!#`e%7d!~31$ z8?Cl~5;a`BC^CH7b7^m+b9HW!-u~6MjTvTvNn3vw;F}~0cXdt?T*)Zhzk@nt=TAwHXt8f!~gM_wG^@`c}GmUEflG6#WI)vmld< z+k93Ta*x5Grf_1Ag@gffX)9FT!T9TdLdIX~R7m8b_pnKUB+aC-#8zl3q1$^1;BNMH zC~x+7zMU22Y~NVdM@#xtlksUw)UTug$7#3-`9I@DLqIMO@YxWiq`B0m9wNpwqcwyhj;w$|xqV1&btWX#Nja;+dN*j zqVDa-hgUURrvBUdR#WvJk$$CgQxwSnOdxcRSSpb9Sh|dMAT!Q!I;#Acu}Nm1-VZZ& zlP;^DdL+8c$M_94i-)p}I<3l%)CgL+hC8sc>_-F5kHuo&p+!iriM4NeTZB1}*c#@d z!Ms>gQU0_Px57@!N59IAeSH}y3hNc@mygfjTvFD&skt2_+i&G|tqmY zxFrr17_6*_I=CwHS~-$NH*8trrDK3CxGl5|S?S4mc;LVlRuWRXNAN3IJVKFnvb&8H zNP?7`#I3PqH5+mku&h+Ucvx;Z=P}F;^vyvPSy)Veea0Ud?r|R*-WFT}&$i`Xy!y;q z!@~t#wr={JqZ7u)3|o`HVL=OrSVC>XNe*rnjV`!i0R;oDc8 zdn2{_5Ap#pkp}kH98cnnfXrIRv{1hw>{W;H6LLg@xf#dgr0&l`WFC|`@5_@e^=}!A z8Nmqh+C;w3={VM}V3PZ8tY#JC!oFnx7wz|2C)|(IeEjcRx)d#IzNk$aA%I~28=|Qi zrATq)`vxlNL07KflxY#Ulm(O)j}|_fMIFwCtCbImCG0MwR1`Z6`3ig(hY#XZhnvM@ zS@jJ8`qZ_)R9?^j-Ooe18{ZTWg!x#m9bT5+U~}(7Q;thE)+E_g z3WzdNQw4D=ozQ^~7qc|SlJvd?NACRk(qY6x>FIcT;nq|1lm>6c$7A{@YG*C?8la9` zlF{EEmQjZ{W0o%^JR}YTEFBa;LKec9g#p9TyUi+`IwLrHc3Y9z(^lpdnY-SO>uqBm zox`f+Ep*Lp+};vdEFQWQQY;Ht$kqRM;taw>UkxaGZ3^5cPH@T~0t_DkR^<2?_VpQ3 z4)zlsQ%h=AK;ZLGEqU`pBO{c6TSt1`5%3A?F-0)89+=o8Rc86j<$SCUu+~LqirMuf z{Mb`Q+n$c%ruraU7;y>n_i%9E;~Mt~+x2(FD@`1@3cxczx|dllju@woeom!Y^wY-|5h z=v1bk$`4)yx7Q@*M#_q#c48(8!ydDH6;}%fDthnaC%SAQ+OpTZJ6ked_=;XUG9g&N zNbR3fD3j<8=(*2yEeQ4@(hGtssx#g52h$AG9Q1l#%~16swJhYL4;x<@2{V{a*~F8Q zYHFPcQt1VQ;>uM-GZ>bMUN&D)(A4({Jw||>QtN)XX@C1DQ#Z<_?-niJHRsR$$q}+F z4SgiwA;q>-0&X~VkMv4{ z|71wGM=BtDd-ffLzmIl&lg9f-z4!vr|x9kcH2N(J$T$} z?AoRs=NG^(H{jvy3!u^PPE`FJ{b4 zwMoC}Ji`bPd>a2UnU)_q&D43MVOjX zH7oIPAy^U2VrbQxE`54T#*Z=A-L~L>aPf{=8RTKauFO~?3tl^@)XQiAeDp9wmgiy8@3>u2p#n&})wWOwsTm*{7$vBEpclr@Zado#8O2 zFlo=-2fUiJ&a>9{o{DT=k8_rUT;x~8m8X`xuYE*yJ&#%eu$v-y+N3hQi;^1beH;=5 zVG`&HAoF<>@goszB$28W$Zo39`J;Q_FGVUh&gNlz)lphx(crq(9wBEZ%YBYt*x4NY z+pSj<%2=rlzS)qN+te>uFi>Njs^~zeG30|+EnG}6P`sku$1^=XZ2D)=CxV;6`#J10 zwSQ;TADXQx$juaZir0?NI$Z7rYkJ~cc(U3XIolF%)Po%Bd2SFp!$MFkIT&BMOXH}d| zmg(#?{&To^tjf+h$0jkt*tj5vrx#ig?xFLC;c5A?EGS6`FJPT1^^sctkJHwrv-RK` zn;}GZM$>RD=36V!bFwU8>Q&?S?}0f(-x?(=_K~-0MMpR_*=0rR7{rO*62l{zLKnl1 zUz3HGtIG_})ARGmTOh!ZFBq0p(;nu^0aluUmR|yxb5t)RqtL~lKS!D z$@pyJq#E zR|OJNwK<=G)c0-1@?IF+rJ27iD6W-CW`!6*h*^a;{y9WH@raSp@1DG)tTO_bA8Fs| z6UN6@t04bREP~55kvbmk4=1nwg8JSMjqg1Nw8s5*F+cWG#6yZu6d?`wL@23@^nS{f zV!`Q3M6(t%BHb{!JdvF*9r3MmC{MnW7Yb80`LBPg7_O*r@|@bu$t~{6DWJYF;Dpeu zh7v8Y$p81_hFoz*o=*;MnO-6_8y+WzO0wf7NuH4#Mz+YodP-$i;wF?d>*N_8xJGzQ5SteBa=>|<}D3| z>PP=n-G1GCMKn@lBp9(NggMBBrf(sv-x~WC-6Suc?kDXFE6Q_R(zhapKJ<2az9~xp zqc#Gm7Bra-H*m@>@kr=V} zRjk`P+b!B^uLJ4pXtXt5;u+)a%v(wXd7a1-15qV}T$Vr-v@G-bhoagQ0*4W*-<$^+ zn=s>yKvMlyET9Tbk-+U5V8NIerhYldsavbuT3H)Z|LUYicq|-TTpWKtH?(4Ze&B{Q z{uBvGt|H5~SW)UtO=~iA_y#hpkRGV6l3UxVcpB(8xq&G#q~e9fZ24}yOb z`~J{?%2@#wpFTi&#^&%GuGnO<>R^9Llw$oeewT_mUD#DrnJ$IjzYDh%f^2f{_*BaH zWy4sY1p|-_VheX(JpcU|66|8!T2@TjSN?UltlFjR?b2U83Xzi_Nf@NkyFkqA?r&Ml zJfOY>`qpN${Cc2J>EgbJvFyTrBngiEE}BgC8{ImbO&Ub>or3y(IrF51M zRGa;`&q*@LrFp;~nB$e-#J)yH1wv{7ezS&xzF`;)$;&wWBhbezR(aa}OnH)vF{y?V zLr6=!O-^2p;^My<@-Bol=e_95fpasYQ~=IZcd05}GV$7@UZ95m@mVLLZAW}TcrC+w zl@>_tR+srrr~`B?_R6?7sPpnV#>q>*RD@^5m)?^EUoO9aeF!lci-E!2V&838P@MHY z&qBg0lgH-MrhG#QqYmEmUhoe_j2d(Ddx^tH{RRQLgFLlKX9w^Sy%Z&IdrG#FnF!H!yM&i)?1|XbAlj{8cUt)WGrpzo)o$) ztwiJ4!lky75%-xtG#?icYt8ClTWo1p47M@Av?UIc0I%4n>V7ay8}Bo14c zQxNLF_xg$09!f=N`Km>dqa6`ChdwJT8E5S*dkVkH+L2%nF|w!qLB`ih*5=6`ZQrFXzH=(Gcvpx zHf@gj3L=pH+tA;x0}0@C5ADVe7Bjcstdlfkn^j;`Hn8{e4?Jm)%aNp-_1H~8 zZg7%qN`!UN!%%z2qs^l>XU}}BIQl2^yAn=ZEsG+(gFN}%2#GI4eV-C?w3TJW@bze? zR~?mc=BT#dw=g@Vm3`6emXHUGR8KrghR}5f9Bq=)npbpKtLz^6*A~AInTk)p6csqj zC?#j#45bD2^K7_1tA1#_?jY*P3c6$wG7;eV9}k$vf-4gAH=ve(RZ^W8n!)>$&yegXO=j8??w?Z!$-|ytT z>2=ukbW^BLN^VsJ89naoM$ByvLVaaL6!e%IW&WW5{M5!~i@L(KE%mL~9@?7|j)Q#D z#Yl|dMXLTu^gVB5{&kk{x9p{9haZ+azx_6Y^^%0W=1$bY=Rb&ioIA~MQL`y!UIdpz z7JZvgLwEK+Pyq6O!vaK;RHd08(d-~7rPgM$-=jE*Sau&;6*jg1;KAhu(l1OClZPSs z+wxp##N%T}f4O`kl7nq>-*q+}&PURf=l4iC!PEUWu8=Dx6ASHO*KP*_;Zoo|k>&i6 zU8@mYmPu&9PAr-e!!l$Z!P)nbkjrhp4R)VyI(bQ{T0?|!Wyg4dxxHgUR(8IV*t1}E zS0?j`zqn;Vw8Jn|AX=KR%4>mE;gQ0ExtN-n1at(%0{yB$oo_I+)%O!9SK7#O1xfS` zX5J55ZQW^2#1a&GdH8kthu7!xzZi(xLd7=>__vRV_ge+MpMj{H;3s|(d`$z%6Gb3( zE;13KVsQU?CJ;pe;Qbh>)v~&eubeJNlb(QQk$l?)t~(mjvx9zP2sC zu4yM$SX?>%!YweHvS@$bqD<<|+WsV7qE$BKGkMo;1 z5HR$X(EY%)uEeP*(J#G`t&=WQ)#$$Sm1U}&jE=Kp)XF*W&DT6*7}nez}fF>eIAQ|*c?poX0Noy;hUxYNL8lS>Rk zZ|yph@%d)e35FeAzc;u`w=pFTXNGQg_od5W@$ueEA zUTD*G9%A^#QItGkoE24{aYoFI4{LYpuzeL#I3#>{{F417_1EB&71!LBMQRXhz-*BH z-G%d>PrQAjxbQ=xvsOCzLnAzt&<1A&I=%^!rwfIe`w(J$tp@kw8)6{=Rhr`vT=M6C zXlNqiIrW|D-J(yxnMdGG21$NH)gjle(ADAk$@JaPLja{saDhcJcZ#eY&d`Mu( zsavoMLMq`=R%WQZe;^t$u>OBh1=1{zB3z(D8UF%mdiZMyR(n)lRd{ZHrr?U8+*N2o zmVT5Z+E~|^0z{)%Wzny*qp5hxtnaUTt;cMUU>o_?yuu9_C}xJH;_1_{Vx=I%DY zn;Z%Aj;h0FQH!{u$XG?25l|9fUQUABMba>NjHm=N5~ zqJhJ28o7^YDxTw-%3p9-*$vLJcuooP7nI%Lr%4rg_B4vm-p$96ks4hAS~c%uu$S27 zPJUGdE8qCInA)`Y`X~9amx-FHcAEMlAT#XmE6AG2Fq^pvPO8mLuG9;JdzTqw&q#^> z&}7#d;h-6lvT($7MSC9hku8L;hqsZ8B#h8?UGDqe2)!JK-5&Z3iq2X3`uYtTkBt1n z2@cNNn8;DjuQHQmIn??uL7QUB0)7i;&tpXGLf`&DH}@WWn|TiuIF2S&+%=0^^TNLT z>9UW<9(bF77&4lBefI@Tga-W=qG!T5_%HH9z6(T8V*^gFK7| za&zXYJs7wqND@7#Tnc4T;m<12JEFu0UjVY+^$HDgbHx(-@`MFQA1)Y%s5R#p zd}-IFmbmOq>iY=ohem?`YJ8iFY!K4721`g_Ex~;oGQ4h13`Ng_S-dY(YYOGG5Zmar zp+3Cgl)U=|$7>_ZZ{D@)?aa;U{cx`5xz^O2R$!c|#*o@h%GX$dSVS~Zx&`1H;w7^~ zZtX&zb5u*%isi^Oc_~V<5pPwR;zRXQqL^{^^$h<6^MqvE+5EIgA+a8%Cyj#wi^I{3 zx1Su64}p*3VOj4gzaY0>`kLHa?Kf-b8$M=PpCk+chnve0w-f9j1;pbzGdTbOOCe*+ zzOn?FKEF=Xo-IUkj*vW6R^|iKXpThKj{_Rg7z(--+z{C!fsuXw}frG_0*WDV(*a@oTef8}SwZphIgPBxyR`el=XM zJhQ8y^8s8oO5Nscj#y5q;ry*fKCTmxDZ?*a*8R{J+Qs^dr@HE;m<1?*X@68Gq3vl>b3nxG}X?J zAouv8v5}5D(}_d0X;rE2BnDJvKAMVKT;NP}X%pJL@Z%4CI}UfgKWn9GIX;P&_GjNk zFZ6o}IdgWQ3HA1bQ(J^dlx#A|#n77u1gh+0dk+2H_KL1s{Z_^e+16c$ytBq^tLZyC z@0k1jG`5ZJ$p0owzh9@Rk7R3Xp|B-ibG3$xp_Aex`k~=#)}Jz6Hq=WDU|5qfr)v7U z(a|Q=VwjauW-y3?IgN5<584SetL18rZxgYrW<>ReG=6Y2jX?h z@Ha4Sc4)XvET*X=1=1J<-97=!wuM3M-_P&&xM43?>SD{h@EjdYm%Hz%6GwbX24H;` zN{=~fm(0~3m~zP~6smb39vLau@fq^&DEV}=H_gp=i?4fH^96@*Z-5T9;5L3g3P@Iw zf>Ox{l8pB3#I0bSy|5+0k=)=hAFn)&$KJ<(8wkjj?84yGRtAsz`78Hi>>d9y5oH;* zvCzlYH&pNad%xo?K^j_L;rV7AhU@P+Qo9)$MqOY92kE&#G(dumI&y&u;Y_u7lF(TW zP3Z__h-$gIfD%j>r`A$+ot#lKq~4yqxXMsEcCF6@GfjFtL|zPe{d);xCnNa?=3d#e zLuP6G$McQ|TGZTA9lXs}R<_R{mc}SP5$(z=_ht%i!uE+G4i+on{_DnY2XKkB7}27> zZ)WX=O#^e&X+~riLsmFbN~@ddjk)or4k(=+&Q0dbQ~7r_9YD~J9QL>e3D2y1=fh~( z7jG}(P(QQR%oaz zp_dI)N08|KL;dc~9|5#3U{ctkJ}A&GLTJ;=enwQ( z3t9C>AL2LWW_WQkm(2~pza#A2(-&)QOIrlljvo(Hde;kSb_c>xv{30CM$?| z-o7W{*z>Z*I|5zqxfd<2XkPWR`!!8vm--Jd6A~e(CCU|umH?UXXI!lQ`T~gPls(h- zFWO^c2OS|U(K4ohjn@^2Cs0R*s6vuzNh+bE3DbrLl|ia-sLO*W;`~|{ z4Rlw+h-N=mj~Re@#kyF*tWw6O%6rU%(7JLDYM|(@d^dq;`!)qdN{2>}!m%nrBdmn6W{&@hVx+>6aYkMZ4I{f&-+3O+;u#1CC4V2a<~r=Jga z06`!SfU1)}n?J3-?mb*`=3};4KG$?<-XEH(--p!a5NQYX`*zB3E9{+?5q)qk`rI_3 zdJib5E`^t`Hff)MfJ(t)B2i$@^dYG7ZUpbFKA?B4Tmomxf-@!O3E2wb_5k@(m zRUX4mmm|MJv4sm7eTW?S*wRqXw2RmSP(N=+oXV?^(Z7wZyI)*h{BSS>^8B1On6!ArFM*Z$)A(SY8;$EXnl@U%}U%jb=fXmL7!%?~bW3`qZjqG@&ETPVW?Pvqlf3oQ^ z)P!BfUIuygqVNDjdNhm0{!Yars)NJluBmj)rYN<~t!+g{gm>H3Uv@m&`N67p`1EuVueEkH4(^HkqPv8Hcc+j$I0I4{!E$k z4qw5On>zl4WJftR=dr?Pr#LqFP1JeQhYHdov5y~Mt1~6&GnbV=G~Rvh_fW03Ya#Z7 zWfby2q%zfnp!-GrltT0M=qC5`hus=bw?+ZkmK1xDtXrkoFPXz!jebsKBG_T~N<85) z?sn*%)3y`nn9yiz0{moU*S`XT4j>J`sK{{a&l)QsfGy zy_C?ohO05QA{j2~9jYb&aiAj)2^KTGWSnKHU+|!ZjMVyP+agLVVyb4#XXG$kcMCcf z&*j?1w}$7kJ@_5GOI)6J*h+rRr?xBw!ShPD$rZR(rE7D6b1Iy#m*2MHcq0D868 z9XUx=(HP|*>RqMuFGXa|7INz{yI#_E>=bR*!4Q^pX?vd;H~5wtLu~a=f_vo)qqypi zJpQQPe+S94u-cbB0AC+-QwGR;WttBwmDUger zU>exeuJcpd?e;?=-p?SX@29!rUBAeN2X(a=DX$@xm9?F9@{_;damN$6-;}UJs;6sy zzTt?m@)!ZtLHe*v>CD`te&Di<)GZNmN^7bgG8drqWS4cSJs=wZPQaG^uJw01m_#DZE4~h%<8&>RXoMW;<&{XGg4x5njX{&|nx(jy~*H<2}JyMBV1X=Sl{B zdWrjwdXdx&q;?Gyz|w-Zf5lF79$7KNOAOUFk>JD*gB3F&rCxYP_qR)tfk&5Z*g8uz z)dvr-Oc2OTraq;D)ecaO@3OWW3$E~JXo>fqDKf*}Cf}oZ5@zjXCe(&L3Wl%^g(D@$ zxQf%c1z+s_*Os+TyvZ_(cHJI!_RRr5ZEuln+_Nql*nj({)97gm{SS=|Y!@-q8ptrU+&>a6A!VVJwOl?taIU-fV_UQOhUjY& ze5^Y}EyA-HKB_ck(xK1_CC3|mt}XTj17q$3MpG?iVHI-WL;+0xUAzM#=#0xlb(2O3 zH>-3D2NaInnEXzjjlG|-Ygd}i#b|ro0{?+DgY=9u>4MJQ|B7oWXV_RnoU()_N}OO0 za$@W%96*?Z;?$D0i7g=GIhN>Rg9iv5LLQRH5~eYHY0@RMuZP-MBv1Bx{!ZmkfG|KW zr40{t-fy#T@G|}`LLKDRVaJ)rI0RXN%o6_ykqwIJY=kEmxr_QLw&O39R;pECJmZFR z7uHLbl?jr78EX_`1T*nA_E}g1v0#8$in+!TK&K8Q`2AtpJHZst?-=7PStk#?IY_nK zV8V<}>Sb(k$MUrjRjX$Zp#pxXNV7;2X&qCU7Ap5Z!O#S7g`px6ZK;QmZ2W?>5lA|| z_=_Tj-c|w8*dD=6OzYbua#|N4_dEUB-7V~}=ks>kxf#Mer7->)-fK2;`Oe$SAX?<( z(8ImR0vJR5e_odNz{?mS_bjoxOb|*=qCS&M^O9^;b1+b6){=5(tDHKCg$!uW%~n~; z=}A;Q)fTz)jt0k;8@APYY$(g}5e@sVG;&p>#ab7u ze1UhIk4Osy<0;b`0ubCKitErdyRel!7zrEXqbdroRqjJ-O&|#Y`m4Z%d8wHB3!V}| z!>F~B#3szlw}m@cK^D?e$ctE}xQlbpftbE|yezlX)`E#G#PtySv;Pt1MM4lLw?&^_IL7pe_q*PO*sKF5!ctw zj4#Lwnd-ay=!=+lrzAHwRAG4ebbdb7w;>`B$(QuJrbPu_qB&MM*z-NxUe9i*dWE?! zVK2Y_s-BD)LZr$fUIGGxNHdZ0phi-6Y?BYG3<>vGfU^&Fy67QN| zM5D29(2_>eu6$Fyd`EzMXZ!Eskec;Ry2c;9_BB5ZP5aBLoA5KAA+^f|r#)~>N?jpU zTjhQI8(DU6q{V_~rwipr1x;P1Pv-m3=w6LGYzxM)0p!Q21* z%TH`P9Pn}n;#R&A27%5|M`a?|DRunR*u6tm<8#03{Q0s zW3i=(4j69<&SY)!5v>W{z-6ss&cqYY*t$iIW_x^(;c-~d4%Fic(|O*}m1pgsJ1~$o zsyw;ZXZMj6W7hY!kt=atiRsat5pE;b?7)JV5 z%r&0n2#Tf?)P)ZncZ7&P#VI#1w5U96W0>bMq`2g^q@Q$iJjWq^^h9a!=!y!Ie4i=jxfsISV4d>?d@OmECJ6-Tvm^{45&8RyQA^ZkKYh=x&ny4}*?iX8+i``8=H-y&!T^ z5$B&w@HRA)Tc}Ajwt(K|0Q~sR*a_;;5Fp}9i6$yUsIxY>d3qx`7AZ}LlVME_I%W+n zGx3}g(%ee>3Q@8x8%gjftLS=hpRrGG*3-}$>&kXj^ ztHah~5svt@msr`1#5$+Fcf17%OnFMw*4p`DYddWlDg}i zlXIbJ8`@jK>o+;&`(^0lRCZ~JWXBn6Uf4;$9S7WuEp@E>KAHT~*w#i~2tNt*qChM) z*NcN5p>$iwNjDKBfG#FUJ%rpVAjSamcI|SDdXm^o3d=8uVUc(+Hrk>);FF@bb#IVx zM$>L*ufu98oys$)@v4=wFB2v{qfvKx(Yu{3>oOb?Rjv<+nCafee^SIDxk$Cd4+$k- zrIs2tjET);4h>*Ot%>&CiB?jWP8h$@1VQSuSKwjy#9uNL>6n(#bPhWDv_e5W8#pJxe=w#?IR=%;8wOt(4Rsv;%L#E(jgFARk<)I)EqK$76+4d7R=eK3#YE zDZ0l7GhCkL`U#`+LNmf9%5zK_vItHTeRgkF$KusplZ%=*>@T@4RF_etcYh!n`YHqy z5%VNU63mhm%a<{Iu}yOBa3N~h1TI876+UOPAd#iY1qr66UCeDoEhlok2gFAoiqrBh zU5H$56p@nqZ@T3M18ldx7ES~M72E?~-;y}dY%CN5aulQ(;0r{&ur0fvJ{JMcCZ$jR zowTacX64iJOmB>WB!QIyP7`JvIlQ0!XsB;xXIV9APpLOg$M@i~C;aCxlGTZjbaN=0 zRW#*KtUAHd$>9-f<+wL%g^HxkWa%(1u|oW}sp!L7=}U0qmyRHp)G{2l?^DbE>>nEL zMmIU{jQPG6B+YXrwH;r@Sn&26Xa*>V-_lXkN(Lf=;A-~Xrh2K z5oJV=a!qw!N;e={$l5L|nIUCu(sxdpuy^E!>8Ysm_d=9V(tLfiEoV@{0NPs${MDu z2euaECyo}xyj#iu;+^0L+_`;|ip%)nBIe~TB*2+;7*v-@GgPbEd1J<^y_G*4G_HNC zgG^d456_p4T`4PG?6DYE3Vl2hRhkiW`H^=;=9o)TF3pE}-P*~vPp{o6sDyDC_x+zT zjmO1TlSbN=K4jcZr1*+AoX08~D%cH_ZV=G;hE^&rVWUqfJWJ*51XzyrT22gPxa@>s zUYM7AWkuJo=*?m<2W`a<4b^jtsW@#{>Ta|vDKbFTqKyaa14Ouma|q{QhrNMI7T}5B z>F;$r+DSjlv+_7tgCR(wHJv`8JD=J>x-aBe>2PWGt-g^IZjiAke znCAhxOoqD5X}S|3CEiP(k?QF)Ll7N7FsEnxw|+_kIUX?bh{qprq~6q^l~~ENz=2Xe zJD6fn{JHJD$^|Rookzt=&q@^F3^C7UUdR*?n};JGnH%Lj$Py^#$hFhMF|%&KLO=d8 zm7Vjj7d8`YE&8&OvdW=q$4`DX;MO`;5_0>4_wl`9wiy?B4{*H$t-L71+iPgzc6KY-3AHaq-*zS43$?^G9BF*?}J#`T9tfhsp^0 zHM1(zRW5UhJ{JddK@EjyM)ZLmfW)!p;v4&I;;*TgsNoGl7q4unvBV}m$B)^*XX?Vq zG6wmKE)@uVcxKG*&=(UmX$rA<><91nLL{3!yEHX(TbHI}W&dW*j8Q_XMqx#($J9le}_WMCc=f2$Hp>&qn zGXv4>(2I(U=-lEC5Krw}JMSK@-yU`57$4)XZ;;H^S$-+?Z#$Y*cU zy4v?}>U1=aM_wx9OYmjY>IbkN3Iv8VKLhuM=XymogH%t}WIDXSKT2ER*3cfWN5;$9k&cGtXbbbCeg5 z+CC0?z+*PiMOm(zp>$wqOFgdE=rVuC6XZyOzj2$(Dx7=-6Wy@>h}xG%$+8anb@F;h zh78s#dwxL(-$5acDCc|}P&^Z!J!^5xZzvYZs91~fk?mA`EtK&a3tdlKT5~5*sf!ox zuj**)ifkXT(>dHPcuC9pscvxMK+m>Ik9tCkcnv8<&sI;K3%Puwd|>rOFDt{ZYGqi6 zI6hSw$m+5;DvtY#e$$B8}OG$MSvF}Sn( zs>>iJ+4e#`+t9b-S%x#|NT(IoX1p?aiQo46dFd<9wY}-clpCx-S6T0F8+_g8;crE5C!$PK zqm{sHZ9l`?YZc_~`0ZeuAebUPIdtR+ulkgM$gm=;yfo(nRor0N5;UhU@6EYeP0^uk zS1v7vPbin5URVC|X;B;wT%f%cx8W89YTeO+J-2#f_4$7EtAGF99Ne{zvoInnIMmb_+*_Jy4W`zp{#*XzzPdDi#^-CcDUr1q z<)!%pSTcw=w|{CI<_4DD9Wzf>-L_N;)WxdJ@};o-|E9cegA~#G=4TH)R;s>8^p00{ zWWU)_g=1L7*LLs9qW#M z;t-%hmfRm2-~QqbfnRAQJbbq*6#nMnmWp_kJ+e%S%ncS8Qi2L4nxT^{TRbYzB~Avr z-b)_cbYtbaGwF4c;&|?*hqP?d=cs=^{n7r4tj_FQT15T_r2fhA=*LT-efgM}M5Ik7 zTMl?EL26wif}T{lum8MN{b$)ww0m)opG)-iHwW?l3ztsl3~URt`k2d8r!+J_-qT(? zd*tD>2d)>if7Y7PfrjOOw5xC^(Y0p48ysauLzvA}->{`5!w4Hw_W?sFZ)!l2#7G?} zLBvAVf3tQnld4aC6Z)HB5t8dpyB_*}@a`Mx>dNql z?Xt0w;-dR$h%9IC+0hmJ-66Rt#;P}uG}X}*X&*El)9YKH^Ek}M^ctzgP9)iwHj@hC z$3x_qj22(?)Nk(@TN(@GCf0~NvZ>=2w6W*nKno9FZxSw;YUjY!W7Q{fqCkKM;m@-QK%A za=X@rvtveXysT}+hc6mjDUHzfg*V*A5>uW^M!sP|iv8+8oaUji!WunX+vTpzMt=w< zJ5`pC7(>S}ZgPZ)-d@ahswS<1p2XZtzxO5WqGM-XR#@PwzTOWlIt?9P>lPg3jHMxh zMo0)Zw)1o-gtnXVlAStkNGR^MIuL?ycF)|9)2Mbg*|v zjn?gD2alJX)zBK?2+6op4l(~i4s$ztu>i)VIHi#(HAdZx*!`R)(ck zEq&B^#+I_!y+8V-3Hj*lj>=CP)hIz?Bk8`eb&|lcynTq|G z!|-QxNSMKCko1`9MApM}k70c<#}`c((&yru`(SLHvZQA)+qj(`UddgIv;7m~7ZdIe znEDr-$P0NVED04Y_P9XL^Zo$r8}s7g&WUE;ojYZd=^(K~3kjhu2u?S2@N!tZ;efC2 zw#8MYElwr3b}ZP_w%2U^s?vXHDW5`cYA6dkY15rU%oRpk-zjhne8}8_59EXb-@4bl zecJd24k=b-HWk$qtxq`Px3Qnur^@|NC*DmgT>a zy8{2a6#Gv(_HO~|N$q(I^$uS-lX+SFB)nx-6Q=Elhveg4?r2L~30+WHrfkt?Zo@zl z(v)#}=vc-4Z#sJWx>N@P9@G9M=Jr^J_iV|G`iYM$ZWOA%QREw`xMhoKiuD&259IyO zAYj}58>wx-eDOXctFFC>B8BKlI_eHEvJ8y5Cx~4d+pt^b_>+g3S zG`dxh4P_#d`U$Z9IlKNBTW=l?<^KPVYgI`jiX!Heq?4FakrTsIDoK(Qvdz2Wlx4!Z zgN%&1%f7^%a^l2c+9X*f*~&5#W2q$BL(FzsY|TBzxDWTt`96KVpWh$9>-zrDA6<1Z z^SbZXbA2phL)R3gNU*+#hp+w)%3x2^mGGQDtNt1rd*mb|9ebYGC_NZ+JJhH;HadCA z^MG~Z>Zgx4NG=n{2>Ua;FaCrOUq<4a5};7}SLJ^yr7Y?OrmFG{KuCE|Dp@uY^>Fh_ zYz%a`;X<1ibJl-Zi&?wLoLP)TcQIrB=z4JJTovAL)Q@XDrqvE4ldATtulmDtZ(gCt z2OD|eC4U^+ljdK*IgI_!wBipQ+D-#}**Dq;L^v~L?SiB!S2@mGjDLIY+x9@fQ- zcI(RTAGZEx@vyL9ba31==B2CpRKiHr$BI{>J60_oC59tu6h6ok&D6N>V1 z>4H#)6PTUo>G76hI(+7CEJeaf%_lau#s}@)L3@5b_JjIsD&o% zt*pJ-BtGQ9(Ht7zXIdB?dE?x(bkXl6Na|}98xnNLacxj1lvI26v!cqr5TCX_zvmg0 z6Z5wG-Y{+~Tf@3k5_r;wZ+5q!vAD2VZJ_&8uc;>OXw|x>bu^vZA#VM`<}Ju{1?+4n z@0oqPr%*f1I|ce0)T8;i@RkZY`K;^Owi|lQ?sB=Pg40Y|70BG4bfm5xsXh}s9<$G4 zGtH=ZNBQe6oimHdsUkh<6qIsH;~^1%Jki2=jdjn^)H+D6P@MiE7_5E!ugZzuVE?DQ z^12$#SrqWOs=ng+(K6Gr|J?B0cP9t@bWUYFevzu8(BY6Uf$Sh(nAC`*33M%d%)Wbf zD>ZNue5w|8$HPd^$3)olyFWoy6M)q)8hCjR1n2vWQIEXFc6)j`QNH@+oQ`514w_l@ zkE0xY+RzJp0>SVer=E*ntJLjJ!W$isrl+Q+7ZoB64GmFhU`=XXj*cFh4qrpc{BQmo zJS9s1nB#s_a^$kgRpc*HIz{*mCSQp~fG1iPjYM(KJjM0L<)8W2BxpfoJ+t+x{+KjVk* zg0O!6n%?l_gpw?sp%YKdpZA4KmT{CF%F9o;-mq@jLH_N?)TSFROIUh$cYOKsVt4J6 z9j{-k{~>nKdc|V+-K7vIp{`oisYormSVA(JnvSLpIlA zzPm-Q?Y#fCqQ`+bzxa4po&Iy`B4jPYA9N4TO4P0SzNWH|^+UW^d?j?v32Zbzh-nIQJic~ht(V!wABH^6`XKrO*#-{2{rd~M&t8ewbt|+;uYbCA;8I}CxBWMb z@)}wAbifh?#5N&(64WPDL)pzn4^vqPda5&(r#kfuxY2AaZ00P-T~Jp1j8PA6AtSZ& zkk$1og%n**3VAEZ^Xq56u5qflH2I3^usQ7?&gZTO$Ka6ux8LSk%;#_OeouE)67=9y zs=b4(5?XKHSH2z!$+em49b)ELHW4*~G{@w-I}L}ga>zaY6_XwzYvAng9&Ip51syLEVA2?y=l_aSeitZL70v$_%JHRQ#WhL*Yj|5vqoH_P zF`8zFKJW!$nD`pNC0_OfcHd_FAj)czsyP(<#^3a)wAs__Ytds791-lodDt}>`k!** z#ha@XD>yHxn3c|nUJGYzbM{n2@Iw#ZAj`G(^W>o2jeazn4_$$C_ETq2%9%dS->D;8 z!z+5s>#RE*U24y3ob~YZ9`&wwLmjtVMb@(y-bG~LWuz>;j6)0CByx`O4k8@pVI;}| z-Fev=8K;eXjaWsvR=<)aGZbaV2Qzm9Xp+FUG5g%4+{8sA-afyB9h5d*;U!e5yL&%# zy3p>?x4G|PM6u8D#;EgQQT`Eup+lcRNZA5$% z>~&OEDSIgBapxhDD0H%Xeg|Dg^iFo62(3Gcbx1hLly%`=Je@K39G%n%=?1#jezY)& zVO>b`T$<&cmCaq#TjfMWJnFfBn!5(}U%s3a^Pvl|>Sv?$PJDfNoVd1N@02-g7@ALQNL#ryv?g!@5q>YVc1cpMDn{E+=QMpY z-E&fLovS_M5wv@#Q$KyGKUZcaxtNn)RWSTR>nZoDMch&n4R5MEXdjx)BG$Rw92h z!81W1U!*9Wf^8x7eWyH9QEFOmM7gFT$>z5cHhiETjGpdW6Zc53Zp7qtdD6*@pA$xM z#yx|3v$V)AZ~Aw8Deh`s+QNE5lYLjNSXmgP>x{3h13Nsw#~!|cHC+t#Sci)}%?Qb~0z=U5+b`}y=G0d?W;yhi6bGNy^ zrFbNPoq$s%4Ww4)iVE&cimK0tu~Cx-9=aE}B=kc5=!P1ZGr1fuyIvof7Kq^&Xj+kLs;$ttU5oF1UFI)p;+52bK9-I;s)=6^jd=7=4)#jk-*QtXh}65P3{+YpvY4a z3-lPlVw|-fjvyEYM^#9(Ko{RF>zrIBA;tHj0Z#(2*kqN81Sv|L{>k%Q;iLqEHR!Sr zR=>^~?CK3E`G}_T_`}2c1b38K_YeuPLQ+lft)tol{#{6OH?KBys8e~mL6M3~7SpM~ zO;VMNs1Q+kCuW|eyrCz9s$wlq2>)<+8FE?xMxem!S{XX;3VtZ0a`;FW=xUA&Fy}-i zsXkk5|H9O@$yN`Glt`D2b2nS+SVdlExQIFlSY}IYggillSo_$HhZ3+deluLg-^@!L z?Xt%C9;mYPejrccPaXM;Z-fWFxfAAz3&hAlcn2LR0i?q3>lV#ZWNak?qJFFs;|Hu* z!b_ODoIUT1`&XmJ$sC3`b33?>M5A?7%w3{ZYF?r<$GS$g6SLFvSNGK)7JcEgbfhEM z$=Uq=THAV4J$H2EQ-!F#!^E@KHeL~nfWHQ|jf1b#lP93FldsT8{5K37)LN?}sVHYy z9)}`V8TukNk)AOq+C?YuuSnyRKQa!aGiZ-J0Y98jK+BplEvxO<0_Uc?QDW-`;$esr zFJpObh%gcPmMz7M{S|6?B)+r4N*BI`Q}!Rrm%clRf)kqmhNap|1^{%#<}A}j_8T~&c0x)cZ?ITY4c|dXQv%GgQ$(C{ z@!L^H?plWK06<7XY6H+EoaIu+U|SD6X{_C66xS)kr^9g`#e8n)3kHN0<9oleQZLK z?tcvFHm@s8^=k0!Y|44j{X0aQ4~^;tm9rXz;q3LXtKGp1s#LXb} zyh&(~wk{EdDsD&4oo5JexUc8d7E|?44w4GITTAToN}T`9Dib zB=9l@*2bJs=pi-GYgQ$pCIhjQJ=7YE!#Y5gQ`wW6)_x8 z`xb&q;5&NkgebBNJSyEANB7D|@jjyJIxv7TWtd1<1_8<~@4|zRd>S}D(5(B$vo$Fq zs?f1$Z0``WxIo~GT%SP@cL91qfEVzZd2)Z4c2Q=2#wdV_i~!%jMet`iF{OTwNT5rP z^hQ}}l}@B)uC{FNt(d!55`S(aom+5!!gFt7uu=Z~he4NhF0}_o0oriq@7`V=dTl#H zUcWxTX?Mmh5kdAsc|$aBj)>Oq@N8i4b|$lza$Dvd@ZjfIuy`d-D#9JG^+HObuQBGrp?NY~w$%{gKmmYvU8xF)>~r^! zpw{mPbA2n{>F|eI=SV>3V&)s$HO?&da^P|t{8;Ind0Ovp>hC!m&Tc-SS2N?ig!ozk4Y z76hcf`$vWZ(^K;Ns481Fjog(jm{=8{3O)X*kJ=2jiQ5f54ZU4Lk(_-MhKq0re7-s zgp#>8wXHQ_=O2NX$eyve3-@CJm99`XI0*hZ7K&>*D?1?)4NI45Af0xQI1gicfd84) zlB1&hb0iArPBJ0+JF*kS5tfI$3?k7FQ=0fuPmS(%x%+A)3 z)>q}-%Bwu}!NM2T;JI>PWnys>vL|7Wy25g3TC=K&}QoeZ>eb%CL`8beOL_)w62l*P*Wm#H< z6g;BS9IO(aIkpEFyp4`d;a0*=j6xb;A?OKM#u3|ck--|#A+|143wP_EPHS*}G2}dj zECabU`d+^H@aSKxup+vDqZKFKb{l@6szK(HmAh{4#k(rk`y)Y~=SwGtNb2b~O01V*Gsp6ZKe&^g#u`F>Ha zMF5EuUTkBBl9Yx=;Rl`u^B+YWg7)C*|Ehes1*9$EEbpJa&hPz{?Yw4z!hP;M^4EFr z=NOm_g9gx2k--Zg4(pVB$BE4F#) zcX_|;0=pmx6j{%S*#vwub_V#IS#Mjv(Y&Y{0a?l)u^ACZtMLKtr$adzxrAhVpGa}V z#QINH#J$VB^gzhYAMh;rZqTv5t`MN!ym=kU#ZJXJ&Z+@4Wj}EeCgDqAVi$J}LNZ%` zo*|^bPc%Wd%)*<_N&_Pm_vSgvt4v#r)~H-PU~3rQCI|@C7-d*xHc>Tfe+cwcbjF`c z9i46VG<{p}kPcp7)3hrWG9L%jBJRmE>RR$S-Yb8lItlV^A7c%SuHEMkAmfh*CKQ2> zR5;+v`6C-&&ym~0M|1|w>m(>agG`YY%i6+bo8Nr2M6=-WH0PEgMp8%3b&)}Kv*)LD z(Fp8!G`sJhZSlhTr`gTEoBQhzJQ&N$9M|>D47T}HR}yG-W)>MMK=Ro*k@&9)gf?6& zNePhup}2oloT-a>pfLPIb)0}7xdGq*(SqaG{N;aDM1`2%zbaiO)Na-!GEZeJQ`AB( zl`dlT5Mfniiqp`xHu-Ye+1r{uzG#0cL=|=i5Gr7(K=1~Q=HP58Z zo~T25!@&ajbOYd>bYY4~z|3`&CqIf<^FMQ--=K=-RFwA?s7A^_*kZ6Ss~+XbOY@s? zCM5UCsWLC%PU-@OtL=0sfzEFcsNe)t(!ySIvEKcHuW4jc7IUjCwB=uwMIXlJ+7d}F z(+x}?6Nmb)Qj37H@ZlKGuit!I=S-l6f0~5rN5hw_1kYI;oT6Zx=Y|d$3u}^@+s0ps zz>BX_GH{KnM|Txf=5KFk_S8wTfGWyaqMGq_+Q+UrTdIBQ-pKeBo9g7b2lD>xxvDtt z7v0F8h1ouZi+3PrD8f2kO(ec{SpEwtXmm~#psvz*ZKO&k8FwtB(gh;+3$_}j+JtB- zx6<7YCGy?gMUc`w6S7mu=Y&ps8uM51c&W~+XT8U5cT{j?{SE8?_SHsV%sK_*DIfbr zq+>_PY&LfOw8 zCDw2zA+s*+Q~WgpzN7LS*`d*msCGicIZ>xNAV0G%dxA~UW{h_-76=}qSw3~OcaZKO zU1`dUnxopp`>FH2<{gGd-c2`n!By?6bXE_jEh{}Q&qFo|&?~d}eJ^=1q-zNps(j=; zwC1Xn6ibJ0)dieGkuW*TAo$LxC0tQAUNlAc7vy`uIShI!SEZwSsTr`(NU2IIwX`2F zW9*TzlSY4j(i2WdrK*z;=yez+=1Ewq$F2+{U)(7fn10#giGJnWpU4@#Rxt*pWi?a_e`y#*CczaiBt zBTc+t(N^EEyJU61wCR|HZYzq= zzcYZJixQDC41SX7XD5{y9L_!6&|SZ$WSEyC%2g(J=roD6k#Zd|*o+oo{;0hjgoH${G1QwU ziVH^r4K~-vEcr32u3(*8>6ot9Xyx>=FwH`0FiPj}xq3a}X$_P^K3Bds+ggG^a=kka zuMK~gBJ3v39SUs;#Q|h`4Nk2$?scXo6OctFrwJzVn^x{1tfO<`g7= z^FB>psYqdJf$#&N60J&_>$h|B^&~cYl?Oxcp-Nrk8c(5Fr7*!&zyZvfIf_(-CNRx`%bvsLdz;bYJe$6Lkh;VMdJS;O(DDiRi*mEwKG(95andx zfX#6k$bMn~!cT^Bk42eg5eDxAcod4jzia3HiW?v^#a;ncQGW8X zAXae-nwS*EE*W@8Hpf`nqXMA04-B*_scNHGP177Hnc?z~lf(#n7+5n@7>m2!otP6O zMYQx(DKu?P5Vh%ctxSDXI<_nuOI{R@d;+IZ(h9oP(adH8OPPBju6L(}tld4q+%aI@ zNeEo~VPuyhEy}{wcl_-u<7fXpwIb)-){<4NC3FrH@rid8Ig~GA+htJV45QFgn6JlS zn$sv{Ym#FMu{5~`=IRGIM&m5LS7w~Z-Gj9^8?316E&C7psYc9RGvSLY22Oh9=^n#~ z8g8$7n!exZnx0S39c*9M45(|k2lKIwcL=lYiZWP##Im8&s*t(MeX!oz>M9Vbs);YD zZ18eHos)+T511}1D|&#{HF9^OtYlM7Ie*@ZZ}4Lm!$z!FGJk^aiW5c)EOi2T@h|O>6<1vOIvn#BTY^5b?5_X3~oB zoFsU(*bq^3+yyJ-!;~$8I20%H=cn;j1X_*1wb0YwjH`)*qnl(cq5r+W zJ%Q3$eU$?DZ-o;?(w`Jr6k)b!b}J!+2&Cb_OR(nf#aO=xR#C;RDQj+skfoT8sNT{* zTXwk^{eB#!#^X^E#SUqUl{97geJa()rE1@SXk0 zuT!Dm@+VwW^nty~2Z~)jWrOW%I`yC|?aP}_9b~&gEV2cxgZz5M8P>PMM^1QoC1q_7 z&LU2i-XTa3WBHFzEC0+~%6x-V4m2S{OT?X1*CfOt9XQ?<^{xdNs5VooDs!xPrfML6X0cq(Y@zwy-sr z#6WG^ZRkKM`GArG|c*vgs9Pua`r1S$V3^*9%ByWyeI_b*;!p zTjF}t^0|-o;p(acaZ2IY{We_`;XB@-O6?N(_!XF&ur@XY%@xJg#U43Bz%Belx%cu| zu?L1m2s7odpIA{fzXd-FQPB4b6+MxYQM(tmx;>>z9r_D8^!+}v&k44hzvLvA zicPJ>%yRN~|cN#RKkty!YvzNO;yz%Z zMqI^k!>)-)YZ_yUeCo5^kwIU*ca`Y|Lx@+(*4)X<+7nwdUVC3f_EKa^sEL#@quN>J zmJ@8ajr!CsOMGpvazz{pLcrfqfw(n66q;gSgh^!XlC1j=U|aeYD=Pc)4b$bHM0p96 z>dZ!44Rn@2Ok782A4g|0V)G3LzDgq^`>UuLw4Ac4Xq(o;;0MD2IR>nGE%Mt&U| z@EjQ}x<5FIMY}1V2izfivdd-?msTQgnFM@0YWyNJUa;C$6*ud802({aacn}p9$EuK zX0yV_v{PU5SD>m%;nG!@yF;eO0Len1b|&-0JJ2Ap?~RB#IPcmJ;M6fQ((&fz8)Z6R zvE9>y*p;D^vvHK(vyuy~Yy(6Mw4-?_ZceEZhE@Tjjl@9}zm=o<2RX~z2|+X{Jq1%9`FqxETY|aolFx)u8@CjEj(_vTF*{ncMJv zUHE~*Am80s^j&6WKXFY2RX4ybvt*X5S1s#i&}89C9iOurp5Ei~e1qQ5($Zi5dap*~ zFZ;-w)e(&>aR7M8G)XN$oBf}6NWI1Vv7F^;exE;##H2ZNP2TCL9Cohq?Ffqk%N^^0 zbKk#yHGZD!f1BL>`t4NtVU_iwe^n|uli$%c`BQ8qXx#;49bodnYXTn>u6iGXATco| zx;MPDxZGWJbU03m?$C#vJFzz%91)x(yEX?aUpY&UUpkM~d0a*Io?PvlEZ^qLO(S3Zk2Y0pKh zw9mS1i@9E!s6u>c28m*MJhScOc+MYg0@M>p()Fo#pS>|WmIIdH2?JO1^HzBuE%z9= zbeMM@3k*X7t|7r+zv>+C$)Y9yedFRzz8d{smCHO~A{9MA9JGhwjLcB%Kcyuk3{?x{ zudwH(*cIo*EB5L2`vUI)V+3{w`o$duAQAMA*fa__beU$mhYSRA)D4f+u{~tV%WL_^ zdoF@GA<_A!k0zWv40i z&SGJ{-0sL+hlpT<9^Zb`i=6uMOZwcqWTTsv?b+bv7#M0c)Z^G)1-fzxRTVY)UxoZ7$-#HC zqV~4vJ%#rq3D*u4+q1afJ%G3amUxKMOwz&PT0N5zk&EmhM-eY6r2asW_xPy*-^JJ^ z8LKs49W{4XhjdxFt9^_~HsX^GrwMmoOY{Yi1J}TNDo`yPDv^0wcq z`ASa(sVz*aza-=&&7Gog$ucAX*ShBrxrS{}E|zKp+Uj-ZE5-W?VqaOPU-|Ivr3hY6d?k0#ta`uCAF8S%wMs~^OwJ5b{&=*5AB zQ#k{L#ZG1$*96NHE08wZZ3(t}=q?sgL@UVMipzmoO?r)MjUPip(`eTlRu0F-NIw^WZBWE;7#RRi~f?n87eed4Pt3+B@>*&VHRl%apK^ouJL}m_> zE7(ejKn>d?CM4T#qNJDxSJjOGkzI_ixXWZ_#dn>Mn6O8fmr~PoIpXwNC*M8iOhz$> z{!l)IXpZwn*CTPWQwL;*Z2}z_*`%}A!l;PXY(SO?QkmPZ*Dy7$s(A&7YZ8C%U7}bCIs7&crTI@f&08x6i7P1qG0w_W z8}BjS*K%G2Baz3kI-zr%a@jk^kLU~_bcHdINBYUjB%SMmxEk27jL>YwNvvNf2F^e1 z9FLSax#qGnYc3=SjlPh|>LBx@OY%j-9W5Uj2OMNvCx%b$Wk#5sqkkAdTsx zt1{+ZJHfFyh|@auTG#pan(jo-2b-K|s4aK6Q8raF1IHMkeo9TPXsY>!*QnFO@m-#; zUy-vc0}@A_r>H^#RMBxTEnx$1=QHRx>>pi=kx(_!RAFMJ2}_%vL7VL+n}~9M3?W6Z zH8Hl=vy&c=M<;aOBKN~gALa%9`kekl&-D9qRW-A(m2MOv+L>ZY!4@_hx4NMUTLR6B zfo27kJAVddmBF&b`b%7&_o$}TFL}fuW3R+9c1^kx&-&14k;2XgvRj0@-SHdT4#wP> zN_PiS-tE;_iCzD%TUj=PgpilWl3rZ7ZpGx>rGFPmGt7e{`2$yy605KX8!hi{(oGP3 zoyp7MiH5%wE)CK>|7I@au72OG53XR278!CJ5ynsj2)+@Bf#Mux8?a`ON(Gv$WHqILHWHtI0Bl{l0%g2q)ayvF-ZE z#*cq24o#<^0rrzN#1>YvVG|LkIID0il#dmj$vucUNZ3n3t^!F_>IY`)sM~PjtJqrO zluA-;mFaJiBN+z*weJ_vsSdsV)kk%eKYqn0>R9innBl)FxSspGp0b5qRh=^L>Y*Ib zr6N=JC4zX*^P>gma1&Fl3%3y3kY4z1>n{y8-&h|irq>gpUa`F|KF>PRYoTv)q&xI2 z>+a#m7@Mzm*Hn=A?t+fxw)1s{pO}~IyF*6EV1$YF{aU!YWFZ`cgXK04DHN@RrL>&> z!vD^?&t(-p@*!vIxJSBcl5e&f-gH1-G+kAl`iukm-mq{a+t?mn>B=n63{L(Ex8SGadf!+pYG zv|!biT)#`i)B0jy*nQi)s5HXV{|K+V7e;vDF(I#RerrFiJcfuhItQ01GifMXOm$;E zL76(PfEF*VA9iBUrwJHzQ0Pg ztYl3}(GW7``Nubbr$oo_Zn_TE-m;Mc>K~}yLl-J-|oHk0iY9hn2 zi207Fh6Ex6Y(l9^5FgJ`C|NqKpRgj~dnJ)xd=drlj?7}F=4z!HYY8YJ(e_uN{>i@I zou58QI`vGZT`Jxe8ZpfInArmDUjT6uIS-_?b!W#A^la!-%-N|DyySxm>0T{7b>{kU z&~@*-u6yf^TaIc;F z05QGR8P|(|i1>?aFCau(vy?w5{=%*2y`b4k?O$GVc^1~F?gte&D=7%^OF6dVlY5l8 zM>uEWxI4%^-l;ZOpI;ERwSR2GX8V6;$8{5wrAYMS@tT^P{Gy_f597KS1&_QtqL4?_ z(c`l^ku^2hB*31H^fljf4htjSbRu_?pVnFL(ICtmg%dPH8XG82DYzC-epN=D0<*46o(?GaAvNa2_o>>q z1xn%>Wu*WO{M^hUGT`K6$+*RH*s!O~{HwCz17nhVpum$hHW}0Y9^=xHD-%POe(e9mP7C)Dsdh0`T zuDtoTGfqJo9T(z`81ScR@>eqVX4XO_J-US@{{!n0y^mn69e1}XnARmEFt=bVzH(Dn z>Br}hd*?2s-5cxo3_82=LxHb zXlQmL0o!SxYO95NG+^yw&fu`5jjz6pYv9c|&|PoFG)CxQt;mW27a1WXT)KRMN{E0V z%HGZs{kWxsv^J;~4&DC_t@eDo2Nu_&Yn&F&)@dQdO3A7VRC5gr%ujjKi-U-_jsdH3 zZJ$C4xbL5s3%$TVBlSH|?uoh2lW-j$_e0n|)EE-2TjnEdxH-6FBGTbfQ4n|MPFd1* zhL2dUFK%(^zAhfXZ6{!D+YOe*>^TCU60_FaDPf{ zw_@`mHomr9qJ0;gZBhP!o`+>DcPnm)?&X0k_OZ{$>ifu>#fYX&VKT!yA}m1mX3n>Q zraK(8x3SMG`1|VH>9?K#*$mssaTplbu9fxQATAs>dkww)Ku+M)y?)%JED%Jsjh7Mk&|DG)!xi2vu;ZLhJoo6vif{(lfofxjX2eU1i$YBKM4mQ%EJ6 zUhip?U(9)!eu)!g#ZI~y5xNRXo2!=7lNn)j{rSTa9aP_-Vd; zwJzu?aZKVj@1T5Jn>Ur8`w(YfCXawg*4L1pkN{#tOMFHJz9X-*ywKhyRU*~Z)qd84|2+R#jQ}-1+&vYD}j*hZL;(*qTH@+IZ%6k zD!<_2s88TfMqWf{Od%LDXnFd+A00ZDtzDB9=_cJIA8_kw+XNzGFK_a9Y`sxiTMYX! z2%PTpiB<|g357Y-CzR&UWM9L+`g0P_f}3uIG!=#nrI*K~Rh+eSmNzVCOYPkIBfMTw zq9PszS@v{9ME)1%?&OMNUq58-C%$aM_d_6?a|i)&RB|R1Mbq}~(-7a5LirA=-vF12 z-Oub)ZUt!OMC+&_2-HJyznujIhz?*A6JI;4bJAvP%&a@Co2spdS4(F)H3ALPyU3Pz zM_vW$*Xusy6bIGb8Z9gu9~$YauRU?=E{Nm&uV~mye_Y!{sa^r<(!qkEPFKa_D$;P~ zaYY(;GvjRc&JbH8EVT8CeIg$9fN@AhNSrhmsK2N8@b@Zz!s<|fFYq-Fixk$!x-2^O z9FL(Mc%IYLH21paFgN>ju>1G>!dYCGFt6bxb9}KD<0w}Rikz?e!WW81itS^9?kcCVuoD9?P=S z{aW=nsAiYfkwh^fm!Ey;ADoM<0+qu~EljIluKK}ykj{Gw86-}A5<83X#iW)W z0a82&f5xWYb5-%+ER2@F35yVQoFy9xf!Kl2En9u{WJnrr{Qzq_C}XCGq^kqnEQbnH zD-oSQU$bs9sc39;`?DvxExzkyoNu2YS|>7J5~p*i2^1{Qb^|i)?p^?=F~Z)+-0^`4 zjBN*JwRYK(7JEKVmuxKN>EUO)T-|EZoUjxzZxwEiEh?giddY%IPTZimBROUT5|Y-q z6*8=SG6f+>me|FOj|;U49ia3XBXSR}Z+=P}bM4T-H(bM$_DN_`?CS`kQ?6 z?)&yTPwjXGlD6X;G$_eL%#b&wBO7*x9O8-yANZP8VC9lWWEWHw++CBdanH&E-4aEc(+Q1djdyN?Q>&d*34cXVOmOde=K{UTS;V1z~q|^mR;i zw0!*7JR6xE)*coS5wbUDKw zi<0RM$)W9KkvB@(ZT8ZqB7h=Kzi%fyT$S*q>-M>t2DTlSo7x}s%krb*y!$sK%leMR z(lA5-w;EtLjirKe(v%KxP|hgsg>$g&d(-B1g9HXuG{_h*VWdb()tUqvErMpAF0FR7 zP$Ee7)#@Yr$tpyGDnsU<)ry+NCGfWj5SfdLD19E)eL02Y@mfR zq@&zO5mTG=iaD$4VUoR%azc0_Q~SnI!<=BpljyeRLV*@t@FHh@>)lJmUiQTSo}cnS zKrRRTV1|njc%>)zInL1wy(7EX2148SgSNN9~M8ZD3@(U%U;)frPTZzYVwqDLzo~=Lb5m0@3D5icb3w_I( zfrJ?MA1>5$VyNfhcdLxJ!ufJ7?IMP(xQ25)v6YvdC6nU9=2IlMmP2 z6irs@FL8R(9scF-8*jATem}V3@hTPN)qwxC9Z&}FgtZj(0DBN7Yr#OzW>*?FQ-&T2 z)R1f7hOHWP=A% zJi*=e99pu7!Aj1Waa{vm*)W|+T|7F3`T3$X-!A>X(oH4O2u))EFJmYh*R`2n4QX23 znemlDm9d(ss!V-`tBjn#xpz&SM7qSLu=T!WLcPo5m4`=5?~f259a~R07Wt6wb(Nm0 z|Bi^ipK|^#19Z^>*4+SU+zj=Z05dXv!Ao}IE#>ZUoIge%25J52QZI2*+n22sJ@xy< z9hRcLfz)Y;0&7uLTsUDp#&1&Ub>S=Wx66JLjPT3gOAUZA17WTAM9!?-qA(se7D|U7 zE1?|~wy{$X4GXCX(s+ou-fjZych?l7pG9vQ=FRBr7fiKY3+r4Cfo|uE-B6v<36*p> z@`u9TqGPUD8D%l%P*qUrajzlwV8yeWdoF!yC(Wj0yLd184QVUH_0PeLVkw^Rgm>qV z$>*{-`4Kdj)AE1Vdh@8Jt}kpDD~gC15dj4u&S;scGE_j~gs2D+v7jKNiV7jc)Cv+I zj8HzsP;n)u)nrG%YDch%A)3y%d9Se#zxtR#hx+4&D_=zb4gyCP--K!C1J4xnET3KX4FD}jnZ4ia0X$w)E`Aav;Xk75|_UXrUe==+w zf#sIks8zSqwP$AkW0ytBwCwRqiakivj~vvOIQIiRL5QDb8h~zpI%V`atf;xNzJ4px za2W6pjwXqUq!^l9&rx_h?_*NXsg`usYR`}y9O0hAuRhUUiWu&Hv}Ao1I@6liXqk)L ze_K9xffKd4k;+~aGdSSGdI>1SfAHjmNW*I6BI&J*ELwS=7mNtx;t8U5SRB9lNk?+AHMW(GJg6fS>Q=x&8T8PV;MD#UZsKHvuJebr< z21~8Sj1%MboSRN&AYDUGstCE<!ea-;*5j~7Gy8_RZD|}<#ws{ zR&AoG1wi!kPF#n%q}Fd!*ml+%jjD{G0Xb#BaNuVXJ3ru;A|+7)zVSzRm{o#}G2*%Z zS@#SQ){mD)|AT@Sh@Z*R! zQb1O$^p!bd{S!pkYop3EDS_!e$aZStYS=lQ#;x< zd2u){ay(=yWmOhIe)xWs8{+P^Yi3DE(SsMM01N(m2@#EaQ^PGwOG}i`dmEo^Hj-UX zvZJ4CY(AMkp-R<@*9a*5S@ASq9S(9iPhqf87L6hbBR-lB41N(i-v2waRjP}1y*_|n zrJudyKW?%Zj~tE)Xz~9M4|}3G)DgNncu3}VLCck+?uT>uPRLSh^H5GezF%-PD_x>Z zX|6Qf{aJ=<%z$av?^X(DBUp-l&M7f8p0La;wu0{EYm-2yp?+ zcwDuvwhpB@9ANulHzfnXNK7&yOl$k zxeVlR8h=kWX&*_HOhJE3SLkgVw(?S}qR2v(98xelxt^%ZVd=6u>TN(ci82D5iuM@F zG8?N!!K`gHv7zk=CWe?sBO#8ptcPynw_NgW|K$ga#EK}5lU-I}MtkYGW9@n4bH_C1 zj(1k=a!_Bf05Q>%doVZhl=n#&5Ev73y8HAL9)1EJK+Pnd8+Y3^8OE0Pry1(aubUdQ z6DuQ{XL8G0FNFo>ou+LcdN;UtgPY?Ijvn?kW`r*%FgkQ;U^yOxV|1;nxW?*jPP3C$ z8LWDOP1efp=4=COs_|;=PDx~lna7J~F-=a0ai$kGbmz(I2jd=ls?+Xl$1v(i`asyd{9t6YwML4gMRjWwst8TEm(Ovc#BjI{VFPi)p#`_bo`@+~-rY zl$}cbAQ(j7%KaFlTAl(If32zq(k0g^@F=j00Z$cQ9)Yn@5-xQBwbm~~OlqGsOe}+} zeVCXN9fDTNJvtTJM2KOK$vX64?o)>Bvt)N#7O;5qs2T6`;}yGdX;(75;~!xL3D$@n zZoP1h!;MEoxe(k8l1aaOQ+vBpmaj~!)hrrO8Dr@!yc^ZCG0g2x8P_iw9VqY1kMx!9 z4%-=QC^2&_OkP!%Cb9l~P>Wr@`oqZ#+iz-?2%co8(t>)$<@GnUj5fImGVRnqr-wAu zl28vJGzwuH*YpA#xPNA;TZiLabhnTV*Lj3XN; zQmI@IIISy@U;w;_#Sw}n=%;%qy-B#l(MH78p|RHXGku45d$!C8V`fHY#utc`mnmJ- zQjNV;X#POp9gjNK!SVNdTd`3O-j4sPcF8Z@A~vb9PYhLJ_&iLX)e zy^3x4f0$5GCDjxYj2Vb-lWI|?)ZBWW^|GRtJlu!+*W}``Rf)YJpmQ=hb1&4-O)8x% zuRFNfd?wkb+0A*vob{{n4oNfsGYy|_B2+?*k5v;gz&?TNiuMk-X?bIT&2%E8O#CTF z%vln|BG$TvAFHeGW^NqbeYwdn>tmDQ<+oJBu()hD8B!E>K4f=qYzvvkzOmIBwXlGt!c<@f4!VRCVuM_VuE)L1g?|=MS9E$}9 z8ASYb*tjw$JfD1(y7o%5)P3;t)GD5$qIas&p=Dtka*3&Gs9c{N;43Sk)4rc zB=j;}VUR4FRYvn@NIa!9a3XM+LQpI(=~k@-#@*CS6|}Y*3FwrBlH?e2X*k5Y*&bkk zGmjKMq24!{pdzWoe_OEoC;9cMv4kCC8sa&GGbeZ&!R%drA_G(s9d_EalzztM$lvJxFtcVjRvTe z7|8A6i4U??Fbo>}m%&s!Ef)_#zaGi{bPybEoU|Bh3GoJNBQ2x9iM)KoelqjY`C)CB z1Ic2@?G)>w2NoLcWKz->R%CVzJ71MjzBFVZ%)BODBgK)@$sXzk2xQ$cP!zK%72F92 z`4-i?Wcg1cbKZ?~6V*U9J0^)YpDdqW6E7X1%3=}Bx-IQ^EdtQ$tRbr(<3m>wr z?_pYTalo7F4W6OiiM9K`4_@pRDL?W>enPHyH#aL&-EJrHCV}%8Nf!}~lW`0BP1Slv zYqS4iWqvsk0%L#e-p6A=mZON-*C=IR=Wg}gNVskpXbg0NY@LQ^Dv3!gcFFho@7LE) z@MQlU8!h>Di`O=_zqwE~##$9CB2~+OglEoBbwV6(kLHw4Nog1vkehNc-%6lAbrL;_ zm=!R0;QLXYFMbJHTSjp8pO;J#f{`VxwYWt^&W@2CBXfB%lAswgvrO9C$#QwthRMzk zHGozgOxpVJ8tG5rd>XEqA_p#S!y(=|UQB`9Y@|9o)oDGe{hOMu4QI_e#SgbtuMhVt z!g3f@&ShwC6HC8mCbEsRkL|g-b5n}Vwuu?q_*SmDQ^(KMhoyahRK*bIcc(t0{2j89 z4}+>DkZ`I8EAwHNje~FSmKK6DUt|q5zsM=PD(`rf6B3BCK6!Ah8a@MslIV_azwg5s z8b46;;#sea*|zCF70xgq$C7;-5(u!+w^kP~u8WLUna)6BZOc<>or2D3U!umuZosMd zIb(Ba>C1Wd8M9(!?VSD#ea=;zM-eT_P7j8qwyF?Q&qL0WnHI9F{Gr2vj#ih?)3!4p z{XNAQG#YOHJW(azr0N!y*K@=*$ZDagom2Krt%Z={<-fAFC%3c6ksOZl#VX#gYt(n+ z-f`_7nMbR7KXo2&q~TAk&XuQ%^IcB~w(BBy13{-?3^*m0{$(Ik(2^xTJ8H7Bd&ZA) zyyq#>Z(mIH-6fct;bxA*(F(8rxO28BAvf4q2-VF|1i(v7+hd9?Ct*nud6Od|Q z6L%SOFye&#E2?FfMnZ`BQ>MeJ(%^`p2T$J#h#8G7Eq$X?o3mdlH45=n@Zx|hED}Kq zfJiy=tIhHatN}Q3B)HcaIeI|hBkJJ3&9g*;YBk7^k{XqCaaFtxWz|vZzhQEl(2zk& z3Z?50G|gS$=HoCb{`uImnyM}vH(lRusk9<$BSkegqQ?DTrdnPC8{1XY4}plH#rg*b zBBXQ35+CLX%;4o3)@Fup6YyC=Y~0M8v5RH73eOsJa~m(QmJ}tia6ODFn}N>ixav$V z=!YHbl1jsDYRj&+{gqaR_?I8kz=d#e{SQJo^M7Fo0DUf2sTz2z;YVhXP&alQM~$`n zb&fMe_QdEnp(zT^nGm2tk{j+Y_oTbm#WV=wa*XQoa*Bd}J@v?^uJ5le)W*5PAw^qW zB)vP`CO?&~;iB$?Oq(QqQ(G4R<+uyd5cX;l>)rLen_`J&b`z1Y>eEKNz)!y{kGZUy z`Y4+=cLx^Pk0*K|tTI)sa*!qAQn@KhV6tugP`d{+z0g-7lnJz-Q!vnN`&>%F6*)a>P0TwT$aVN1DnN$#i@cFKh= zE4-UFIXSfY&#JlF&i~K1=q3lHSiL1f-lSMXR%sT2Mmq!7aY|xsGfe=z*sO8Rwq{pZ zB^J`ofA8)VOfgv^2wwI&BtKX%?H~;;eEp?5!vDYR?_a6!D94bD{`4VRxQbNi90+BD zU%?4h7X}Gm0nh#}ZuS7y5zjGsAtuC|EUUxpGmD6gKCMj_N8I#@hHW_&wKCQE7ii;D zkF|2K@hVmK<-+O}13MHQ^Tn?{n%+7yXzZKK2U`$1d+K}Avj~+7?AyL6+?*}F3RUBdP9BvD(>*37^DFFRhienB zGb#5K=elcc#H8-Gi%o(Hb&se%e!U;l9#;Ak z<$iphE-UK)0@gmN<&E&{uN4Pil)T+gu~m7$mLzuq!B>q0ORggU4d4^i=WlW1Jo^&H z6D#jNfV&|Ca}Z4>)`m#xj`{9MkC?Atquf`A`9+K$ukD;BX^s4bjMlH|QX}z@=61T=UZYvxy(eGzkJz!*tWfkb>gH@fIRKoZc z_~7`7sq(26{7vE2&Xc#oejCC3PYwCJ?8DkGe}Ps5JU)~K(fQ1i@Xusc8)X4680Po@ z1wd3ftPb7^#V*v<3vDSw3q?G>797|yr`>(i@9hrbDPs0Ykg^L9wfRKR`37g0@3oqC z(436=Lmk=z+#-#4p5h$D$nmNnIKNQL@W4KW`!+}Rxg4fZ1E73CM8vpyW2PghW+}Xf z4>1>@?GhwWwHc~3ozUbEBzQ=m8kF>3X9hlGP@K`Cr)S&U%h5^B^8Cj`e!=!}MMZwT zhx4gEpi1NiOFDnPeBKB8bWUI8ooQbabZo3TkCXShn3la8cV3>H;}R9FS2$MP)aOL% zv3l4wJ@d!=v6TaFDSj^0Wn4P-^43EX{v@mJA;03q#ZxrU%hC4+^Ny{PbTWwR{LPuQE^PGILk(K324mcC z<6Npw+@rpE=w^|>;xuj{p(oF^Cs=E5Y_eZfZIv44x16x_JtLA|srtSveShkdXJvm_ z5z)7@`#dUpya4*qln?NRb)eDN5cUmG8~G3UdWsj&u8J3^K9NLkr||EEQVTdodz9(* zJ4sSsyLg2s4AVy7F>s!iwqt?Lo#iCWA?A_J+RJ$`kkXxp5n~)pACCAFI>Wfy+V*~H z{c&ISuJZQ*N9`}a3#zOx%FZbm-$U&FpU~_l2t)sK?pOeZd>q6W&Tg*Ih(r|*+!5V` z9L|bQ5a=nkOXRNH#f;>JIg?zGn=5}v6Xjm#!fbaY(GUlQt;M@9j9-K2t~_{@Ip|R6 z&===HyA;$&|CU-^ zx6B34f&V-p6jx0^sJf31sXY8n1%s@Z>CkCPSC9=qQW@GQCM z*O7>WG2(>KamItPYF@8ZSY)#NTo)8>o9fph4Sghl_BHU?Mw6`=N9ytbs@b#{=%9LH zrhKxls$Ez{i4P^N9jUDHcm?6kv%hu$8rhh=v$JT%*TbTuM`0&1wmMN7?wTAK9kBh- z0%agP^WRVry08W9vL0nJ=}eZ9>||FtHyK&MJKJU{Z^+())6XMU&%;mAVeap$rHPRn zLt4)=Eir%l#^3#6y4zq?pL3CA2xfYM5WPkJL6oURc*vQ$BP!noY%ua9x# z!YJx^j2Cte9?7D1cesSEA7q%4cSl_3;A=>U+lXzc$zh~72wU?1UwCcMLOJZ)=<@*m zlc;bVkx)7ywx05Z3cp^(MiE7zEBlut`P}5|xA@al8$So}=BXxx9fvG))DgEYV)UQr z%6(qe6E^OQ)0XE*TvNXDt=oCHx_|IUMRo5%_GaZ2wzB|MwTg)PfXzXVH4eJ*i5?MmM_Kdl)|+hEzUY~ z<{?gMvj3EY8FstR<7wp-tv2q6Ae9Fz)F{~UB0vOeYP3TG-47|=^z8YY_j14)|bXdW_VFh}nhC5o#O1{7M{;FDz zXKm8hsTWYYgmR7{&gBboIZ4blVNcp~#&-BLAd$JjX}72#p*W`P z?q&xG!ttfw)V2%3Gg+|~cIKj!ppUU?r>p?qDc`O*1KHrjAQ}mIPa9?`yNXUlPm^Ow z>Z+~&c*bW@Kikn2h9o53NsiuFq&WPhO__wScFgCemb_~H8*^51j@$1S$BN}qB(J_u z>E#yI_i6}Ep4WSffgLA$=I?~dQBEUo1?jxfTSSSrA+CX@CRftWt_5}BV{ija8*dvt z*;5g+-wkD_*tF{er)$&lU!Bhxjkx&NmtddXp{xL#zAV}$ryQzszH;mpqK60|N4r!5 z?Cvv%+b0r5G~}ioPiuaxJ62_mBYt_o0^9iE)gny4n&}EChy8fdTygS!{}EZ+!qMux z4|;y?3CTr%L@O^;s(!X0i3WwZMQm?xMH#C>#1r5cJ>#loPjjx@q!5nLU)v|F)<6} zIf~Pt0tXF&Dd+O+@9?`}4+l=Soyk!g=~Wn=!A$%jUz!klXY48~EBii^6IKr5ksf`5 z*GGD1wDl~CFLa&hg<%dnDSgFpNVehCsg|#p&%v0m;iBPBgMFpgk#NOIwyXrcnXWKZ7S^w0^$o2l!KuRp0j-Is zF%2VxijC&-3yQPIyxBP|9q81Iwd|Dt&W|RMR>)VJa~M% z)%ICNRQyaP<)12r;d?l*Oyv`v=rKtX3AS3qf*ON}^*8Yb;+u#|t<3j1SySx^aSFXc z<{q@a)c`NK9Pj|oZ!pnBXXBZ=`D69py}-|04GfSFmW^f{Fj?1OQ?#M|r;eO{CaD1D zH1m4h?p*)*5`tuZRuC77ga)S>{W5iI+GI7RKD4+?u^HnxR;S-Q+hz3uZQ)2le0^}_ z36*71*MEFrp3k^XT4ru=xXk`s|MnrjfzjEjANFsu?cRYb`bjklg`WW+Mtt$TT0M*| z?!WG|jbSU9>{1OkQgmwPYc(VMLZs4ZzeVf>A|ND|E6GRFCjU8p4lenF8G?QO1_w9w zw7ls3{1yf<~wJGsWYD*q};#4@yt2h3sCPirZ-9tdX~PJ(?PqtQa^--3%S~DL-%k#yZhw!Yrss}k znB2<`s~jdSC`tjEViyXbDp-KX1qeSCbK_3A2W zJ^0J0hTkXmsCnZQme52S3%`bhv{nmE=H(Ca{1E`bkJqlZd|LeTi`eWn9)rpb8qQGi;0pan!8fG?NFO|v zG^B=UD0Dp=;Ii@Oq2oSuHJZ@G(Njc<`R;3zq437N=_raz$sGxauMBTaw|9vPGxE*S zZycMOTpj4^66xR&>8URwh|^V?KrS<2d{r%7B-g{k8%$Osm`3uN~`OtZ<_^ezJ)w$$9G3Iuzo0q zQ;?!+iMKDeV!Uc5gr`xX>EJs4`uv~!FYowK1K;L7`(EzPT#Hvh_*+a)l{I;Dzm(bo}$g^@}m^olsB>b=2Oly2-ne1AMGfGPQhZmyMMsHGq$eSs}{XW z{X2W_-s%?iBS!q8UTbXxhU?liPIq(5qv!|v%tI=6Z#>y$<3RnT0_?uK>1VUskAb`0 zu21FlB{gvfjaEa=jb=Pba@n^)wNOuq!99B0NfWzi3+Odzt(%yl11`2ea1#-3C`gi5 z)*%N;+T(zz19D=Svfm$_yi-(B4MtE^58ndutv@3#oqE|q8e93|QG3`jk1_osD_Id- zhBD=KWTcgM4)i`bu&EWzkxS>aUQ&?dPOBBG(VPA&a9LRP3d-X zQL=Qj+qWL4h>+#uUi$v)o|hQd+SwyF(=*nAqvs9>yhuIq@xV1?2L%l$#4!Et!>6Bf zGR1Rz;X1QsYcs~hb}yCt8(8alrjzBy2F;cBru-r!es$aDxEY5(H7z$e$qTk`E+FUh39Eo*CiePoP8i}^_hcdr*dyayR7oKABfyOY#hB| zL)w+~Csyv^&i;uP0dBAupE8Mu{5P7iM74u2j_=uwzN6^Dxwez6WtrUZ_TAijuA{qo z&hlBc>liI>^!4N2*Q#i}&q@up?4@a>f1ER&ST83qXs5cEF5>pL-7?P($Nl$%uNd~l z7LRbsc#Rfjr=Sq~e#}UU?DXmib0umtMCOe}gh*=*(hOvm`C)-!>UR2HTFryT7Qm(3 zV5KmXuooZ=HarmrHW)8HjJJf*-XJU?O;m{K`Z&2s;5QjBAuYqx_u%VAQD&!674PBL zvB6;bgJn5mSE}q;`FWJVhN}UQbep1ox4Y>+RQ}I>^E3k5L?#SZao{xt09>ttf1TE% zrhE>R6Rp`cnD)O0>9%u(8@20m{9&I(v^7_9=;6qv<>@)2v*}$80o1Uo=D%8B>HGTc zD*)y-q-OO+l#7~}ba zURC&hObeR2B@z;0ton0fBxyA*f}Uc{PV9Hm>#X3eV)XMXGdOY6h0f@wn)|rRfqKmC zA#J;5W)ao?!i%*&ck>DZ@~PGR$aKk`MaW_f$_$KFoGDKo_}Q!9N=Yh=`nTk67Jgnm#oK-=;q6?k7O| z_VP((KQ?yodREeF?)Lt#mTr&I^17-olOYf8s*zqwc)hx+Wgq8n2RMQ1xBF*^x;UlD z#28xg(h!W^;=i=6Z#q(G+21^*Ltnr&Myr7@b$Mv8LBHz0T_RK}ZVSSqb`}3(bh5)H z#rm`nQfE848i_*0L&9fP%GI+I*CFTZoTpv>$vyL*_d{j?UWSroRxpWs9t&)x3~;nr z3t8>R^3;4JP)uL`)Si4pp(8f77m~ZmiOWD`Lw0LWAUGx9pLsB1Xqic)|)qXM*ho4mqHnJ3WQf2F70`u7z*RF)zhZ7Y1Y zPya&Y&Z9qH_;RyU&l33=5&cd+pxz~}^3ZnRxBkU*`&B%>U zDpCZBB8^f=(=_9QU6U=e2QuVw$R5)D2xt>7 z{r~>~0u32=tZJyY>_$Wd{)@1d2V(Y$llabNQYnw`M&$dO*K}D)6}w<|W(`Jf3dHp9 z<jTAIB@*UWYhOg9;=saU-$jB!<*E0QiK$R9%z;~XW6N=x5CN!H7FyV zZ$&&gPFkv<${lc?!hv2ld(I36kX@UZWiU%e8DPDnJWI{GN_|X){K{qG&Kcy)r-c0tL z$PkQmeIhJ=)HiGU)n)B7veu)LBZ-@SDSqP8J+vN`U6)7~o<{SNrgsyYRQi|`y8r>L z<7^|ayEtDIWU{`)rf}Qz`+)uZ)=@{avK+iRXF}{hed2q9t9-FJccR#|qB{EI=G(T= z+FdZGk|82!)WnlkTtvb|(C9Hf&y-RhEzUTewC9_Loolvl@XlYUN-+|}#3F24kKkpP72 zhT!){cd(jf>kZHrp2Uo_81HY+Hp9Y(5_5!OX+x{g<|;mY4YkT-J*F=6`r7&^=-HTI zHj))!na}EzT^gbc43;`&|_Mv41#5wH}ZN z$w2?Ue(NUP|1^h<_Yw7(KlU+QjmEWBJikH9xZ9T(qKK;w`e?C?@_>lCkmoBWh3-Zm zvlUv9F7RR~&+_8{ZQ~}9^rxxFv~Up)Bug#aFOZ7C8{bgfe-5?f7BH<)O1#z6bMbDa zyuQ%x-8`1x1;fpt7~Ebe4oX4 ziWH~HP*znQZgmI`2$K{U9KguY3Wx9l{LYaYiVkq)Z`iy>2zq{bDZ12Z+q(rLmw>7t zoDO4=4^@|O7Zu^6@@^PV^8ycJffE#r*JuKq040%(nI?(wL`X{8VVL3TvC67m#a=?Z zbQ)~0fAceGbWPc}djs^Y!?slV{x~VkzO+LPxoGYEAzZNvQE8dU&5?#(ymOQ|UPV=s zJ5qxM?8C7Tabf$w*G4C;=+CY_EVH+GCJe{=g*+J?tNxT_TopT=OxFlb6DXVJl#lH2 z=jG4XM`;Ct`kXhj^E$@!1f0&ed7E#3jfk$gz~X&C7bqcSdO+U5F@XfankTwnuYr={ zZ9oC0y@gmpk?t0=HE}Y+xA&WTApwMs?T1?oyX|!D2sJixi~D@6w<5*Y<3%M%X%8A* zot&bZ#tDYjY`L`pPTap8ZFKWzX`aQLi_&8*lw+G?;rlF#Gd;Sw7Q^~pbH8Vjo(^$-hW9=gRXDGJ>j*$^%q zW)I|mH(YAD)!WOG{6r?nAbA=g5pY@~kpSxG!xo$vbKsm@G?RJ)e6O&hEiMz)egA3v zc=tNYeCPhlc|saxpuex*Lz(RH3LZ2Wes@2qG<-r=ejJ~Yo$RbvR~%J7a6F7YIdh+R z1jtv8hjSRjedrWh`Z+6%v*^@*iJQrnS>Mb(2|lfl$8AUH`G`}FMekkqpoP@nShbq! z$z!eruvdjij@%gJ0XWy87wsTBjq_07mn>6kQFe-ioFs@(VcHGv-ud#1_uvDvPH??p zN4Zf5%@b4x1nmI5MH2l(Tm;#kYN;Jq@_xI6^ET-DK(&<3rj; z$I=@k;4c)w8(96!ZDK!i)3%GDg+v|39cPquvm#3C;ENjevEESOQby7o^3M=0gm~;W z;Q>*#{0h9(-RS{PtyGa>L6T7&VLsyTP(9sTR68Jba2!j@-zSdDj}j2`aq#da{-rWfnOTt zsKIa&y@iuFx1S+^v3srhGPDD2Z=9+euh!$Hh6(j(l?D;6#$#X0wboQT~IYkMy8 zO2d>c$L}11o5bL>!VS_(AvD)1b3X`ED0p}}>9V>eqDJySL=zCQYFZHVATRkDzMpTh z4m<{o@dA|AT&?gYc3I6NT2r)wSlg&J9G$U({Dxoq47c1_+v$|^#Ba&dr?(H#5_7!N zzp35MMaqTE&~!j14vTsC7DG5CM{S~9J+an_3QXhWBy*Q|yj{~>03zZuZLDudx`-7l4+k3-i$$g!UA&*WoboX`C zZj91MvYakDZYI{gTU@j_C;v^=vo>8&;+nfY%mE8c63&qn`L-gUk9*!~piDwcnfcG+ zd_VL2Fy}g=rSO#?MZvyiW_~-~xcN3gkO2=<%JJLXZl$H&QvWYsE=2#+)z`J*ci(11 zHH1B-SWyNEGX@U6LY3wWRrF1586&3I&W9k~h4NbL-}BajNq?cDCVe1B8 z8kNlw0`3D8S6b6sQRn~h?6a=vaq5)S#dTGQA1GZ22Y2X(5A7t0xI-4$g)+F7ss08S zx4|k#m*LP9k$MXWe1Jc(&}bZX>HC1n3NNW&v2Uarmg`b8GO1tRX&K>J4Zs%0h4=3iRX zbzWI_AG)=F*`Y31^U@y<4X0W!8bVxt)6>~|;jG@?O-ogvyCbTlEO9a$g}%5q4D#&n zbuz@iy7$#DvztIJVUdUH(Civo0n;+NTjqhsi!ux9*A2LxO?O(6G}vMRufSY z;Z0rzDW(-xo54FCFQ&vhEwhcyAIF&Iu<%6dBFgdjmv(8@(zqOPX4ICcy&V-&Z{Ir1 zqPF^kq0^lxRm$U2{uOzdR~D#DZB$VDor?!tD2Mf$SDAm)27%4xigIq&TQjXIQFdaO zwu!2JFSffiu9hhI(b1U@KSN8{Skkw-Bfn?*ZCL2D>?_^5^<}9O2WE8^I15OdCPehA}K4YTC8HWiL*#R#nlX&i^}E=->Y!j zG5E3?Y%aq5G){GxSFl&bq4l@Z+gKTOb$xebQ{P7&SSX85Qf*Q|oupBr`p3r<5aS#M zNoI`?4pWv={nzzQPif#9%b$9a@ypN zdmiY62`=LD!7-DyT~Ax%!?Fw-4NY<-kz^^OC980>`a;RFa70#}RTP{0bb(?sytjW@ z5QJk9Q3a`zl`Emc`ts+(Q(#DNyxD1aeP4O9az}f!{~G!YP|?;%aCEqQRsDx?`|W+9 zA(SIvzBnXlm%AM0m42=7q&iY@zlT8??TKM2R{SC3t}x?pe^7*WGj;9^jWo-0{i(OY zZC_{wQqo=i-s#P*VlJI)d466|NU+T`o4WGHTUpmgle54x2vKIS?1x4T(_5ylA&cs_ zk2w)5UX*swRo??tYu9AhOXjXPU zSSY+%xc6#6*rFzLJC_aFK?`xfMq+AIM$G-CN=QpZ?1+z_uh;s~a+r2#{7r=(rsee< zoh;JN6460sQS3bdT~v^*)*q^SIEaTg*}n$(;8pI(TzOjh?36eqD&#(Lw>(pnMdT4kY1*H_h9fbpAd;g?@>%2H4R?F*a5 z(r!J)t=v0VCdxAY(7M!+vn_LT!%bz33&Mll|F&H&9O3+9syHBpWEt?^NBT^FjT#MP z{<3dsPh6$Q9O30cnSp9q2THl=XbS&R7JmH+o&)d~4BTKEvUZS}Ab%HZ_$lQv)ysb; z8o}IxYl!+Va_l6%NEGjnegW})l{wnb7us6g4Zb0@HpQc)Ao-QMk?O?05Y|Ce6C^BN z^tWsofbP*$e{i-~{_u`yYGM@CZS+A}u4A5J3GO}NsaV*{(r5VY?Xls+$_$Bd<%3a1 z$4feDfhg}!U*aa#I`e?Sxa`xhbnnB{Df54znkA3ehOS$CWyYtVZU4K~aAwBm2NF5>K=IPP3 z0l)_S9*Wde^uy3qXF7|u$zI9|5|v^&>VqVfhwR^9*8^@FAWosO#PYA#{&Xg!z<1Cs zTr6CT@AvBj$Jvw%>UY5SOWW7Iw|p=;)9Cfp4`3wA#Ww--fh^wvhsy_=ko}^-xcb$= zEpcffYU96A3=u|nqtlMwUmQ=-hS^J~rM|w5hK8&E%KxucFi4iY-`sGq8;rM&eC5 zdok{fFlEGGvvBFOQG1e!I;N3mV{H1;X+yeaNgYxj?P~#Jo$nYap{}&oTf6=G)JSh= zqI9@tF~YDxWBWpe7GyFQHw9Q23mff;agLkCZp35#io+6P+Ohs$elF<>bu%~;J?_yn zWAy6Y*~+rgstgHrwRuoezr4pAy)K7mdjq3O{~=WR4KE#}!OGD z2lMWMiYZ0VE}EU#F1J>kR2Dkz4z8MWX%yZt)GjXJzJ4^?Z&+FKSV*()yC(**r^^SQ zRUHrPV|q)Z;rq}xw=bSUD!L!^*wCmmu2PbF>X=N8khhg9u2OA8lv&6KzupEBzn}M= z+DV~&?VdGNErIUTqW8+bNTO#Jm`fu}5-K!@tzt+9^?JU8AXE-bH-|y8&fzDD-E|yt zw}k>`wR4B))~{eXDl=5(iYsV8pSRMV;YCVD0_6}iH*_->i4epD zk8idordqF2ZN$@$mvk!h#Jj!S(N8JVg3(UA>Qr5_b7^1WyMec3RrV3V9)r|bJNvuW zG-fKCS&*1E{7e#wtp;*bng)P!BN|LUbg-4Uwtffr{1glq?SEOntY?C0L5NWi-wy>> zyC+NhC`}QvtgHEcZQSvQEk#KFXohX3adTH|Zm>c6*Har#wMO9|=fh?_tF83CuXDJ3 zsp6ON^4pTtlf`!=Ro5r|1@AvNXNxW3`{ z0kS7gdisoK1JF!|FhTE`Zl|Q&=I9*@{h{?KVFFL33nLo?9I-@MBJ53$!;=$b;kMut}Q|ux$ z()Rri3X}9bRQ1(JtS`Rf{^lipBwPR>i!Y@oM$@_2_8EuWN4G7N}(;r$5 z@@xv7K)K79$&QXtiz~FyZX3UV#x#>3(U8vx<>N;8CZ}n1J^V_@J~PHl%I;!PU9iFn zK5_#*zsEPV05-mjJn`SKy7Er_3LjS6Wf;&mF23ai_xz=9@Qjpd9KoYBnvC~1+qcIk z+#Vc;8u9>oAty(%KYbr|;n5eW*OiE@ss#8&{37az8}NBqJss&-=XbvyylT+GV}s-_ z>4I<(uZb?u9VYNe(I#qc2Vc7&h6*pNDxSGe9M&6VdairAyO_Z_)@?1U@~_?@5GZrr z{8hN5^BL^}F?}6VeHC(zba3P;SsMlwAYRK7CBh}+)h>w&3yGg)>uGd0VOo1%_k0)C z$g?u_t9x%~*73$u6BXJ{bA2a^Ei9!M(lo`lHYE<6G#tGD9j`G^_WGOJ9iFm~H1VN+ z7dSArW6ns84WbL|W8q^c=T6FIqr+{n}k|tL7btIXJ*lNMN=d2e+_t0KC{}a2r(VDf8ha@KS|F_3N0V zR2nZnjc8OcwEzirT7!Bg8y;e{4riGiMM-?2x@$PFPTpVqx$|S6<%|W%Jq?>Ni`s2N z^K+W(SU7k1Vf`gs2^Z5p#L|1{-^x6gDI{O#l#Mewg5w*_uyjd!w92R|u_aly{;6$r zy+vPWUaz;SeOk7a;P0arZqjFeKFHWTqzhLdZbn2DP`@^3Kz&1B1}_jfJGFw5-c((m zd7lxv7X}DGg))^}G@jA+!Tt*1JU&s}GA$6yE)z%Ikgs)ip@Z^ADv9s@z|16#E9`RuP*DF z9<;W+UVL%#VS>I#%_eU?g5o56Gf}zejtr{zrUy85@Y`b5BFO2iB@0(0Wv5Eb5gvWYkI6hXh(RJcaC>f z9NO?A&D85fMW#jsTMRd&V#R)yc3P)utKv{s7&JrbcnwmDD>il0TIww}RoI$N+zA23 zcg^uqY(A#5axl8`czbm1^bK#=Jxi$dvxnZxvJOe^}Mx8vcwwN6@0{~BB~4 zwp(FwP=rfEn-;%Pu$+r6bhhWkJ_5;3i!iO;j-r`51OBXiSaF5qP#9_K!hAsAjyGUj z2oqlh}GKdM*4lhFJ)6#u!Xd$jHP`rFvD(O?Td^o)JH#Gvv;*QBx%OGI< zQH7_g{{5ylY^us9YK+fqtcQ~XXmF;tcU8IjMt`nINn7%$w)#aiW7zBY^NizP?8f>PD|z#CvAVEiTO0$2d5Lg}!ax~V zx898L*@dD-NF*BCAX>qy4JbF}QCip3aWp@6JsN(ac1;ddaG|Q;{eIW=`TqW>tGY6C-><{-_;@}ZcF8xl zkux0;D{Al+BJE$6D2_2Yta8CRptNaxZ9*NPI-ov>o{(>7aEPyB3o!nj2*GCfjy9aI=zqro+`-89JZn?_8Xx)Gu;}{LK ztbWcmAD1k{>{xx0C&2cQ$TEuhR_!*3yUxZtuR)2e4vR9CKFWK)ECh!89QgM z`!x`kId5k=DrRrQ$K|_IXochWH_mGnEAomU(0bF31g`EMsZ#B%?e8mjvia_|0dcbsNGrbCxo9JQ2;9x&aH{nOuLg!b|X=yR5V{ zf$e0JyyezjMAdeJ6ueL}!g-~_p;n+A*>r;U1e_@PZO4QwQaEORS!iUmon1NH(3e!( z*Z1P~fKVV@T1OIiT)gOXt7-YxVyD{<4>q6uiZ`TSr!rT{hZ~$X!VDV_YNA06WSotqYAeudJ!pLFJHG(s|9?5Y(Q>)sXf+p(D=8EsN z<>|!R3jl{f9L$%a`ca z7`>8D-B53n=~0IgCKHB|@p~0ZQj5nBo~{CSlV{KyY;MyP*1u=qBOp}zhvP6@u=%D+ zf!%m;pkT(7jT^7Asa~AbEui?8v6+tpniqWbgV37*^5RCs!E4YSKEzrLVxHeJZ8_4j z{61r)2Kmv`p!Ynd`|3jc@9RxQ5UuX;@7Vl2CU-ewLs&n&ymu zTq$3bk&<2-e7VLCB{kyo{71z3iH|7TC`X^?|Lxy<@~t0hDXcBnSovZ6+zxgh4g4GX zDp4&RS@!iu}M=l+nzB@$w`_+cEUkrJyr;pu1T!9B+WfxdjzaB1yLb zbHw7&$ndzlth%Fwj~mkOTwN51Fc-qsLDT8?dN?8bi4$YI&B*A7Lr_(o4>qC5V1Qst zdsJ5H0Q8n(NU5WFG@aTPB^$VUpMtGcvx+9-%RR%^qbR7L-j-ntymDnDUALG@OcBrK?w_V#rc^MUt{c#M;IZLOBM+ z&X%h$p~d%m8T?k#S6#qvb%m(A0ga2kh!pv_D!j%Uku^+v*-qb`Zp{Y0t_5R}*$Ecx z2V}Ec|XF%%&gJ}&FMQaE~ei9T>{@TM2N$sVz%z3mVyTIH{w^aVV7;u&5xK3uE%zdl#f5P4O?h%9^ z{HEvJ^yiZBlT)^o8C|Q?N$QYg8|^Akj$D0+CMiD2L`~!v{EBll+eve>39!_#6Y3UA z9jTfFC%A%Xk%;MDNU8aSPOk`!dDV@)O$=ggJlJ+aYvwPtEnNTK`R|q@)diYFwk~nz zKFdHP#Q%?>S@b4Fsx)NIuducn-3Xl_H~pWADc zw`2jh76ptMwBw zy|@+6;Yge>?Tr7~6En3^Zv4E|^4(|NI-}2#%;xMJ8%5I|a4>D)s+@uU_bqb96n~Sr zhB%l_#7`rAx-Bf58jtt^TNFl>R%2_JBw?MYbmReCe%%S?R?4WQa5Op}ukZ9CeEkpWQmUEm}xdjO0x2Y6fL9Ux;|!XSy7eYtIk zVVBu^O!iq5c5%`0w=mPTFzJm)evl$J@gmI9MiFnio z(cEUQ0*|mweTLJ8|3AlK+yDDmu%_V%?u2;ZHu4094PupN73mYT@=J|#1vl$9C_Ggc zn86B!*s{5j4`lm8s~q0N=jOaTHFPhm-fC}WfnbYzYtdWueA|9O*iNfM3GNvm4pc;G z+>X`Og0D85)2P501R(DmB#%4bkCoLJZ205?@*Cr1;^4~;)Pi=yi7ZPs2xdcF1_EHM zkvWWCJZ?h1s@@88t|Hbtd0c!R61}s(Ab#4Ey=q>XInh#JdvcRI@o+jx6rSueX)O!g zd#t|Z2Y=UD&@u-A-ZM=saqthk`Fl+sZ!J}_mbFzK0FvQ*cgc;f^M0hVbbHRAqzsH* z6r63L2|#qYf5H`HV-yN1*2W>|l!c)hnHXd;dB4I{uOlu_!L~2K>cwJZO4PRDg#JMD3)KYwXIq1vkP%_dd^F?`(lmpcf#_$*15A zoXsSXm-&`ALYc(qGEjQ&JRgRv9d$TSwk=f_U`px?kKR7!7;S%|x|%DRw$?q-#fM8J zgKLdi;xy|zCaWPl@_5e1E=(`@oRyv|gAq6mELS>z@GiN$U?NwgH-^YPQomB$d+4jR z6e)=^U0jmafbG)&&t~>oSj9D?UM2wIpeb-(6$yt^W?Rg^t`Jlf(;N>~3c~tz55Jz? zY1rg@IrTwsJ*nU2NL_^{2lzFt%>^HnWA8-=pD5@JR3vGqjX2_; zG(87Fw8!9GEYFVn`%hJ!ZiQqL>BXu;v)oGENzSIyT;^+ct)o$$y8iD>Y_1N!OkALHugir}kMF&CZQq?%NmrY>Uij<}*a_ zi|5ORKtsB~c@y?#zyal|j$r*Y_7em}n{4K8bu$~U{#L*b=JbwBqYIK?!B&pd^o{Y? zY?KGJz`%5BK-Y{1e(dsW1yv-rRhr{P2zS!nQ^c?6xNb+#4>7r-{kaguVJ_??$QzMV zqH*jMsosm^FIB5yA+>=(?!BgnrRUFt&PmLz5lNMOY9+_89jN@2=Y)_q^d_`IRG~+1 zs@Yyt)^O?(g9ti2OyZD-;iy=@i`|l5W1jVB|Cyq`;2*?{_`YNEw+NJj=$cIC1VH*Fd#%&qgS1TSu@6)fXU1LV(CDOlM+JcjvL|D+dR^b)x-ss4hAyJ|H&s(}45rlXIF@rY+rHB|PMo>{Wm88GDFP+T`^5{Qa# zS$fE)Q{-I;O8~nCmY)C=ZhmO892qL*#A9ExUJ=(Y^;Th<>P-I8AF+2fjhm?R&S7-8 zDBSM~_7-?Z`(4}x%rjE{Dj!mT(hH~%#qFhy^(FH~j0c=aD{T4iP~9BWlpzyc;A3Yl zFutmPiLKiYs8DE62xw3WSNy;~O++yIYR!BJM~^s|NtC-GZz>v))z}937bj&A zdp(}@OwmHO*d8p|MSS6@yaMj?$^8Lpw-K$K>`V9nauBuHBozr2V`S5j{kOn>x$3N$h zfH(YtXh9+}?xLyYZD(5j078$5967GZokt=idSwtnz$-d`fI>9%Zs6-UZIyQDkI}J3 z-O2em4?PO~`$r|)0*JDljbuRVyU&A~@({so1XJ9|PN3n(IX+YQXkN&}RX z&-ijT3Czw-CKjo`LEGUr>1dGL>C`u(HV0d8Jg&}4kwQ$8EaR;V;p^8C&t;@N({8Fi zFeH0_rwsbUmoxPGl?5`r&08wFg_Q|)-B?HT>iFD3zqhOffc)4kLAP5(d%DNqQR@I+ zg{?U5&!d!u$`_g>4`s=1{{y#1pO&LhixsL%E2&94q&3d8Ij8b=r2M(|Hh2;<2)yQ< z>^;cRPVn@gYJ;C#Pi|~Sxa>(XEZjR-IM)0;c+B|_etANxHO%zr63-CgkQF)IKt>(x zNV^*wSq{)?7lq}DVJ-I|L13TE!I`xh<#MY7}_>qi*fMSdot zmI)JfD!XyGI}J|QCJG$RSl;(&eza#BzA}#w$*f|p51Lzu5{`(%k_~t9K|Ht5-LWCp z6%7e@kJ|TIO}tNu3@y7Gg)5}a&I&(`&ka-;l38v4b}cR-+MnOUC(CP;PucKzgY4|J z?m;KWdT$w)L%iCH(wS}>(LT0cg@|QFQD*!tKA1USFjiG%EE0N&WBW3nS`%z0AKW|j z^qwiYgEIq*7JH9Bc!ONYR<9P8ByEa3-4sdlSx?JO6YZ&|!PvlvgL?=7N z5*5-kmkfdi9cx19uKB)W=J;B9Pvx&8vz>bg6I1ya;_0`2n;w4A`VU{d#|`%G0r1)H zmcxm|%{FnclSj0baPF3*kBi^e!p{AI?hCIEIw3p^gbqWO;~r*tAF1nJk)AWk+Zx_g z8Tw;?yZxKW;>EZ319q#f*6qZtW__e}?Nuzz>LU$X3bEiKwc_8XMQ2K$s|p->+0eJ@U=lWFhn?Y9)6^yuczb&hpPHKLyj7EZj%TF)8Ff|Y0+|!G zf#KKcP~A+^RXYfmlN?0^^-+2b(1D3&wCgJPodmlbmCw$9ojun2*?Op!`!w)g?#+dt zAH(^+JTBfh9M7k5-hhYvQ1NKQq>{)bKQ}$ebvHgzE0|eR8t!p}WGt9*WN^-%jd}NV zx9!Wmc89suzq`=S&@WLOUeKJ%tEca!`~J4*C<67*m`FHFg?PCqekrS$T>)%|;vc%C zbf|V*<1b@<%h(2DhidO!^*(u9b7;s1NQ5^E>OfB<$W`8!Ftey8?Nf95+;Njr{AW+A z%Db%dp4Df#Zj3keOBLhL9SPXd4Jv>vH2mc3tj1{`A-@E;HL$iQETK3vO0Ga=Q(zLI ziPYt!Ymu3QQd`*u*oI)DewfGVar;=e9sz*IWH4aReyOQz|0yJ&8y{Chc|tXsODU_- z*ZzD@q}qryJQL3&!O?FDCJc+v1fQPpNmrIB+T|9o*QeWN1|ZbI_%+zZU-vGHY$RyGcT4|+zJZcQjNTsJ6kG?YTw{mS7#)uNy|b2kwK!&1-8Gf2`|(hSaozi z>Xtg-5zd!!!8ART-Uq~yd^xN_N zw#2QnPH3mA@fsUw{c-)%ZVrc#?^5$D!y5}K))*p#QZQZiZV*huojbpSNS*3kF$80z zD{VUx2bRj&2~3axHezrNp!7#fXQA^t2K}JCEYmJOmouPAXfg4)5!Uw${|0qiXfyzf z#L0IjuX9bpH4t^XB!C5a4ba~BB2I9|*$Vlm!RmP1XczCUbKztI67!;*lt+FdLT}$M8db z*m{UhVqQa<{xu3N3YJ4E0a~VS7tZi8y+0If$+p0&h^URKLakb`Z@VnyxF%4AHkrK| z*foP_d|!Di9#2+TGp4*ksg1|{0$K;w^<B#fvmL+AWrREKbZo7uc{BbiBkldGSJ94# z_oUUF9^-6D-v*a={Q!hxvHSRgX^snSH}Yn?hzc6ZDPHdyE#a(MDEXi zGkaKIS5H3FrS^G%2}pQ_drD>z>0~dOzPn13&ia9oFWIM9Tw3yacgZM-oivfwDb_ih ze~`ZYB2~O3vG}1CfV}MW5zb;{5hABS%Q{pQ#%dwUk01+Ky=3mYwkytSQQ9CrK^Z>k%TRkC{!SY zCZ{XI6xtInWH{Xy3Ys!dVcAS8S8r8BPgW<`c3g*{uvZejb9H+o&J6jD|K3$;O>a2H z+#%pPh5}84pB{3s+pZ<`L`Bc^q@}w^i}*su?-pUpMWXO_VAtC$8#nU?K0y`EqEmqyJnj*ZV{2|j{vm-YoCrSrK(`^=!lay(ALm3ceozfpezYIT}-PY z^>+%(#t!3gUnE8_+?0_IbkAjdtr;#!+$gSKIPDeoZaT|tcg+pq@XL%=Wn)w)?er7u zI8kOmMy-uC2Dn%Witx<7XnkzFLOk`4LXr$lZRtTf1pNuAJE1AnlrVKP;8S##aON}c zrMqAWsrR4h@uVMNWR8qam zG+bE@!*&PvCHcDeGCQ>7<-?=TEvo$j4ID6M-)9WTIC0aQxa@VPk5n(nLy_UjxO7lB z;pBO7XeYU4&b^Ovp*;#&HtKshmb$v<`3>E^K2_3I<+ta*AX~LEI`b|lUS$3 zSo?XFqtv67c#V$P3GMh_srPlWHUZA|!@PG>5BtW2BihutBoN^#&wGJbq(17?nc70a z8f8OnFbxsNt@~r49L5NAI3OVhA6f!eVS6|Z2M#^^&6Qs$QSpn|t>8+&W$%0HFAwSWm% zMyHE2$WL~^)4*Tm3&*ht)Jp~*;&r}g(fGYlLj{wL+D(LOL>+!m>P_+rB-qUDWG4t? z@e>_pZ`g5Sc|XdhQtIZ5ZLXb8J6TV}FJv0>fhv&n&<`=zKN)QDb&uVu)MGvCyTSt8 zrbXBa*sE9;a*j67Tkj$Nq%7CmA~117)EAi12yv)1Gi2rV5+wvM<237(r|X4>4PNFX^VxqgtpWL{w=3Mx4c+4`@~O7 zatn95T)yqFyl?-e=0|bsU2fg=Q0`Cx^QtcPE%P#LyOA1dCNUxEZ&8l4pFBjE$TV*1 zFhd>vM7GtPOl!p@(3eqL4QY^Nu$?~)xFg#b+xwcoXYTD0H4S{j_j`GA#+`5=i4=RV z%WkKe=j$uP7w`F468Irk_123eV-+A9uCSjd;82{+GmqA#?-k5+tQ;j* znzLnQL5BsTvyY$jgpcEvp{Lt?xweosEtL|oF*9EHOLkWpdlyUKog zfZ`!rpX+{X_^_ZfyJ@#zOW4LcCm=l40~o<`V!rtQjI1LMt~kTn#1vD-!*OsF{{Yfq zya5{I(*p>1K+!alx!bw-$LEZ4=ntvB7UlU&0|obeW--O2^qO#1d_J#WV6R?J^30#| zeqaBVb#k|Lg(bx1ulW64N-m1SPr9kLumr@4eFGa1?foE_paT@vULHnm`DNvn@rJSl z3F=jV$_?u%1EOFTqmlu;VTD-K=f-Ps#*xk?;Ya;d~PP5$=b zt-{4{ypjS$wjJmeQDd^DyD&TeA-aKU!KWyJ0_|M5Zmm0!^yD#w;NwQyGkX3sbfG=+ z?F|ub_sz?zpT5)*TG%TdXiU6BTV5mwDV|FKB0Grcw`i+ZeG?r8F8$f`l-Rp%&WOF8 zQ%DLq_GTn<)5lS)qtf4X*--F--r?>?0`+PSLo@soHfzk{b^Y?0lXFr=YYB0tZW ze5TT7y~WE#{P}>1XHMbkG4*5}dO(TdY^d-`Ru~>fuNE^8blMS@k9iwMrd*9u;&n$! z#$ChI(ZE`JdvTuT6+1BKbSS@)P{INTLre>tQx1e-P$|>Kmg%Z?0o>wA9iqqEqW%Hq zJ!;*N)GPjX@2Ztyjh^CM3|*R;s|?i-4@XCTc|rT40|;LW&6{oS-YyV?O?aY)A9I>U$dB3H}z9Sv;M7GAPK z2CyF161k++dV`eEh>NSKzHV);j!=BpB}=_l7lBCe%ga7Fv#SZCiz%%P5+ax26*UoS7fk~Jblm^he$Cr{EODh zW!OuoRI<_a9?k5`Yw@Q-_#s|y%3e}(%^U5BQ^kjS!Ixgm*X0%=8%Bqt-M+44aAb-QsIx*3@? zlTxFp-my(49TSA1WC+Ip`8OCZ<}j#3vlHV5j7xsqmDlctsV2$0Ak~m%ni(UixP zI#i*s;Zx7p>QhZ__^Z=Ea~)!#>jv75_}IS}eEmJDkx!i0l3xhdIuJSd(NQ!Fd$Xhi z&h{cA)dRIa+B#D3T<^*7B9zd`vH*O&p0_euZr_-yJzf)(^02#-U;yIe1)%*YGkPX# zH<6D4ZnwR(lqAZ1@k-^N}Bqpg$I1dT%w*{y=E#zGH35TFq}CnA(c3y4tcA$g5-37YE5k zi~`V0En*lSL8J2gM#KS{v?2~E32#Wr-t^?s1?P69-=vLFI3|Aox->)Z@t=k2;c?JW zAG~b>#h6rd6JKrE*39}}(O!bT<|dp2Yfs<*hI`@Bhi9c8al>^;cewjujGENHXYK<% z0&u9qX|jlbhiNXCH$uz(0OZn3;3pe?nK7febj<*sZL!)hk35zUon0W)<)pB5x~(gB zI*)z$mJptlq-XPf+>;mkQ&j{Zu%fH9j8JywYEEU04*=n&XEZ7NAgStG)(Ze1(kEJZ zo1C}dD@Q=wq_^pq1EtmoIaj0l@%IBkzm>@r zOvkPIuNL#rGcX-i<2CPL6&F9&JpEc|m*owaAhsF3m=pZ{SY%wk+Sek{xPg<9@w@N# zTV~yY{LDO;!m-G^>d5Fn;$IWOb;t9NYaVpR7bxXSbW}d6cg#1mU&t;To%Na(8Y}G$ z#Q&;W-w_Y^^zTx1gocMYU2LaaT%Ne)hX<~v#f2MGgJ2lOkcLWpwP-RFo?{tWuJXZN zBuFJKxJ7D&jALXrn-CM#l>6!*g*-Dfy(!yJJ{+^DbFI^ECJ;wvA%(q+UqwFU!Y$_Z zt~RtSv~!`!+)5GqE`9|v*vUt&*w@t&QZsFtcAWDH=(0MO%JhC{Ysq#bt^u|8JJOwB zMg7rrT(m*{J|V)Uw6y(HaA1bs4=qlrXLr}*Oezk7H2A1$Yl!h22RigSLo)Iv2l9%> z$P0Of`>ReH8xTbOVD6SwTw9#D5rEAR8#5vvMg(5IeAao$-?DLP zk&FDLG`)!vFCQb6?3JsYXRnm+^R29U8E8?2k8>gxjBGA^akk=ZX|BZj%Co4zXHQR> zcfCU2Sz%!40dnv#%?C!QW`Rc<%Ft!Wpzg`u2*sk2STcU z3wAa{JBIMO48MKxV{??@Bv-j8-ZpeYQ^t~vl&VPQxI9YDfh@f$_L)$cv5MyXdA_uC z0vYV#qu&m7?EAjsCxubwrHcjQbBk&YVmv6j zy{dQw9T#8iROW_~;7Ba4FksX{m{d#=V{CfCSjOR^Q+D;rtim__`&YMTP5vo=+vo4H zuG4MZ^%7zwgbX`}Ng~{&nF@z6ei?Ttk^?pvO(^3eyvm_#uGhTHcC7CDN9gVPiTB*j z`|%A&i|+kTYpDkXZ~i`YBz8#BTY>56!^z91lFgl0zfu3%#aaamKtG6Her&QmhlWn;p!acIT+EkuLDl@&1?#gxxtNuNGbDj`q z-9rZ9#=`@uR~w(uWV39szZ|Ns*>#vRzwQi|#LrDNN{q*i%vU5GlMi2?xvVfiYh4wr z;;A4&3+!JeXbuY%pM8z7x`Pfv3kGsA4M2n-*8_|dP4@i`9GLbFudP2 z^A#Y%HmRGciy)2exqAm4IYutVeRtfRW;S;Q8yoI5PySK+^Yu+CDEnPa@O)%b9Bb=7p;NiNaC(jPM`ovwED~*dUe&pj5V$jF!FU`+G zLKnSminwr_1%AGHBVX=IoZE=k=0pCbcKo9CE)GGC+WF+N8RB zGz^$3*v+Ynebsn5r;)Ec9!XEGLVpFVVWic2V298t;9)@3-Qcbelj9B+ccrS}vZ;+H zXB;&LLY;q{elw$w`*cy0KhuAy8QvFZYe$+hUKPvC=qJz@3Y)3p!kPK3zh}k@dFFJc zM7xIH&3XT{uI}lDC7;VIZXbC5a^14|4y@~cljv-u!6}^ZZw_7f0)TFRJ2<7!ZO@_S z4e;*B2sa1xV7SJ%y5>oSPTyfsn4YI+=S-5tZ+#(=2Xdi1+P3p+%U>?0E z!1Bnx><_I`nVW6G#`|hF=iJkzf;bOs5iDJUD{n2<&#$m=rHM&>($q%$HOT^uz$Buh zB~&rK8L!855rt2BNC`J9@unlBr2^(o>G(_rvRUo@C|EfcK2RO)WaMD+d$`_SV^Qau zQTm|yW^gds<2COj>eU3PcfxsYme+989AV<3ywCG>rB(8tkD;cn-Q20fFIsC)iR|>Y z{dD{6lgB*asn@nAxqh`LzhNIuIw%;byB+wrawYQ340ach|C*}S$0-|NuYY58!Eha- z6(Z?1QZ<>Gi4@g=}sx->i6Do9B~)|SnY{cpO$YomQ>CS;03 z+U;Y1fxu>3n@3HRNpk(<5Jl474@KqOU{(Lj9uu3g5xgmxM8lEs!5WeVH9Kp7K{mjdq7HvnnZKhNIe=7dU4s1C^G}D)?i!BS1 zMkQ0d*i8=3rK+!~b1n&IyJ?#`E9>daX-MU(ESWVG@^fT9LFY~qLC30G$Tg)hICgGC zy^-yrE{_$d1$q40cJkkV+lDl6(K*e1Wbk*g&iu%cA*-uqHcpCGs1C!x_Dy^Ia-hth z%G~WC6N-1%sczAYUU-D3D2?kH6+=nLhi4fg5}A^oISV5Y&d0T|OxNHjM*yvW^Y@xcK4q;FL3J8hmINtTSM9HmK;J@_`6JLRnMFO% z{%W2td7&)YvoT}O$&obQYaQEj=|OpIQ6*#5v%rm1P~`9L?{VQs%PR7CJ6YNVM`JT; zt?(PfPfAr@Y9jh#&;!Or6<5(wg<}iy{YduuN~2E$XizI@n^5g4CF_q?uXGan<|lGS zB9pUo^FuPi+k5VIZ(F(U{mJ~4?{VEglEPLSM`6nfq#b<~%7Ai3>SU}U3VIRDXa)RR zBYzFk0qv1#=xy2B!n$n=LaT#iEH{FEu)cf!?JhsTWVly%rP=AmG2txY(LMRIg=@*X zx(Wyl{A*xFm5pu+YM|Yam=gyd5X&X8&Nzl)GhW}xEf4C1OY0(vDcSf+!>ZIlzLt8o zeE5db8Ce!-jPiB4ppu9i@eS{pROS}?_xV{Zwcc(q5Q7I;?2>33V({@9)}>zIFe7 z#tk5czMSDk4W&lV&*YONyx-5+ZKD>7zi4IA)y{p`vQenjtJ92WF9V0bfc4F3dB=h6_oLfqgO2r>bh%a4*2k)AMiy$kh=bh-`YnCZMHRtnlbG60iPPKe zs81*e6B>o*emN};+sL@$AHdwDFiueW-1kSbChKTrFT=k9+0Sv8dILX-!=J*(V{Qcp ztLP&7>pmY%=G=YG5D{DcbpGr}B@=F{gKAfW=&I9hsmCH_DEeAjdI<;!?P_}T&O7suME=IrQ*Q2L6jOP7V-EEnY|=;QpMtzKserxI2n z<_?0IV#^X>EbA+jpgO`ls9|F;bsWh{m4h6{8g#R4=b7i7)C&QqUlaBnUtxK3xSV%; zxY>DAEl+f|O6C8tEhw-+47hsX_K@@LZF7hS#gK--*^7Br%q{Q*O<2#s7BmiyJj1&Q zZ&K%$!`ur&lG))-J>$0Q6&?uC?KD-Jh!VKpno}L$OoZ2OIMQWjwfl!LlfZ5(^BhX3 zGU?3s+HvZw@L?#@5P+>^?vc}*7intw!T5FTrHt6aBEJT{ffG`Owmm0(d!se{cBpPk z_L^>vNyo{~nQ!-sCB85)_uXFW(iU#_g|{wP8(Wv@89l^~C$C^4p+yZ8Y7wL9dA7dd zaBk?&6`PXlXRkwCJtn%~v#D8RuR~eg^7d9=$*keuk00rrz1!r7#11zR;>r4~ZE*hS zjj3_Y`l7mM1zx6o-F8Zsv8{o(_Hp_#5pypmq@uJwC96QZM<-(AWyu!7n#odY0k!to zeQpdT_lp*t7=z527fX>?zK+_ithb6?#-K|JuG(6kZTKfX=t)l)TbJTxlAP-vx8k`ugs+$gxN*=o4?x zd*rVRVL4`{XQPWBqNd;a8z!G^k%r0nt@US8G`|sp9cf=NV#Wu{*z(K()T2o4!mTuR zHW9GCdCL$%m!o67E7!Ehb>Q1^pMY}L05HTR*) z2==$ixRuby$8@Nw?=ip+;cccvJ!Kt9=XmSUpq83}g#4^;5J|H)e!25zs4Cn|O{7)M zWK)CfB>JP-1{Ff5{hB|kB9CXhn^f13?&ZDN$z#*z#>w%BJXADq&9q<^A9;dr;v}0d z5@Dtcg*DF_Z5(Pe-&|>eT;JkERG1D0HGAM04&x`fbh*Mi;l)749cfc0K3rgyU!HIM zu{Q)!8kyWQ-hKgl|8FyH0eKjzA==O=Zt#Off=6$rQjA_yeFucw=5=IHsB(mvEe{>F z55LFRU`(8Ttq$1!4H~3LMR$*yYizcqQIx>Xh*GqOiw*b9!3a)=6T&OzAtkCxw8K!*1 zPh@VUwbcI%@=<{9sn!mNf4R55{&8(hnoub?uq%Cg`c^j@a!J=|+W|%GW^5SrgUE7k zFzlw!iSqNri6Yf`rh`bm4MGRyZyHWMj5qSz%!%@{^Io<1dQ5VJZhyqaY^MdDgoJr z343|t_+UXYUwWGLw+W|#d_#g~8q&@zo=xN#tDWS-O&!TKfF(a6+iCvSd)Z9b)Y6wj z>=glw=RW%*E=jG;Lp?}}btY#MrIMA%n8`uykE;RDRRxPzg9r`~sfgN%2ZrvSv3V7) zK@niH3nr6PCt3etSAPIHcIF;h^4#nM?IFXhjlA4>JqjT9IZm_LS$ra3FZyNCdmBka z#djg!`-2F`+zPI|QApPFfBVpRV;5+Og@~7DXkNwTqn?N5Q=hXD(cDs`flaO`XwwBi zC>U;q#jKQ*Xe;@OB>-wEWRP&x+_LBA*h5{;n;Dr>5nQX_e%;ni#sT*O?}hFzhah2a zija1!7-0PFf{!OR|4MbnR|zeJFdkoQwrb&L8VX){@f5sWt=esV1`q#2MaMcx+RiqL zCAml$w8GXgzGwx==~vWS&88=}IN-0Tx8?_wW+&&2mapQLSEl~`+x|7T-$y0gv&i-k zNl^P*O!p{!riB9Hc|#==U7#pXve8u4E~b?xCw%862;Gt(M&UF^wxyizls=Y2>BX-A zz0*K9u6}VzI%Oc1jC?P)>o#x;M4}4<`hCZ|jDPk8isf(xL0XUs1IN1*35}c^+4>^p zLWSvb6D(IsTZ`H^3xQH>Dj{5BAuZ)pP9Avmv&oLrx9Be47*{{LDy!jUP$@a^vKRP+ zz2q3lY944;o+btY3g207dELoVN{h)}uP|-~X!bWi zn*jIVPcF#fca9k+kkGR(XW$wa& z*Hac<+-PH8f!&%=uAY^b0CYE!%v=uhs$)L3e$*)~J9Q&CYt=&4B?#Q0{X6&=0SX8t zMIt|1PgMG7a^P^Mt^)-<;AK`jC_Yp~<6GTn#}x(Foz|8yZRPppz#)4=ZuBfFvf5X7 z?3l~;beXBP>g=83(NAsGEeXo}%!eTk$eg|R=i^|)bMWBU7#-NM2C3vGYYiL%yVN-L z5@#c|XOb)-HFWYT1`^azjp#e!0Oiu)d%mdXt?P2V5=N%?w!II7t3tzz1H^B}Y99Ci zIp4@!=_Gru_`h!A9$$WPO3atBCjck4=#{1fL^_BuF1u1?)s0gNg9(F49fqUiz6|F( zC5)Fc?N|(-9O-U093wi$lh1H&e6VXb?>c7eYm10qv474OxK~VBGV4~S`GNBvB1)h) zAUF7xmTCh$cpo>p$8(6`oB4^^8-SpC=ia95CGyfjzJhT5v>a(6YoUFls?N&tZ~Uny zmF6b$_vKM%az%}WA>=1OV*~CjnfCh})EpAX)oGB2E~hB_hGNul%qM-q)H3PWCeP zjmVT-S?YBRr=WqrCzQ>o+ppe7%f_bN0r}x^eHyvmHRixc;JQU0vNEIZ>aru~wde4P zWX%fW&kFtHcYi+d`)|l!yBFzS*scBB?!nmq*l7xphTrjvoUH?(eOJku+E8?Cu+2d;*{hU6cBKeT;&`6$4`PoCv)cUKai9dO!P`81YHuZ^C>iWsG3A z;0}ke=R=cqpq0%xaU#kSo@alJ#&Q026|7C;bC&U;OLB^P8K%$06JkzM-CpyI6arx~ zOmExe+``@YP4uYM*X)gBdP^tEHYTZW1TWNBBZIFc=;pS`WYsyyfVk`i4{?EvSFYf7 z4CWOyrLIHqZJ5)udLe>sYLu+x5{yU1Ab$ajQvDU<=Ifpyx_+dQv{~U3$><%@K;QmC zvmeF15a6ssOo@YoWT=a`rryAu(1+}*wpK~p_Y6_9v2P2zi3|`41@0&PXPg^ zj##Iyl)?@T8}O$3eqtB9nfJM+A$MH1b7XpcULpcAV|I9IE);I|HyEU3oqTCE&xuM} zq*!uSj>wkWk|#X};RIz)u(d702<0@Gbfj`|#uO6%Hr*}R8(TTzwEJN`P}R_7lUfat8$516uOqbuF;nZI?*<|M-3FZa9nHq1H41)=lSi<1cr*_2;+}kW z{20|0XVL| zz_ijAtyO#w+98^Q_FxoRD#c&rImj(rr&du(PV$e8C>ao1)c-KlOfrv|!*)#LOc;^j z)!j@Yhcdg;!FAOfLMmF32MqV#0vh%iRw0)^_!uugLc9WP;~q^KF~~z~+$yP%G~wci z%hh@^>Loedr^m@u=CO8MVqj|(ddZ8gcvT-5De~=(^toY1+|_m3$d`04_|D<9Vte)h#sWB+0!AZ>}+|=RMmwwl2#-jS$f`g~{-NN`sT=25G_-rsu;Db1MZW zk*hOcYPMltmw}(#kHd9OZ@7E-c88_nMz>0+HLd9d0oeR?A7n3PSuuccAtyqS+syiR z*K%dEjre#6dJ}?g58fzmOS1D=z&I;%QKZ}|7`x!~7}%U6=cuQOf@kwZ$8I2kS�t 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 Jxq$y>{|}mGggpQN delta 302543 zcmb@uX;@QPyDb{*P!Xd@qaefvL`6s`ozle~5iueP2tvxTkS1bE0f`W@>;M5FWdr$y z527>z0!D;@(j-D?N-3lV2}(LeX<}AX!jdG5Gi#r{@45GR_BlW9bABv%6tdP_bH4K( z;~npq6gRIK`*Y3JYOSNrkiPfsh5uYpZgz^|o7}aCXA9j$A;0y5vY3i*Hg=uE+;SmG z7%PM$<8hl3^T)H~F;MA5T;fPOg-j+dpqSr1Bz<^t+0>J`2zBBd=K4?QjmaEF^oXRS z#Lb|&KLRkw#2f})&ILc)@{Sbj&y#5@7JZs}`qVmLj4y(4t@RxyNwt|`+f$=^#7RjL zgU54ZU(cLNr9QAYxcSx47ien99A@Da&2>oh*fGzs7y6xbn+HFj-V;w@;=z|{)q1Ox zSxCExcnw<43v2w1-%il8PlRU@&5>&`zkRn5>w=GcN?gzzK6_G_M)S)LuUnoITlRi` z_PM*vnP=8$S!_sgbDLzsg79eoOvJBqnEQ{Rro(fXsW}YqJjCQZUAQJf(5{>T6a0|g z3c+4j9qPVT5=YqjqG2J95fdKGT8zZHcTcm{AbqDDo_V^WrJ-WnGC_osxI0>I;~cMv zl&$irX{f8Nj%T>V99Dzrx%C_prMeQtbhYQ~9ER_z2G1P};N@VMl{NE?p{s<^T9LdB zpG2zG=HnL9&LQ?vFCB>yA9pKTmzhE{m11>ATT8-?-`-0wLmpqJ{_AJdUa^cFxoPDG z|IvM}x$l2@-?YthJVvdU!!(tVzTIF9V^O}ant@h-Zk}i+l2HiGqN^@&0ruwv@+KO0yntbK(sTW2))Kd*1MdRMoNArl zb=D1eVee*ipuY^LPR?N#O+arVAl|P8)z&4@Vf)|iNZ-PN;$!95{7?M8khw_IOdLN94C;E&> zHnQ3Ysq{t29x+xxINj@VQh_B^!|lnGhIqJGg1g#aZeQU}i;H~9mdICq383G|w<);1 zp>OJIjOPeB9RIb#`pA>-6?Amo>P5#FuUoO_uXW!RZ@j+v)ZRMiriDaG zS+@iZDPGGluR)H@zFaqiEcM|f2bfism3vv#q-m$P1m%xsZzUVwA#FU|_o*Qx z;oz4F&Xd=+TXEm6YZ?e25#D*8%1J2LIEV3--?p+;TF+tbwQ24_Q=iZU&_pj>fq|Zr zn6GwgCAq_H#xzd^QrvrGwgM@I%i7#QT_U)@!TFa^C)Mj{mwXwXj{<{8^keCDK3@1M_MpWpLeM+zy zst#!i3DZZZZ>|!|)#N#hHW8!`Bbu%ht53xr(-btEDkmXC(tz4I{s{v4hYmMz2HiNQ zI@y*P8WnjZR{!#IXpN6Luxv(->c)ViJxS*iUWGUBV9kq9$mdbac&lK*h&8iyEegG++HY@<8IX!{1u= zFs*&m9N~#stEs=7&l>eQ`nIddObf1P%du9^UUNr(0V-qdowr!9uW%1)t_Ho#!+R!YnR~`N!`I3_R(Wp4~W}P@zvz8<9{3 za}HylqjZ_Wd`T!kH>87AnViG)d;=I%MQ>+mupDIm0StW(1GRYb;2>z$jier=y!$?f zd8A}8&m7cRWjZ9%)z?yK<}fP{f)D`<@Z~4;{S@zP7n&>}FzOiJ zc*j8E$`!&?o(zJ8ZF$D9pIYB>FU*jcz&UZANVAmaf3l|MC|1|9Z#S+ zOfvyi9qDmfjg0JtKb1#$Vw0w5x?bZC{4X9-T=K)u-UMLc zR>cS)QYKi_&1k<6_9LvOW@B&qjjly5!f9g7kdo3uOb>R*`1XBww|#wu*=L_#nseco zIP#_wa#QU=cK$kP&ok6WZGp8Wo+)}<%&N`L#~;pSJX+2hSu32*(- zb=Ne7&=(bV@f=26qWCAFJhp^3^ND>@FYsqR_~dwIt1c*h6eHt-&@*+k~Gk3sm8eEA@+MFbMBDV zYAqZSKCWd7{qM%3K;&R6Ma*GtT!zRj!~%Eq2Ql4kqayYO-9^2yk?G#k;HhwY6+kb2 z$h$@(RZPSRF6ZVWIeg>(TE}PMaY>yUT=6YgGgSej5!toMBQ;s~0^RLD&;IoLgi{;2 z?F6%TeK*u(zJLFn!fS*OVakrp(SADSaleLrJoZZ5bM^N#NxqoU1?R33S~HlP|I%`X ztj6#oWQpZ%`bv5mfz#E>NY3V|H;xjJGpg-08j>uEgj5920p2t984)`%*h*oU6a!6F zTFJgs zza6jbo_?IKk$+)-y98$L7$K}H!)-oz(>B@?WS5wf!rj{EW=QX$n;@NHMtbnP_)q<` z?NXVoSojK`2I;GH+=`1P?gWnr$fGM2wTaaqJo)I>UZbOmgG{J+l$yi$t!tqhf$`Zz z#8@lOh)tlhIv#&kxZdf83gGq^MkBcD4 zeY3NCy*;3(lTJy+PbTi|bEqsk-LbNNT8C3r75E2sT9(giqKKh(+-=-K?s~*l($&mP zaoY~#p%zLqcYPo2q)2EKX8cL^wBq5Uz^nQ6aIoO75NhGE@lU4(K?hvZHHG)y2}e#3 zpC(c)RTXOEp=roKy$WH+RdNi`FK_9VMT_v7^{>-!FamxbXC zFS98ddPjA_Vt@XzPyg_(hi3OkPV^x{mHRc2tj;rr3#5Ki^@d|BCV2!j@6i)$~8)a9^r)q1h&7 zqtkdk?^(706$r-GVWm4%=(1~?hv>)^O^)ia+Qc|k@RU&1I@8QDG3g2U>^Mw+7db7~ zs{7`U@UVHore8<>7at^mzz`(@I=UK!#S!%d#g_LSXF5` zi-K6S1%&`U-IoL(hGr&rj-itxDOr!e#)sOca<)TM!xWt!)7C$x`% z;gRt=YFoG#gs&M=AjPJ-tWr2O#m%HnCm2a6Eu@tbs1>2pgAj%t+1SLjMLa}v7=!UqO9ka7 zx(30v;cVcjN65;C6=zl7^RE*A zO+@fG^@^<>sE&3#OqLL?)9st!Kq=(j?7BnMWK}{fIMApdT#Jz>#}#Rmjjn})gPHbO zr6Cee(>Re=XbC^oqM|dsvz0SY=h|4EuKx0W%IwHJbV-kD1!9CQ=~S7hjWi6pDUy6z zK}kkejXMnqXx@@li~Ae)Ng!x7HRqIqcbmhi8p+>RJRIR>r7*0Fc^rQ6!9ixR2lASc~w(pI-@A(yZXIH$TEh z*Y>Gnq>L5F49MuIU#+VVS;)Y2z08)MeKa>Rp~9=gv+x}siV80>85X?`W;Z%q=H;_0di}9e4VanA@56XqnT^xTm=nl`@CXCA`*Vew&+3FT0%Mg>QA+*cW5% z@Kpeq*wOy;_Kip1&Nk1!{ryFC!}I#ux{Ub1`csmO`1o7q9uKqa?mGG%ID-zbV0Y*? zvyrL|fG_zIYRhr(9t4*<|3gpB{pA$|A6`7p2f{T>U z3lxgii4~i16#d<*EENL4^xKQmDiB75_xJZ zZ#8O3ABYTntUjkWni%SsR#l{ zVf+VQGl7X<30x3{&_x-_Qzs8$a2vxRmP5h@@lp#{H& z=7*FjEZf|ikaUH1ixV@oAw(iyL9QK&<2JG*W^`i`pxDis8@;|c{AC@PfBj9HVCS~- zNLjf|be%9FL>xku$;bz|57k+y+J;y*;a1U3KB5w&8I2NrGRd%EyV*lZG|m)p!_B9% zTL!azUlu_T9`PaOb(b6S#|2sUo-{2pZoA_WmF{?~I#v18W;bC7$T9y};Uj!>sk6ep zodu9n#3Iz<+MvWbb)G)*0M1dmYx23~G&jY52dCg-$7C0>hO`w&@_JIn2?QVGA6yVjbj=IUZdHLbe5mqq2hs8+;@H?0GGDn-n(eQZl+h zVaStsh58T0Xkz^va=ZtoE69HR_zU=HufP-iHa2*U>A_h^e8I5}P?3<%#_h+?pUE$Ccp7vy(HB?uLFv{L{S*F-D z*gpv~5<~!cVp=m)6PKXg5gb0`6C|*18N4&{QVmL&8~Z>uCZ^{Zf-P1aMp*EJkP^Iw zgLo;b#FUnw^v!BQwo0{+A8g%-ToL6`TF}J~cNI+vyq^vJ+BdCx z_JE@-e=@F(1tq|i>b0h-{j@)jMzCQoX{x5^8`Vb@p(#J)H%UfnA2?o|kr`Ez-N>p8h~N!R?DCR~+A; z+7@{{-nsZ$RkmbeaLEPfHSFxNrlo;Rx(!#piBfl-eSc|W!|t?uZAS;z>|A4yE{7I^ z$zf%Xs%l#)T51ZiMd99t!-8yMt1w`u(hQ`!*pv5#VY7`XA!Z>JaDDG|q}4GFbXV#* zL?cwZn{dM^HYLZGgqf}Dvv)P}=01BdhoJ^JowDqk3eBwTu+Oc2?lAA`-9z3VYD$*w zX!-Z%rM6cwlB+h}2j~YtrFX?`7cEee!xP;R*6`X0*XYheZfj^ha64b9Gd{Xn@j1!- z`bbNjG0~Xj4dbratK`@S+dVE&-KH1l8ihj@xe#F-QQ7dsWva9Uh#!}APonF5WMJDX zLeOS5yxz@Rp`+KyCLd|J>p#J4xqH;Zyz8#V$9u2sHHF~mnWSn$+mNP)5KbD-l7ei` z=FTSzpLeBlorJU<{ZIRR;3ZPvBJedv?Qu>MD^HIi@c|ICiR(P1CP?OsRgF7_ zT9w6Y_m#_|{f6*gzuH~2E{x3@tgC;CPk2{_F6#y0 zjuXU-+-KSdV5@LuZZ;&qlj6|ZKZv(%5oa;;ntGp@v5Gu9hq)PJ5){ifB#qQ53rdFl z!z}|VtM2)Q$t>!#-|Sjfv%R5cNY%!?dt56Ha&y<*n8TdqCC_1=VW-mJeahX)nF?xh zOWrbMi#SJS+|feca(8%IxMD&8wB~7^F_j&}&3L=BVp~$BM5s5Jo&GrUyqT_9+t|qA z)cE^VfhqJC|Dm_>n!2QrY`tPcc?aQ0N44m3vlT>t>t*~lc zrQU!zgOc}gF+3vvVI&+zjp4Qmg*N}PKzMt)WLxwX8OSiF6${l4&8onG>^aOy2ScX| zcXU4n9L+g%_PiEaRPBcthtb^>LrL}(xPPcrahi*(1j1t@vgt1$l4mV%anh{O=V&K^B;QUzEGbjY ziX2Jptzre#(v=E5FMh?sQB|#ezHt{=Q4)d?QYPjlb66|Leuhr=S-C|4U8NDRv5uFT ziXs0~0(bNBb+xH3>oBUM7sOO(dBZNn4KbsIax?Fjz%iK7il0a2iHB1Cj?}4^7mlt~ z@RFyo%O=kf8@C_XCeL_K`a#FDU>Prz)xmso?_A#ppbmYGl@gI1M$!dbykb8K3tgZj z`EuvqYcZ}z9EMtXI_gtVk{pUoqgn<%YLZx`TLvb3vI3L69265#;R^QS16w!r5A(k@{q)@i41L$TCQu`rPXEgm;?Td$b8p(euL zTjM^bZyI9TTcrmJMw;kb2FmTMt%xe~vp)TSCmUa!>)R$9Ltljflw9&Zp%VK|#oX<$ zwVLUWskIhzg+xZL7^$Rcc^fGi!f=;T1LSQieRK;oW270?g+JuD>oaF>D>|d>?h$Rb z`PMkaByz?|OGghkJG(l(nV2|IXzr)PiNPGWE7=1yytvV)l~fb^injz|d3Q&r0jUcK z5WaGAQ0SYP3|;u4iccr)xzS=co5x!8(s17$$fAv9#$K3s-|22(#mCE8<2ym&2!O#w znn6UY)aMTM{7rf;Z+p4XCbezXqGFMO-uhoJ@AQjV_9C*JzwlDajnhgl408Okbvs1Pf{Ru|FD0R0gk7_+95<4a@y}fu`x;6ncx3 zu+O_MGG=FW-<2!Hfk7jSInEtncEal6oXQyN?5|*rMF%9L zS4?o%Dh$&~+&>Dx%zjNyaC=a;XFT@V$De+2^he~2;ZbHsUq6a@-TIr~RH%CG%C#4v zrgM>e}rPe1a*Y!;U5U!= z{CUJW^m1e^Qq$X_-+@3+}V`Nn^URZ5t@UeGdNk}hc3{`18S zlH;_Iu4clD2Kxe}>lJ-7Jcmhl*(fpDY;UdIYGyEK2Po_8Th^uPp7An}9lqL><7J`g znUXxgl?^fL%2troI#!yckmxT47%l(;+(?0FIY$yG^AuC~&cf>;3F?YFhQHdJfroB! zEjkpoM-(Z&EAny?-Su-nR;^D&JIRtzH2ceZwf{d|mN^UsiX;$)imjenTaeImjEL)dr?*}1 zox@~kr$M4L9_-6lL@jF>#SRce{}$F9EGVV=v>10KP3!J!CRNR}Kx`AlcUHBp|3Y*} zeb+LiuL(69R9i^o*!Nvz^I69fHNJcTH65z{Tr&_IbQHXDeN7!x^MDXWNXsICrqL@R z5W3CmU5(`}ag-*)O@bcRCc@0@SzS__DyR~nh&st_esT7>vX-d8;SUp8vvyeyGd``` z9KIjD+hLLq?O=U3P=u<@aDYFC5716Zf_$3q(BB5Hf@_K-y61q@NH@$8n`f&>jk^I^ z%t^=XW(L(OEE-2SHjLuelwyyv(<4n6OE@nZvR4;AxU<_;it644$#Or`wBOAX#OevI z9@i)X;_aSBVdfuk}7 z8HWp$N0Cm|ZrWDOqZlAT;IB21R0OrNqPDeO*Wt}jGuI4$S|IK+ZimE#<)a^{?ycGM zqDMsA@$AASWT-gkl<-SkM)8t*2UZ0O-YrCS#VR{AFF`zYEkxT`oc2We8YSg|cn% z_{5)%GnCypNKys!(8YlBEDQzZ8@7qJJX(%e%4LBfRo5CM*$Y;?Ss{;LTM3EJNX|1c zPHg7eyp-bCQp@yO*%{_v^QIbbwC_!bXwt~5jL{JiLxr;%W zSLnqj2;o&Q7OpA2SnT0aN1IJfR=XC5J3{yTJEADzQP%&ib-3$V&NR%bYFL(T-LS(j zJzlUG!@dfMp5UsW7DzCIF8QDeLOV%|{r29I#I^zups$B(uUo4s@i8tc!t2*T!&*OYi<^E-2U$sw|2s{-QYfDQ0{3A&C;>Bs@JWr+?01L=nELe# z=twW|Y|O<_yDVH4zEyL)^;1oz_|DX{t8H^^+~tCZw}}<#B9+T^sDZFuQLICOpTSYNfwX41Fc=wuDp!`*G5Tg(1_4~#G$ zY68fA41awNQ_GUs!>g3P!onsYShuBsZ=~^7(6vSaso$ICOSDr!9ZI|joH5$h)F-8B zji)CB`<)V4DKWWbW731&krhGFoyJxRU%q_NAJTa8PXsr`20h7ZP?HByrVD3uDX+rv zCTt3%&0!dao8c5gH`MFlpEPYr`o2So1=0g5-y4$5LC*d@j@JgstZs16h4VJso8m);2a`s_=e3PqJAi>wc`b5e14w2pap*t7j#ED*VzD@} zX>yk}5>rq9cWRlK@v18YTFEs-7%Dfhm=Oq5GF+_0>HGF5@X4H=*Mr0BGNHtt@781c z>ymoEIj0>W{050W%wdp?pVj&hp8%f_4ojuAt&Cx3DIU0j;9}*K0ej)C;GGIcrUlE} zgiE%?ID5($BaG$%O0vsJqcj)i&=`r_kTenSW7X4~qF002^qkQ6gz|}i-=W`(coT&G z)wrh>sbXo69=H`=rAHqUR;ci=7nkli*e zd!XWQL8B%seR)}V!0XZv!{Gs$1C808+YF5^<-eN4Ncmu=G#L{_<)g;HNVPnI03XGU z9Q=H&+dk6s0QW^VA(WDWT6GsU^s+ZqzJziPD2Ry-yUMAdE!jt^cFP}6qNW4WUON6& zVKJr~y}5l~t-k_PI$sB}(tAiW$U@A)Wo!3QBWDRI5PV(zTRqrVpbj@FYKPYxj8$(eV9nr$rlEVtsINWCH;bYqoZE^mG2j zsqo|1xp6=lXB3Do9tIo7nu#t=R{7G1NQ`Pdje<(E;}?R%Q!Jr0u~Qo_FW8&tZ(+bZAGAK)9^w^1ZBk#}ovH zORR*lWMn)Wn;h-A_-TyJV2Zsi#oKVQ5DDEsr1pRczhoz?_P=|j9Q3{1Yk;SaDf91} zKceOzdRMyMIMdkD4Bx7;O~>;?<52`^PxZ90?@ zQ>~C^OcBHzVEN4kYq;%m^9r)@@4@<>X@>pNjFs<^4q~c68IM#%k2pJzFR%^ryl@tS zJ|cf#h3eP?FI9l1h9~+LkJB~P3UBJ$+fTQl9f8fR({u|EMnEz@`y8(f!Ar0=Aap%W z_wmcvRpWK%DkPrxB_G0qadm@ZeJ(E9=%dHjm1M42_uEHL!{;!ci$EcC6>9p8@WoN( z0D8C?j5$p96SNHq6(TlWsSPn;O4lHuTw3_6g4vY5V@5)0%C+@{DYsDEcy{0TsG)+{ zl55$Du0Uc!CC;9VP2>;Is^+@9mr_@J(12YuIZfqh5=12S$z9s3vVXyTk0ii{aLCr) zyT#}djwuy{RkZI218z2piq{5$GCBnNKC{K`vel zWg|R4w7rjQ4-8^o;v7Mm6#*&Iof%CDiDXihym@$x=E4Q!Hu zKS3%M-g42AV0G+AAJ;PbB7`e`Fjj=o*Ht%OyE|$cb+kY7_xXpje!)&TFtbCosL*V+ zH)$bw>0fw}TX>>J!e^AJ&#JRQ$Ob~P6SECkI?2rbe8YNr)8Mvf%1Xqs4c&~yNW6<8 z^bY8@%vzw0VqNzW-MtE34fjrbNF-!F^sLKPGroHvl*RzG3|UW78{~*6t!y04oTs_T z3n!*jQL!z4*=wPdZi{JaAS6fOC&p)N`)%0mfrN)azVqpNIm96s=cnUUf(YO6nB`1~ zsrh@i6p0XvP+9{nREaf?nu0QQ9o+$HlJa05oSxcGG4>M1^M&)>R;zJ5#lk+dGmK9~ zP4O8GMDZL(r=@aayqlWVnqD!UQa;J_ZjRCQjgNF2d}9JlzLss(RY2E1goQR0#H=p7`$v!wGS1ts^n8rprn(+uDMHab4 zWrZ#WZjc-45V;GnES6;!qkr+9`z3NskOZ}Xh_DPuGTIry{1%0-Ds>6GBGv2=`u9Y4 zVhoNS^Y(J0|gX`julf@-pRHr0CiqZ3XesKhOd=gQz@j{a|&%JkZqPsUiY} zr;rF-juRQ9TCm?r|OWqg|Y+w7zd{Bd+MjK5k< zSPqdHk0u-i^vu|VqFi_43zfl#ww$xpEhZN6{+{P?V?F;2Qw+g=fu>}4?%*d)4o7C( zb)xSpCsLQbyym{(?E^~evZ2}I6Ir91vZP*4=}$b*;boF6vaO?FL;9$_j|N0e({Y~Y z#pq5AV78(RUKLn)BN3~)(}9ozK{ZJ^@P&49O`6oIe=r)KXk6PWyvbdMyqM+Pl11L_ zMqau&7XW))Z$&g>(I@5&JT1RqQ|P(*>xMtD<4040Ng6_K&qsAu&SCcOhHK|A%c3Fm zfZy~u$e;wk@;=gzX&V2T7f!8AS9K7i3!Ef*%Ci7rwa^{75Io;qEW=8kriF-zhU&w; z`Mq|Y1@t`q!3}*4+oQyjkK=P>ZsEoq-iLZR-yhM^IC+yk#RHAc#T{MRT88jijoH^_ zz@r=V=CAZXBrbJ^GO*pNu)eVU+n&>PXZQ53oUef#uH~Js0dEnv0eF6om zcJUnM%?Ymd9A@~YdMmKSYE;BUL*I-@L&eOe1zM}uV4xEa9Ga{yLZIOa+W9$5Uon3c z2)Y5xyyAJQA%kyFn{cFEX|Bnpd4`L+1RIrgnrw6twQjaub+n@yWC*SUZ5NUt(W!8T zGL>KK82U(>Coik-@1HI|uC~^H`j9>A-WogO^pBCGboiNPqx0@Tyex=$siHM6_u|F* zJMvaYja_VN_R%j!k%a;N3XD5ilJ%lN+g}y!z6WKq1jFZx*|B~$j2VpiA`AKSuf#L}8Sx61&-Eo4-fDppcIIC$7u|F*TFKBej|9g>6%0o6MJt!qZ zFcDxC>tIi{N=EV@_@`eaARpY;8>lfVf(5Wxk zMlGEOCtCt;&+t~*olr2_yA~mhcC0n08}gtHeazP26)^kOV3+qzGTo$mdV9>{fUESC zP*Zup!HQC-DGE9p*y-%qPnl8^iqPI{ZewgnNK{N@ez-0D4bS+;>KLzJ>V~`4jz^iN z2Y&<^T8*?N3A_URdVhSnKS~np!_R*A`vwAT!EP{KU|VPcDKHGr7=E=UbW&o_i7mn=-B~2Pz8c zRK=o$0p-4RP-bpxdP`k!qhhE%U@W=RePRE_LrUYfX(i*c+)V0|$Z1`h*|%t~M}^Iq zOw=7&RTPmeZU`g_^b!L^&)V@=|owLC6+R2)`vP z<62VP;>*}N)X&X?Wi;m^Dg&TWjiXaY=U`?@EBW)O)3487iMe32-pVfAc6Bap8HSJy zQ4rUz!vlWG7n<8KZGABddp2qQ-9Rklmzy}>De8CeR4zfa@Ee%@BNAI3h%sIIGOn4oIMgjciG7)}aWls3Rr53QIm46sCKT zC=n$!*!jAy&CszvHfpJB)Q)b0U_x?(t2oyw+=MSL3r6zasQmjW+}8Z??C##_2X>4x z3W@_2#iHCfOs@%j6A(fy(Kk4azP^^m46JEIwr^SeB?T93K@j%aj!X-@9z!469 zhPK3_NkTQHHNnrl|557L=Da{%KhK8h{_2qy-&*dox|#+KlofcT;12KMo1g8<1BhJZ zN(cB+72AwYCp>@LD3-5KV-(Dr=YK~i7sY7{dwzVda_>EoZ9Zzq+`VGNLtGKqY@C#P zMVubXH}b8i<(dvgm&uHk0-drgXGsMm7&h#W2tmjgwLxhC4z-VkUj%Ir6!>VVN9!}T2VA3z!zO6vq8Q2nxgI@Hx>?y@S1~~Xoqvgp#p6H1X_KJ7K9qG3x zzkn_P0%7Z$J#^&xOWw=0!`*Zz#3*PeX4g7S#wj0SPqdfwu|a!sn>VF6+}9{O@#CL@ zfUX_%_h3Oj3Vcf=!Ki90&J04T;{fap*7)iyEV^36);*DL>G>_NwB+ zSUw>YXjSvnddS5>YEDb=#`<2_dG4xlGkYXTL6ICRD5O@k&*&I1k|OORoe%WVJf8&c z#a2NV)kHQmiVp$OGreAQ2yG)gPnIPXBb39M62S4;i8!-z7t<@TkeEzK8C~8>J6^Z& zlX}CbGSYf_wfWud4||{7D?eJg`Q)~u%tHWQmm1Gu!imWGgPBjlpnuA5%Ry#EcwN$+ zQUOpY^i@Z5pPYIhJ~N2Mye+zjWrfGC^;>zR<{Q-N68t-PY~Qv^kG%&N?q@)H@CI*v zI6QVRAddz91#9j{;(tumlP66|xGSl@dq!Rf-*Gzd^g*vj^A4R$H{ZKQ>;CV?8mF3? zn&&mORaLdud*AjhuBeEN%+JsvlfyRpKdi02ZgJ?*_0@OIrfwcMn_++Az)}omRnGzM z0edgVd4He1X{bN9y7x!=hBmp+K#c2NiIu0W^U-IucfZb}%8AQ|A}g$|^RpyllF&ns zk{s_IJaK&Y!U?ONt_T>Q_8q8cGk2|epMrVy2YVj+j%DxzRQ$+|T-8+*o7WLVgx3FAo4`#P8G`-8EU;j7S|6MmvDwSE=4d^@;*9Moh3aUH^0Rkdv= zkRAm^x)RpZrnrQQgu}vSR7;UK)EsXeJan|bVM~Z&$4oo43LId`#@})Y$)8vPd@cNd zps|!r1tS;M>}4kZa!^Z9BtZv~Qku*V*6 zO67~)n_f0ph&musKku5#dWB!=dYB}`O0law#d2(uOLk2aE|F#-0kNGMeyPqfnUi~H zBzy>i8yql56ho_!sJ2mKSbke=`gGiFGt5Yp3+-QUuGe(0vhQfWO~tm4nHOv#r|_0Zj;Vu} z85(VJ3)cEJZX2nTqYAgAI_?8#J(Jj9@Vx&_ z=K2QCr8cWuHHQoy=;)t2u7xho)JV`bbbZAUAq=Ec*|kukB+kzZ(|9v#Rf`~|JkBA1 z#t~inO!JVtbi!|R-SfoA+srtNtlYwhy2R14d9Tk>LLLi0c>VF`snaf`A%c1r7F_~L z$3tJhQ8Tz3AD4|u`Tq4SvB1D;LK$~egr9;11rN2yg6^_R}u6Po7>hx9UW)XA- zjN9f;nHR`{j+*?KFVT$$gRowjn>45}!l~caZ%4R;F;r8l+KqMsRfQQ}QhxC5koN$f zS?r+k%RZf^otm1IvaWFb%JABBY(E9uCmXB~QRgKAJ_v@L;kn(0@Vk82efcH&nR+G5 zmmg!2Np!hNWq%n$7Xvc8PzOXgfj(&L4lI#E_=x;Ea6UYTQfc-j0_u#!@)bo7QrjU3 za6vm45C5#`p}A5NQAhf?X4J0sI-}bz8y`wVN5#LP-&l$|wWAQgp>xnx76TY#AjyuA$Kg(7rpunD()uOtLEy3;nJscff zl4t!%huNeD387H9`C#oYZ{{#{_7YYVqpj;2=#Wuf@e+k5gmYbn2&El256B)w;s6&~ z1DYVd&&LJ&=-S%vb|pXlvJ1>{bx`qRxu*4fUqOcfA)f60kxOn47`#_PB?tD}RRkVx z?2|PZoF%Yfm=hMykV0)}5rqSqFLvKtv&iDDtK2DJ3$-bw*SM}R<5{V5N zCiS{|o4>B9pltgJxX|F(mUVe{nQI_-)ek@TfeJ)_Q#d3VvJwhnrFJ|$o+xjYCnHOd zhBK~=G(gz9l0mO_>L*AM^JyoCevDXIJ9`hQZKYTf8U@DPqLChtbVUeU=-Zfl2Z3J- z>&U&+@MwVqrz;$H+c2=3@FxS=V3_%YjD!71h~@9bM{}SjM6!xm2Wf4GiU@G*(aa~O zpr+rs>m&TKBQ=k6%K@aXabQ4Ep%)kCmFY5X^tOEqa+vV%)%j9^|1In7n5SRny1G=) zD~CHx)}eqNitZ3#UlQVYLjuWCfLn@H$(jnkqSB{#3-xHbo*?^Pkyg+*(3T>2xVODM zc4lUFf4kV>niQ7=fu=fYLGlMjcv$BGD~yf~mjBSTYB~Pp8})KmAgiVB@inn5cJ{2M zkOu&ISX;?=GlT?8B?Z}|m>@1lKR~l^Y>_VvLpQ&GzeS9_pYRTkHBM}}7r2i)m?g8@ zVtK!7#UIx?Y%x$`3=|K+LA<5Z#~r9H9c)j>Q=>_)n!+5$NX zDcIuADRezrAZ(OK-=N;1NV-3ZOLbWy(HM3F!fh`L-Tr<@8aJx+2)A2$Ha(xTZFTU@ zkBN7B{6Qn=Bt#{_WJVIrBLSespWu~-P4L)B=8aZ>&_O`Xo2v8Dyiyplu~R}t@L(i< z&$ow=Ea9+Kg*&n4ZaYzR;0;7f2-Vqd{xXp?I{};y>p?7TAdH*ho>Nd`oG$duIzbNm zi73F{(JmfLff*u4K4B4UBN8I6;ojQ^6RuX9;FeRPyqL!ua*p+Hs_BbH9^E8fxpL9E zwdHT)Q$gI&ttC&iP@QkB+je*?>uIpvE7z|)oWAyshlxJ*<3TOxPR}*JgIbx_7R=jD z>ULKf_W;VyP#ZZbZR-1ejHo8PmUbxb4#aenIa}qFC75_Jl|IOa`~#&CReSXBj1THf z2E4sf=zisL_XxN2$#{*Q#4)7=56aK=$3*(hdY{V=tXEB@e7#`4wEk1($b9>+xT@Hxl`2EDUBF8L zO&J^d-?)Y7{7LTizNu)IJ{h-6ty4ff>oa86=pc!&H+4SN_^j45rM%+nQ;QdYlwbDt z8Qk3;o4VmD?sN~SORm;Gi!KM~a6JZUvg5kERyja4Uk);^fFx~q3Ir3}T|H1JYYEL& z5y|53n%*2Kw}Y_M&pwLJt)!&OHFV2?=(2y=zlio)J<0TS@@mZ;t+wOItgZ2&H~<)> z4V2%e%?*%Lu8nLw32V#l!L>&fq+8LQi{uh~3nPi3ZLOX!`itXSB7?;16p39oLI`1a zV6%iYUPmCF<5A?Qp_3ZBE!VnBv`K4Tj^Y3lgR~R0L-YE5-bTWHc>_2Jj zz{#{Osj}zce|aBkf%ox) zmS3@@+UzCxvkTpte?wEVF4#HD6K!QG`@6~i>#qhh;*jk;)SZEB!D&)i&%4lE)XfhL zB8@WCtA%rzjqRY7i?$MnL&33uISg*|e9aQ(llu$*fRI{rsXMs2Ow$MsUUWbaB#k%M zi4704XIc>ktZD-4DQZUjE+)2Eucapc57yp1s;TVT7REwFMTmlcf)HEKqKOSi34y4H zNFheCK|x3jRYkPO5dEL5RN1{ zzU}Ycci;WS{k`#x@r~~fWvCcVv-eta%{ABDFGz-rRSH%T?}s<0DxXf1Q!tcf?>wc4 zW_g5r?W^nLrV+zp?DArNrd4I{Y4NEt)I4nd)HC`+0a-z6V#7h;aqD?B1#avjnyKQ{ zUNBU1jR58m`{}*{C(#R*KAqzWPbE%9ncb^$(LILjB^lN^;8GlHtq#Qc>rOBU`oqhq zCdWUGE+4M-IiXUHG~Pk_}@Sy=$M6#BLn))Ua zNsmWrC*#XTs-0e^ln#q~BssTAMohCyd~44a=Y<{j5e0t(?SH`U-%9zK@_{D1^!&uxbO|)tGc-UEoUpSkTXXUHIjcm9q0R4Y zORu-LuDH%ID_&ptlY30aW#;#40$Lgb6{+peTQWk5)A@|Zbf=sLGW<$ES&}~RXy}7k@}cCj zPvW7YNqZ2HkhKu?I+=0cS*D`D?=Z_tJd^GEW!%oH>iJdOrptY;4yD2ie~Xr0uc}+C z@?^f2F00!eEGKEH8K~_5%P7n%axOZNu@)h}=--Geyq~_~j)OruvSrRxLonzF)Ffa2gqWY*^y6d(M;8&uK zf{IaA$%eVXgp!fkrkj;Z50CB0sGfU&r#H>yOD*VK90YAA%Jg5j1QH5)GYkQe{QW*| zxDV6w18np2anL>O87<7264a9_G6+yk^Hb%H6U0k=5(PKK4 zJR|OLFpp7Dx*foe7BovfMd7u}3j?p*lw}@5>;e*$??CuBPZ% z194<2Qq}56zL*9Rhmrsjxle5ZwLj_+51qh3-IGs#UIQ?S-y)em4rG)-*jN7 z8sXT5NG|Y zc==r_9RvaQu$Y*Q75EEvp{7srJNEY@HgVjO7D zLycM1dR_{akTs97qm%1jP~Pc)!_x(Ve*KCr+L`2co*}2B6t*4*uYMZ+!6-jA_R@yC z>lW_Yb(yObUFG{)DTTOG<5{s0w2%U4W{zZ9V?) zy^eK9S@-@_(K-Fz_0{6CDNfm7VoyiUs`GXmZ*F@3V&Pr72%0ees%7~Z8N&V+HQ_aT z97F}5janf=5cW5FItpKzgyC_C6m_S#=;SCxpSgqK3P6-t?SPocYi>@*#R@t{F$2WB ztVS+f7HUF~SePLTvXhe44uzgevo?Brc|KPBWa)KJU* zl*xIk3t_JcQlOcm>Rp^MLq&*sm6^(wen+8mgSZ_k8q**H)r)FC$W;+^(3&F^&)cVn{zYkqErFY!Apt@s z;|E}%_dM&;#`Ie>+i|hDTJl$WZ(gAe5&*D^2lNVj{&||Jnx+X7^(y&VRLucM#-+eC z$MLPI2&_{Hej*DBH@8-Ao-e z@fpJYjZ_j2B{jG%@0b=XM*R?>eA z{1#9NRvL4UaXEnO0y$u(?WgBW#~HD}pO@32rdrE;3d)kPT2e~g&QeI?UU;s54U828 zmkejtUmJ)tsXbeqFN!z~g>L(7)K>CIu;%0H>qt0RS*f{OdsJ{-aMX%w@x_d-9xhy=kDGnyx^0(n_s?;h zfN2mUCp7&zS?j&$c8j^Qvk84`z%bAfr3IutQ0z80hUixDo^;9l0&@qxiLyTQFV&vy?rWzj(Hw zgK1vXMmdHaLo$E@GL4>($Bj7#;>F`_)w!q3zy#J{La`)CWyzKE4MLqr$&nA@s72?* zj&lL^`SrPDejDZz@5Ic*42(3ODr~q*`t*mC#a8}=Ujg{(hx}ka%}&r5WebSeeE2YA zzTo8xQswc>`&Y?8s?ojw^OY`HNSV%$8Xy$C3aHHn9z_e1v2qA#0TSVkmMfqsiS|H& zkfT>4NTnrbVpqWaP@kGce_VsRu5vv-%GwQ>hNmRVs7> z^k+19!EvT(jm7s_O)~r&2crR=gXSR#Am2pE02pB=w2*)^6k>C{XSM>M2fiW0NFMN; zvorT>)$=&3fSz%P&>wtHot-Qqia3KqGtiUJgmS|yr@nyY~FS}dD*8_hp{7w8^C&V@_p1sOD zulXGcCd7j&#MyB%d7(*8H|C^8&n3ZXRUOb|dM$%Gnq ztVgGiZREvJ!(oOsq_RW9W`*R(=lGRVX-DKWsf^ulzQDJ#s(URAw2jSpvpQQvo3AHV zb($?Wg0ekn%d8U1?2{eRf`pmZd@a@k3gF4Wp^oXIDTAP<-mN)o2Pj=%|SQmC#b?8dW{Ug_k>g#A%V99(x#szk_OVuj&&RX z)BkF+)n&|vJ`m7jPgqQO_b7eNtZ&5BWj1+odZCy$qi1Qx*P}{%^MH5PNmGEe?D0#^ z)fS`>50{Qun8A}N!LE6w)Hy6KBzL%WUv^lcSC;0;(XO*K59qYm)RTdd!DBZE7vKMFV@mffrzO@64txnm83?~F ze@u@S0tjr)@27xXVdodU|ZD04sII$Sm#izzh?W6c{GAvOm^1*Zy!xk4>uaAqXSaT~3&}it$t>nFsdWm& z0XF21A_iaqDi@>2zG@Y#lx)riJ3fbux{xrRJeY+2V+I8zcP|Myxg40H%(BHR}* za1WJ!C}_?WkdtPx$6V|f<=={`w4jefK-#S%-2jBZQN#qxBv`AL7Hd8;{^#Eo{+E7% zQ%jO%@IT%Ds)brbYuFy+cfMVkT*DwxBB2HX^l{_2hdiZnW%XKp)$*Mc%z>DA;hVnZ zf9QM;|6hbY|G(;dpjRX~kcC>}lKHY?=t*K8jF$uBR*oLyCypnFKABlnXQ&52RFn1c zQQMDqXD0rlJdq|lY$~;z4A1TQ#g)Jm@lW$_E?$$fp}s;zYl^iNRRny$c!DPm361#h zcTQZD!BomF(xP)2y^rQ?$;f{+Z(D}b9}CQYpBj*d*|XtOxVbG;MnJFJugNBTfqiGG zU~_bURlJI43x2nD)l~;0H52&4b5GPv&7&FIg6eu1dubSu3&l8IW*EvAIgAjkAB>uo z>`!-MrEcu1^H(^(|83yMZ6oO~UDEG`M;F?r@SlRjVEriz)4d0{A&`KtX4L$#AQXxw z$v#MvEq*|1)W$(dQHPqM5G|wM@dhx3tWS4-F~_WY93btblN+BfNbt>KlO#1M{DQS0 zZ^Wx8EGA*L*VbTD=IEzO_q?7tV8SR2G~MAq2zAWGmV_cwY>7A0sm#!1@RsgYxv95+ zp*3@>mWyKEN`x&CEeZ>S=697ac0NG;W&t|ww~YA9SZx*#qwL`=DQd2>mZcqt7s8^vwD=>cGY(!B8On8pozAG;g5)ge4HJZk?FxA zo|a8%GusGq%5r*Hlcj%L#8G%UhluSdvUnjCXGmWi&L-u1{}_7N+24cm!e#QuZX*uijWmnvT%^cmt2&*d0VK?Jfr&)66TSD1VFz*v28=md0sELxn&d_0#Pzc7uX z1!bf`_i<{2;sI%My(6Y)10wU)YiPbeQNXe#dco8t$U?0P=Td;w^C4JY`f5|63G8@` zyp-;CNaZ1^vNbk$YtFH*t?nG(nAk4~fP7R=d+k<)sGVnfN%+%J~*gGMXSDac>}1?e&-dD@J#{VrR#rTQjAGa=Xfb zDP6{njw-wsT63~}^87C|*&$?d(#4i0Cr;9ay7sDiFaO#n9p@QtArD)M!({l6cQSUa z(zb-g3b3XBpjQUcwqr{oKs8y-1}pibnuLUma}g6EZzCMcD~iS^az)Ef-)53tIAdq& zsGrj-n=-DqabG}>?X+rSEf8dn`d-*@Wn(bswQ368LBn(}g~VP8H5qqMnD4XpFHSu- zql-_KR!+U;~b*zLb)-6Govz>iObX;;TL3hLweTU_G#Pi z@Jnp(r2Y6+OGWnKb*(yGUQ=DE@_wU(^Z`EUF1TA(GMG0662cq^yi{X)e6uMFFi*dL zuD8D&O91XWK2yio9}vdpy`dB)&b>biAI)IImobA7dPP0FsF zU+~JG(0JhIz^}oRBBQ@rSNH5>Q-h9hN7&zH3}Y|ID9g@KmZ_uVX=kLsBcrzGY&+-tc=*(Gz?s*! zq>~l(R|EEpd2}#;)7;_9-r|6r97yyKSXl8fzD=ZFKDGicc&d(mK(*L{Rt}Jp*^7yu z_ffY!utow*gM7|eXrn0#!V0QW+6TW2osj`M1P4!)&7`)jq|KOHkz-4J-e^yYG2E}< z@m^8wXEyV7lTwMh`1&OIJ~XSv>c!aO08S#lc{^iQkNr-fx8;qB)Xff1Wp1xzB(=~} z_teI}tiRh|cmgkPe2B9eYm-*t$Kz!0od@(EfH_M)P9w>LvAG%$ zAab&?q%SORT{j~jXduq{7*YlVU`xYH?P(V*Sw$m5N4VW3limq!C8l@lyG6?APk;>n zZ+i~vPpcT)JVC=HTt>gLNgk@S6)lNs_g@M*DY zr{-ZK4n6)OQBR4hTFIBM(gy2|b-IEtO6Pa`0Mt#Juz+L|VCZty2e%Skignx?4r!5w zekSN#&1x=Cra_XUYDdUaG}pfYLVX9Cn%|ztLNt8MpEkI+EwJYQahd<^DxXr|pQDAi z)s7Q}sJc2wp5kH=zw?Jo*#}Y zyml_tAHT8-I7>QV+eor*fbrIcOaRz7|1-jw*r?jYeCw*$$-|A34Af@1>WyHECo4Nv zz_dofo67Dg%$=LSXirxZWdXH+E!@n`eT7)w$`5TVdLLF3ts55+y)b(L_X!_s8`k1{ zngR=}6nHy1(Cgv2Uas44J!G?k`s2|3U% zITB1Q8as3@bFWj{_^+v{e@3cK^zXU#y?8F!7Y3VDhXgg~Lo}!lQZ|5Txf9I4@%q2; zoQ7ix8gY7z^@#5kdyXNU)M#N1AAcZAqcp{IbCS=O5#toJrNgza6BLUWhk9U zMNQ^F22`_DD!-~v3(6sG_omtV8h?5@AKrgwH_@)$dujU?`p4^gzeir0J+5`y^7But z_OCI;zH0GhC3w86!zcv~%}^Win}8I-h!`EEAm6}jH0(MY5)zmttjz_vU^&C0Uke>7 z^^|i~hT+ed_)Vs|7Rfk^``?Yse)6V8Ks&DFXI=ZRXD0lu0>a8FxMEjIUnp-6KN%cNcA5Y7qhm2D_V9W+0DGnxaP~Aqq z*X4lyd^qNG4O)KwXSljZ;b}7+e#Ga^M5HEgNKhTqmDzlA9(IC@B}w5RsgPh!r7Q&l zS&*Is;J1>FppcAUgtfV@hsp7@8UI#}K1vQiubf5(rOk5%Nz+Anu>qD-O^4~$jq#^$ zzuE*wgdhbf+A-Yj;!ukX2>y!N1wN|w=~VOpB!^o%THBJap&sE%h0dw< z*6we(wueG9jqemlM`Ql%;MFbIgL6xSc4zFODph*;% z)0nNJjDz_Cihfva>BqrT83jj=S5i@z0Zqhyn3ru|O3m%1S~!4+uO=6{hpeE4_e7=k zwZ?C#_>TX2^6ul&7>j}es-NA1*hgcg?lbvCd11BT|Md9X%qluTHt&YgGzlLoTP}oN z3W-?ZYb@RZR-S#R&B3zA9<%OXu^WTe$p_%@uv8L@Ejfa0SJrFpxEe~nr2}vj02`mmlB|vDIsAI_#x#a*q>m4as;lcyMT(`3p|L>+HZAxoXH_1BL%Z#=s|k)gRMuq zY{$#$JQefdDKMWZ)O&dD*MoC8g|!w*C8HBT;XianmhOQCp__E37l6zE0SufzF9siP zLXxpM!+_&t@FhYEMH{#VIyU=eQMw>q23agto`><*6H5nvIY5%H1$5Z8J4d^sMB5L4 z{`@$gjlTO{4NGRp?@GmVTfllgttsSFap=aOac5XZdG_fWU<-CalfZBQg`L0=TeiKI zzVEX_lam4$H?o&Rd3!a?VjFvG?ad98{c*aO!KP{U@-W!V^`1jh#VFLM?AbI0rQ*=U zJD4s2`1{Nie6_Q*vf!(hD`16Kpn{P~Z}L|~vd|p>n2Qy*j*95C@fj)DTA(=M_NkY5 z%MtyD4!8sl56!Js?i<*5Aj!C}WNKP)Kpst)AykhH+`i%3_(Yr4>pOtlWbzL+& zuHK%}!8r_?8w7@PV=q6}*3~_Kb~pc7ZSAw{9BOJ-O7_Q`OW7aOeM2{#J-gwnRuSi$ zDq&HB$_sl9ju;^Hk(73t`woyE!x8OMMbTpi{${!PCN*tg7~fHdUzCEWkAz^wm`3Nw z(~dg#q7l)fCWXscoZhI<2{&`8(mSUhcH=XC280%a9-FKekJ>h;l#Tg06pSB!Oivf@ z@8;TW>l4Q4G)-L|+9RGACs=Zm{Mrw45oh-v{X36a45Xn2$J?i~_xFl$eN}yCoE<%D za3GV&+G(IJf``<`Ixk#x-Y`xo^wu{>m%AD;LaGsI!nDR`vTqL(>1qZWmn!f$D4(C# z^t~hudRzNGySgafq07f5`@^0St5#M|%|vrPPlGH%g=Fyn22AxDDzO?(DHw@MsAgA! zxWHRM*KX$F+lek5I;X*6D?$)v(-dSN-PE=5Ghh%A%5{4U-W&eM(6b15;nqh}?K?aR zLg&u1FDaitf4^Px-7xfDh5NrmaXU$}Jp3}^1+dS)YH2FeWJI?qXAM%2>@l$wsoWC) z2FLUIIfa&~a&|Jd{tc0o=Mizqroy9OSYpQx$`7r+a2H5pTx6M-7QOhoS#YJEyt-7>LW!M;kV1VMnJi)~Q*Bhn`FVulAC%>q6A zz{a72=q*JyrD+Tlw>TjVk5Va}Ga`N7#dJ2?ebADh zh@C8(w5=n_3Al4WfM6VYsxLGq;O^t>-#6i_jlR@_N^*X(CQTg$;u^peS1(|#{0c{* zbQv1kgkQ!ppnC}g&3-l4n&hwqO=^LbGlCIdAJ z{VMeQRMCW>>b;^X*bz9$k-H~tR z0y%n`v{I}*XhAiHG4B)Pp8?Y4?23bu|73j%mp_$&=N1u1% zl8HtGc!Lweyv=ag6WidlD07{_j>(M`UDK@(Y)iXr=HE)+8w|>1aJAcTz^7ep26(px zKYr6TEwIr1$#cX2QL4R8lg%^4M0^9{cH}t)2}xx@7hrLdNa0D;E(@*AW?fc0JXAX& z{OeJU2rlLPjSC)ekKOZ!YmeWf6^$P0fA?O}dv2~^js~dvziZSMG(@|_LKocnX~z0? z;E4q|AgU*NM-@o7YGAr{PJ8=Id2^AZYeA?NlGT(d=Pzrb+IRszAHGcw5zKXlilM;Z zl8ia`8&hWXi&w9|e*@?GYRx4NYY_SntApGUZGG zJQa?LkdOo5lJ`Lkl+#a~(bNIm6TC%qt7ZVfNh|y&P?o$Pnd7R(IEWbal*_8N8^N0C zIc3r#s)1KQvSXN29JlwM_9@N~I5p_eJY$Go#Yfjzsh8hT=4pQC%d(&vTmwgPX&RqE zF(RD@R63_oLc6-4Eh&|YyH?AP>eYqAt=`@0(}ifriFz30Tq=A={|mhm3F(iS9PS%2 zt}bsY)>Vy^M!vy)n(m3prU+wrMG8IW2)~EhMmtp39hDYz?0F98V{|*X?13)XaX>Cr ze*EzCd0pMJbkE82A-6tehFW;8x|hNk4#M<~ek;uvgXI+t1ydHIAs`CBVQfV_+%@;X z7c;?JU{Q2PF4`xU!;kkO@hves?kK`tR~;AoR&;lLRP1q*5hov$PL_d4JA?_|7mV~6$*&-n_hN~$Om6#Rhl2?#ozmnmJ zSnooX`t9S+KRjy6i0Myx6jm;Ix3^|4D*Nx-^EH6KiU(0Zc9{hG;Cq28+aIn`KGT%A zF861OATTJDt)n=YO9gE^6I8buL?huutb*1^gOZ^|;~>CfQIePh`>}^EM3yR?xz2kDl0|XbFBmFD`y)*^wfr& zwe3WIxh?G|EWS>3e0-7$`Vb3K;x6Qm&H=UGJCCT-Mc-x^Zk_7wmz-)^{pOV0I?vU~ ztH7XCA5LxZ8Pfx`;?)f18wmDcrIENMTq zjIWED&NlX2J^@RYpbI%niz)C;%gX#PiJ8>zlwW^qc(hSM)9ozL-|QqnHBPCGGn#XV zFzcGyOSR#YdMiAe%-EeD>LeACjMYcb&5)eGFf=-2Yg3%U_H0Wd+W@?V<^8>(7biCU zlb&hEDx|#oV_Cz(q5k{$F+-IjHHHt4^@1+vtQXIi)dxWVVoX z2D>K26@@w07#MDtjjp-V77?-i_NO-M=LY|H$GTq{Y9!1j!y-9JfJ>b9MZ&}+9MBb7 zH<*|{TINz>`UkcG?!R}xwa_hm!|j(eqYvqN7gpnJI7{}g4>kXzeA)@n-vs{uX>nu( zT$@LQM!JESP#nk%=TlR|eHuR>e7ZHeVi6(yL-n7z5nSPK)t&K)4V+I|nkv4mi-Ue| z(lEu!`2hfCP>LVNNyxHell+oVNRLRBL&?B1!sYQ>`kq0|Gtx!3xt1&h72Y09+T;{` z4p?32GLHGrb)9E7=%zUc1 z^Q#tnffm#FFr^kk^sLk-+Sm=}CJ2q@hmbMGihrMg<-zF*z@BexlK*d!x+WSdAsevC z%PBR=E2>`^`kDu%a9Uyyzp|#0+$&IoB)D#I&kb|k3A|R-5r|GR3CH!q8`E)Ku!bX$ zE~9h2I;VfA^DhXE=B@9FDm~s24OGP0mSV4`P7kHMT2NjTnA2Z_|JKv^k9q5V(}vOf zo&n#TXPWU-!@MjAoLdByP}HV?&D``e8UkW5e?W@mAJgm*f8!w+Mq@yY@g31$rQMG; zEBmdJend5Jk^&MEj7R%*#dh|^W+Fqt^hy2{PXqb!_w&20x_~zFpgY@J?B}m*mhF7F z`j5CA>cSd+mdfD&Xkh=-V|(6r89zlVFoD2&X?V%5_ILsH7GsM;Ro-CO^RD9Q8DJFU z$=D`##epLv^zU5AJB8Lz#EesXP}lUa-_F+NXA_wQ@CVO`BKn<^tk+b3YtM&Is=BzA z@#H_X)mP5_zxpYv4anGG|BT-X<{3bn{tISvJ|4mJGr-HL!MZ8%0aiS_5*JVUIh^bU zn+VAgnvlJspbOgu{Pysv4$^t_2-!XKWL(rkl-$U)ZI2S^1cU`TJwsX4;KDK9x=sgr zRe<*XZkww5m19mbK=8Cxb4(0_#h@cZam^CGK#E#Xgitu4k-4^up)F$@Vx3@?n{}h4 zDs_ylN2draT%KsyQ$(X&>YQ*({mnatXjxx_1tDu!b$#ZlsH1RyrPAS#x38CJf2joJ za5hQyj6EF}Q-iG}_Xd5{`YQ^0z*n1bzzR1(!CPIh9o$-ED9Mz6E-PRmEtRj&{2m2% zYmV2|L2}i8`#)W+yk)E4wDA3gK)&WHmB+DXIQ6^5q8M{GJL)O2z7`W&S(Tt{mT&8Uu0|1)DHS+aJ7@mbE z`&iqLvl=_l5_*FD5r;>9|I~(sR`&M_ew*F;RqNiAdhZB8xeHps{vYe+!;Q>Vyn3Km z^X)0cVwLF=c`zHp^mk$^K=r`s}QN2f4%NxnP>3n zrL($uXoSFTUs2hZV-Dp`!H8en$C=g^8f`rX#op-5g@6HgDSQ$vVPOAZJ3x?-WI7Y6 z3aAvX7_5X_8zdf0KqT*Pu?l8iWgIq2ECs&YjnTY?-H|vglm=wSU6F;BcOBBKkEi}v z(PMk|Huf5e^Xnh{dj$GNI^heJl^4`-y6&*bUHwxxb2;{A4jKpSOgHgDX{f6%>MkcF zkmfUjb5YXnH|PD`Dym(-2huq{BKK4-;kF;;}E0*;czd^lqk} zp7RC)VIOMJ9Is?)DtR06OVG;^zUa+ zCi%vDcR@p4(|WPq-bdW(o>bO#*-tG$M;ZA-QGgXU|2#}cb6o?1lrJRvYtHvd-DU;O zWYo`UqDZx!K6SDj9=sD<{B9&r7qp`@8gjNFE!SsO{&M;rn7ES@fTpAdWb@)vi^i); z*B^Y&tIOQ9HX1*Xy=%cYGlV7xKFm&&!q5ReYIQIV$yG*i-~5C=N34M}A9Eghq&aX+ zRs8XI`3;@}4EB;?pn7kI+BuFIor~TP#%yF=38{qpGZc&R;c?TRqTo_$Ee*^>SPltH zrIQoTW;RT30ef7M?P1jq& zU+x*QwDl3n#4QJ08xW9jM-pF|dz|XD{rTR{b=S7&AnpKtH~|9(((oKOJZ%fRqUEdB z52F}Z$-s}!nl^C+>{3T`tIC`1tbEorO|nqCYwiL0WwW>Az{bSX2Y_}(W|LeU@YhWf z^3xPR80fVRdZWfui%`=s`N!^?*=1lco)6`d+%wz4a%CfrqCp|1EU z`U20c;+IP&<7thw9hFn)r)I2rdqjr00dmv$bWUak(WFFYohLy(}x8wlyX% zXKWPD%yvX3=XvQl%(O-O^_afyH!KR9OPk8RA%io7I zVLK4cc!264@S}BI0Z+0cimBK}gplmIlrH-5&9u3pSH(?go7eGfu5~e0&$YQ{GtjdP zU0)P*$^0l#vL!XOaog^;HD!OSQ)XPe(;6N{jac#fslHPU>)l=Ig$9qkmcp;nR+sND z-te0>ko)h_!ZdtYA2_H6xI>eaO&Z=$np>bniA(YG1uH^FeGJCk0B*`thNH~I=p_K< zgwmxm{KS5iiyJy}Ah zM_cN1Zf7<>iw%7=lCM0+?1L_tbNTnI!#OjI{{SLq*7^-LYi z@=Ns&U+WOCp&tzEOPGmb@CoYG&N9j>wFii9+nDeB4Pl)GfZA7~IRkQWd{h>MLxX;- z@8RB?<}6lI&?rbDHKa5Ygulu`iF8Z;PKs&p6L0#E>{LmUH9KlhW z*xdG*=zd(d;rC+LPtS8(@S=sdBvPY6IM@7t(P}JW~r}1R84OBuZ)ot@S2yaRS3u9smhyMQR%= z#UL~@ulZhxLQmJHgB$Aw9vM(UD6?pC%4f#T)yuE`utv!lFUFQWfKAdS!8(6PsbV+j z(iYRNLrJDOV6)ycqDKK0<{o&RYF%2_^z^Uty4HZnXAzSUFz%5d|xLS+`wM&kwX;JIx1tLRONws$vj%>os}`@F}PD9y2&%5d&yf8 zctFCB)K~2w>8TGF(|6bHeoW6&ga{}`bk<=p(R0UL%hJMY^_3kZ5{0!n$veR|C-yHe z2B3rKGr;?K<~WfCsfa)slhN{?q)b3h4v4M?E=6hJH74e4Mg!oWh8@v!G$C$zD9VZ~4s90gTAZfU!;8_DB6=ekIkW1R)MLW?Dab;;oxY z1Gi(k*t%^WHt^H+p4Sp;O7^<6KeeDsd^oZ|Z9JtM0C&w$L+AovOxMiY+RoF3JQd`4 zN%VyF0F-(d^=;^PMxf*)0SHIPPjd1QJU2H{SS1n<%WZW8F{fb%X-ZU_kNLV;q2FLp z+ylwm(LnRgNZaFmP3%`K-{92$KvOeh40i^E?Qn=D>Wx=KwSb_F6O=|MT>-AjMF$~o z%Nd4s5nbHKH)Zai#|v{-REiIfdOOjiy>IG*YZYH%V21BJwA1u2o}0x|)f5dgDOD9ujzYae*fQciN6E`kFR5r+U@ zw>0DCP&ZY% z9tC5O%x-qMbo{yMz!=k_0!}Z75S7fTk&`;dNwn|l<=T=2a{%ou7{;vXI&5f@Yq(GA zs-8UUdQ@HWG_!wI@O6hv>yK~nMScDI-UYnd4J_KkzP?A?Y2o2CLvX6xh3zH3eml^> z)28F^*42mim3(sk{&D&mUm&(`*i1B6h?3aw0nS_P7dg)GAQvnYQ;1Gdx&p|!Q^8q+ zvJTdEfCZ&LHs{eewX`=Zb4O)fe~aE=cF~CV_g3nqkf;?AeIm1_@2!ipw1GqTf4YzT z=hy#F(od)m#JPW~{oD;;&MyTMQLwM_2e8OX7>)?PrIbltgjy-c0+Bhxpb`O5tQVm} z0@v@6cbLU9crsU^ns7V(1SrQQ?WT>}hq_9S9LfTgMzTS9>*TPyt_Gh z`Tc&QrWc_HR9ft5D46Jb+_&2I@CIeg#f(G5e`Wu5-t*z-O*RKB_We_`Sd06wQ{Dde z>2Ciz;q4V2xb*6y;IQ>&!2SW3Qbk^f5*llRUzaOLhG^>H>W-=a{DCSrz%|Xz-gckA ze9cNTcI54C?xs~YAMYZy=mPUNfIt4dd)%Bg2qxz>xxCew7`V4l`V5t+sJmTp1T0oS z&9;Ecikhqp4+)Jc8lDSe=hJ_)J>~y))HJX5#F)Xkj?oyexblKKKR&NWpVYOk$$2IH z{P;47FG9Dnu-{U@YFz|X4gUf~`Y~fNZ(cHFJ*$i9b^^#>2H0)2AxZfjWryaW`cwNg zcbQx1147`h0sB&bcEw zmV&Ye7tA^G39U#>52?9J zr^9=L$(Ex}sP#^tCL2GNo`1N#H{g`yRp8=}Y4zZw6~Vj}rW9LX$mj#m=Wkdaewt3< z60i-pCN@R|#|(pi_i8-7ynw5RHWC8-fq`YpGbo%jMSSk1~<{_O`cxb9kU;A8Y!Rlh*(a%LAF z{heeCeJRcT*9AfU*9)RHVe8b#&hNci(WwRR-zIdB0XS7|OUBX@=87qxV-)TT_dxt2?XGc64-TM?_ut z^-RKn&Q{A2GyZ*jXCUBEuZMHxKx@z3%-G!%1W#S&nWD}@5a9XA8>{&I)ID%ge3id+ zIX&a#?Uo<8yl%q!PWx~t<;;YApINIa;j$ovdf`_DTu&1|l_Z;G_eu{SkZ($)bP*H$ zrLo#q?m`lpwy_e_hYQs|!3Bv9I0O2{&cjdR-Nl?b5Y`s$dj@`+8RX=*gck;!gZljV z$^)~w)0!gaLtGhUbn5>yW_}+xa|1jluwxot^DD_}BOPTrxp&`?jCB+2-VoAtY%}PU zl&aSx30g z=48UEGtUh*1=)V4^vE$Te75HFj`&LoPt)d>V?8A^$J)cEs+_|eDLuqu=AuTVdTNPEDRMpP4J<{leziOXGJ>34G zC+|sB_h83)I|uXa2f-PS6ZZ9iv3olv`PT@jwT?Ouh+F}Q6QWW542|to@8jIU^uKDo zJcWYY7;M&npSu(K$2?4V6i~N}EY)vJF@FL7G`<%c_c#H0s&;!Zna1kvZ+^Pt2UITW z0wRtgc6kaH;H=ue5@u!>*!<@IGFCAKj3s=NYzMpg#qQiYKK`X=m8}O7M+XUV2wRs6 zUh!$DLl0Z=RZGh0{oIEYlD@o9lXCH>Qvr4;TIC0TbBoFm%fvQT%8%!)|Pfxs3iQ_(@74Y+T4Qd(~)i8tOOfQ^JNNJ zZy&aJE#mwF(vD(Z%dtl_NJbOWG^r(U4V;w79ix3W!dnEp=3Rga(Abk*L~LprLc*OoL+p-e7t^of*Wn=LCbQ*j$gJTDLelNQM@mHdm<~nIu1O@Ze z9#hw(>X)&&SM@Bfc5qIiE*cdTA_wI55qXjTH}BjW#+x&VXKX($7lEl8&nsLYlfM3$ z3`-O3$6~#r(l151oS6eoppGVDv5Bwc<|(^VFNYllNLA75EKv(vwVt#Bnpq@vW&d+X z2azMtb_^G)xkn^`CW^&sG(xo!q@XNWfT7Uz<~=zr*%xmF%7Y?eaBlBAd*hsSl*Ljy z{siyM>24#vwrX~4;k!Wh^RTAS?bi-}fpSP~sEz68XfjE%TnZNtXs2M?AejjZ>{8ll zhFc{sRUTMcit<_%u1N!=6(i}{y!e^Dm#)Tx+{L*)=4*f~ZJg5G*%^g18w1&`kjf54+A_4$cI?d6D02* zJ1BCY_;f{O2ZJ1R;UU!tGz+Midp0=kF#T(Jza%vCFW=0lnvTqx@gSAO*j!mb*%-ef zXKZ?@{IkY0|7CHDJkH81z;k+U{wuQh+v0LS5}UF|%{P5ZCMEPyjfTAm&&OUv3Lf z3Wb7ewq46d=AUUf__TXNeO1Q*|4C)MFCTkitp>JfvZvBd89?S=pvm@aB4}J*?!rs( z4Q$jUT6qj#Kp}%^xRBK>5BG8<<*PcufA?>wp4p* zk@YP_v81#Y^jQ%vAlWN-g<~H0se2Ohsp$dEd|Lp41ZHno%c|^ z8Jr@2M?o=qKP++Yd0u+Tw)sKO!*jJeQXfes95Y49P@VhZ5et0B@ll1qEA>#( zx0UDh*&|hR27EnG_Y%JB!A}61;7mxPNOKNA{46WHBczra;?pRvGd{lg8YH8Z=T;GboUb zy{$>(fFRHrUi1{sh>zl=Tm-rOGI1?9J1Z?>529Zwr|8m?_KPFOyhBSI2uu2hYic#u zslU?w)vU2O4ao4{fAfpLlg3}TZU(8&o%yP29(F6xMFSSxS<&^JHFui$D@Y-nD^dgE zG22q^4W1=3)kvF17YGgBLcN>4oOe2~mkpcVe599f1lIicF&+~h?=e+Nq?>U~HQ`<8$nYowi{#>6$ zC#x;}Wy>6sxnFYAALW4FfBLNU7A1re2e_$tOb$WF4>0|rkc_ql&UN;hRDkuKh;5>? zbp0RSYCPV}a!{Df^Y0A%A-3xTDonBor8oLMdtBWep2)Y}JTkbqXDZ8tZs7T8=(&aj zZ~Qd!PM7qEqrJrb1{pob3gHbb*0IELXLXReCG6(`^DrJVC(BXAn|lj_{gDN4P1lT;D?x)irv0P zM@Y;0rZoKWud23gtEjJ`bYhI82)HAa}N zGR(k>i?-OoD*Ooq*_dbiPB5QAacA_2>RnU4J zRKfpR8+0A*j0h#ms14(kqn%ThdV|$v-9Xr+>i?Iu!4kV&=@&Yi&xJZ3ez_!Zy(Iw( zA{q2=k5XbOC?tqS9@6HqZHA*jOPc~`#|qdd;OysYjxbM}n*qV6Ddp4;BHI$X#%1(h zd;5n2R+oFMXq2d)J{+BCFr{0QU2FUt{EvF2AM7N227Cp#&HyvMUrSS?b7={2sU{4QKa-cR!L`(U`T zLErMFyT;p6OGIR?lV8@7s#>*aq&UjVRc6a2%G2sud22eP+qi1XzEa!3eeEbAW|6AH zo%md)T9pWRz@_kUAA1X`?r9A@hKb2iM z{irYCyQ&LB&uM*w{#xR8y}8sSM~d!Eh%e*+QxpB)@efGB|IB}wjei*SgE<7r557EV3H2QCl*tt;SC#4GgT3rx>!N z|DQm{|L@2O^8bpglpbyQHz1Ir+}Y@bF99-)t0oG&EYlt@mL5Y3QgE_nkCGP8x9FM- zowC#ww(}*-;&_QlGBC0{wQ)_dtm4odmzC!acJU2P9eUImo~i%x z=uz}E<42DkSGt9TO>UbwePci*8u-@YN%r?i8=*mX31O4a zQzXqhB0iS+j}BWQ6h6np5ZP-k2Bj!KX|g}KkJ5lJpOU@Ie1$d-3E>!fL)^se`~I=E zkjX}9l$@RMY9PeV$m0%Gmqqu_eQ`_~m|{%c;O=g{*Ik5fqGy|T`;G;r-75H;lG<_h z9AZ9CeE@$!DVL64MXS6pGH6XLP^5(vU)`lQ;b^PSZYTd#-o1QvPNk^AuV;IR{ZiqN z<$uncsmb+!Jt7Hz7iLu|yR%IEB}XaLtcG%Ze!h~HY&!`qG77J8F50%QXG)J}GdTy_ zso*t?yEY6ED}Rs8IG7ltB+3wuqcP+BNk->tmir)_$*vP-_T_$C&l~-w^Se)O6PAP!P6wT;;BWWaYviVK`j;v*=dz-0F-C8N z1{L|TCNL*B;42ilUSSZ}kZpy&syV$d?;bO`F<@O+X?RNH9+A!H{accMbPn`H zIrR8RimbZgNrv9d+?&6{J0qP3#uyjd;CDm|b*R+%n{#jX2&ojcBtpEX8BUl45bF&`mo4jpax73u5NbXh(pWtm2 zSx&W}=q~#it!i^kPDoUIVrdY!Y;t@%5L92?j6M$rj0cPIHO9)4S>Qb*<&WR`UxyeI ze`$<39Zg4UymnP>cXeFuY8BF`+0onEo7J1upX^X(_4nVu!XKai+;ZD^>mb!;4qi+_ zzvp0^yflV=u(3lX-)zbo71~5;gg*LU0nj-Z`F?c4R;(9|R;UV5l8f?p`-!VXquI+k zK)*qHmIlsBjxl+H9J#-Hv;DgpvjYNfV62rL?>i0^stgG7oEa<=lLHbCF`G&atqVe%}H_W_#0Jx0Q9x+r* zz7_2~m9=7j&+>IK`Jh>u`FLm!jl)HKsjmZZy;gNF`)?CCTDaC;%-8567=m(}qu50< zuH*p440D$!B(sg^Z0qjN{-mQF8pS)zpSbc7gan-cg>w?6bW6{Wsje48m$yw0!*d>4XF&sL}i=9$# zHHJt^OA1E}memNW(+)u=b@mAUzDU&vrHdy;mO-o19VaPuRoTj2Lf}?GQe<0m=swzV z*7xnsbD6DjZi2=@!7&f9i;NU(#1hWuV3Tq}lY0%p7;E@AI*wbWgSeodlY-!UNg(o3079YKA<`R1i*xiYsD{j%9S)J1}nUaaZ-% z8e{Ea!90kX%>YIKP1hG21s5wbqo4tc_OMN9qHub2QH;AQ#E9*CQzE*lATOnR`m*cf z{UI+qKkXM)jGFD~5z)hKH8nuvFalLJ7hfXHa9bpVhN=@qN<5RWcDdyUOr~lD8wxuj7Ih*EG0r-Ls%cWv)R-US0h&?;IfX`ykQ#z_sco0(ksw^g=aHKj*+m<->Z5Sg0`1?K+lVKB^ z&fJ1%+4~uY&#KxRf4e~kx(8bUms+ij6$byKV@8Ollvj;kg-~BQb}c*_$P(mOwh8zK zg3_G2IWfM{G5~u>WX23$&=ce zp728JaKD&SuD51XTOy7<5>}UUHQg}EZkmY7?!p}ac#>rjCFVuo6;_V-3m?a)IK96e z6H(yr0k%zp>7Z0qm~Hh-b~~BoVHePD>p|TsI_v+kv@jQ<-cFAl?M3|G?6nwg)0iJF z9wj0uTyN1(x^HRT`FlgaH3@9UEZMOCiJ&^uy|a{$tYUfKqNiQO8*i7K4YQ?;sR`qH zCHIotrDZr7#P}VIH<-)lBdYcpj;6X_ zT}mBr#EhZjm736 z6;&ETo{z{8M%;Wj?#9N9%&3YA=}~;?*r=SsM?#|NrbKxUPH3|S5A);3iB-2=p0`U4 zU9t=Pa;qwR{^9R4Po*5wVV4~@@H)^H(N8|oVVh~{f7DfEMYbvN`ay7c+gZMu-Jz;m zoii;T2M%>m>7f0qoHqQk_*V0V+}L6l8XQPc$%$O_j8L<@96}QbtC)lPu|)Nc8p|#g z;-xTf4XD7*qf&)MJ?6MT_O?5ap4MKwA#{*%2?z8#KQ3@X^{^<4m&i{nmRZtQihJ+H zZ^2;}#a@6BPytD_(%Bf~IVYj6vgyVhSU+I}@|Qa%UObBg<^&8`byGF1v1IR|BtMV6 zP581-jV@XvZ#vA(9(8|$JnbYN7jb=oM>aBU^Pt$G`bjoz=*Vn?E=fnjgPREO7Rn>? z_#O48QFD+^#l|zMe!x7?)MxguxQZ;&U^m!Sh#Pr^8j^UTbR4XnX-k=H%`=crS);35 zvIE$P);j|_B1$Yg`PI9({b#x|Bc?z7BP=gV-C)T?j{T!EY-7F+R--2E4ay9OJ_ev! zn~%r};92r(-o6SjwDjNL3pT=EY*V&MW=0I`JX76Kv)?5@XJlPJQc|KG?tF8?{U`C^ z1EO2hoqhk2cv<6&V{>qc6g`0S5>)H8DZ*3Ix!63_0?}uoIcur%hbnV>(y9{2<2lSp z#2f1X4-U&{Qz;4Kwq%<;2L4O5=hKR=vlS1(alPUtB>GhJ?`U=H49~Komc@3GJGbKB z2xZSx0=>I|M=Grd%Y=32wu*a~{hGb5toG1w6N>4>6(pl6&LY6WBL@5Tm?2FIJBn>Rj(-27qoO1%bYn=m z@7bAmqLQKziufgx%+L@6eMGiG%t}{dv^6yFcsKuIF%rzV=8M=~ug;q4(WFH=-+Z8N zU~zzdYsAM_>d`rNGXBNEvMYHdx9=bJ>vJ*M{!oW78m;;PMu0g||0o?I3lbU@{VS&1 zSBe0}Vp@% z-zh$U+;}z|)gvg(=@0Ssn4bRm+RgQ@CJ*7y`YjFj)-HZ{_m`E&baZ5Mk|4~WWP?{v zEu|dr`MldP%59 z2?1=w5c&ep-^CYqf*tM#m{OyhB}{4k%+sey;^SSL(@Y<1jQ`jY?eFT|1IZnt7vAkt zbz3LeyxRFS1{Cj3#@Q)LQ@{Vzv6FOgXO3ny6sq&JejhcZHx2dv-e`HHNb_yUUxk!-e%<=UCj9G1O2S)`X7}vR z6#x-GPFp8L-?H0i+9k)eanROvVp(9v)ed+gxYozBi3rQC)6oJD&vRO?X4(0Lne@oQ zOz6NnG-OcwY)j6c5Nr1G0$!evh>X5??je)Y?7T&}FfX!*UOkZWaP}Y34{81V9v>q$ z&dInQK)h~dT7bGxoaOY7&bxj#49t4T%&Ri!Db%mVw<147d2m}>q)e8@r#wW?ND+T; z;Ccp44_G0U!(OoFq>(FwtFn*LLdu?evJP{6qIu2zX;5=#XrA^hdp_R5or6GmFrSb} zD=%mvB~VOjbs`-4iTDaayt8!&i1^<1e13mS&_z4iv29{Jw~l}2ux(BC^UJ^AuAdJ$ zB*LH87*68`31!+m{Oe|=DS|Jypp0<_3v2y$bA_&KA8A4fyz4 zGH+6@l+b-ej;_Ta5Dc~ywn_K~HP0)=_{5ot(NWl^Ej-Dx5h^k?O5 z7h_@~ZNy%YN8Qhzv%2-?#CMb~@ZtqPVg6-!coy3j>|RW6a4Nx!Z! zK#@-^lnTN#UXmw!zOZY`V12){)i98dj-wx*`DRJ4vA(i?t zEoHx?RD4L_dr<4eGg;h3maT%JFZB&Jcf`&OhVB?pwYe$9mm+GkcHU*38n=Sp5Z~;q zEZ6-#pJS+Q5TQjp(HY@kdPMN(iTXl1U=Ah-PYN!0ZJHGX6`aCiy;y`=%uKyKXkcSn zSCh%uYy+Z+cFiO$y;S3Y-7JlNlI$T+#`EtDu~&Eeocvvvg~_hCydkz(`{x+@=1mz+ zPJ9nnukHv!`^XnR)d(n6SHPZEHdR7ABmfL~gUS<3-8?KjJ0~WrY^}*h9wC^ULS19v zdQebt%d*IF)bhc-rxi7oX_m?KYq=S#mw)f6WBwI@6qqGXvwZRygil&ruBF3>($U@m zXGwzYXhuPN?O}NxeK7sf*@zh3CX-ELk_)`tk+h?9cia4&0-9f2aAfG##C6#gDbG%? zuPabRYN1{^#gIskDK{gD>aW>dpph0h)18-=J?hpR&trXs-IQ}oMoboFJ*1us1?!Z` zDD&LNbAiE;=Yr48w_Dy)W>aPpJ-t#_W1W}b^Dgb;+gri!XVtg<_tSiS{^_H;)eZN* zHr{Zb1rFS|Z%n1NQ#B?pi$_gp8sbjX_Qzee{_Y3Ml`9j*Hz2vp*v^)jkxsFHhg^GG zV2j0?S^88}rG3M9+GQQRr^EJ1G2;lF;bTY#y3;nnVa(#nJhdf!s#ml2OE(x5w+x6I z|KbfH=Z0FQsgT9igH&W5d=yQ6MTXW0*pNXJ`RNGFlx_SPJBlwxirDSWo1Z8Jm#PW% zWJ6rUEjQ=oXb$&y&R9G~14`~=ra*YSQtJz?RI{ zS9h23m-$)uPrYks+Whp66qUUCI7zMH3m;WKEfbbKjs5ss_3C11>Da`0o=-_>N!ZDs zGX~xd{OtB)wuReIw~hDr`s~&rO+LLhB%vnQz{Vk2m<8UezDw_%%nbqu)}I+>`P{r= zd;elj`wLm8=!=UzMHhDcvNx2@SPpd0<@=IWeph0TN_%5f(?_%!NMMTV;hJh_8siH! zo9cwZx@^Cq7kw{yIv6{;sHUcH0EF13fj8gJnwVsGUEFx#!bbJ&s=MQJ81qtJpCuqG zaRvnq_tLCv!xt0KQ}|-HBK8ZRjxd;VOG@ENFD|ICV_9kXMY@n?u(t_BAXTCmd~g zLD?ICd>5O{7DrZ9FXZKqBAyr6Yq9=8WW|tOr0?-DJCRBvjfc+a2xym=-gJDl(HF>7 z5CzC7^05(_WTo`qZbJ9Z!9^8b&%p^;*wSW-r>h zBuPg(DKV9=EEu1M>JFwqqie$k8$BteYy$9~T%r{Y4qK*7g#-!94GpN0^<{;LsTtAU zj0u|u5e357SAIo7rFoZsyv02JcQXEC095L6(D#v*&bTJky5GlWuGj)tK9&r#*NVa? zmm=6B)=!1V8cXYJyPnrqt+W-*px!>`G_SWb$`WJ1uO6|w; zRuZ4ldXK6yun%&5YvK_lqn^|IX^_&)(H|bzSShGQ$3Y+|FoX1Ki|aP-*6c3#?(moc zqb}9m712SqelZSSos$J+x0ofirlv^zjrU}wNExUx9>o^}u&|Pu-YiMb80}R(?&N8X zDhvXfBC7+h;5)IddVFP9=~Ugm4@aLqLd92DJ0A|S*PxY`gKfL|h6dK4cxFLR#^x-u z$~$@95^vvn#wy*Z7qH1KgjWESMkwcX9KIN&pM7LiwFqCsB<{rw5IMjJc9&{NGOAH8 zYi~Eck(sMN8aZoNTZEO}lV{VO_cc@#uZ9B|%4AG@OZym_Ks_yq`Mo~clqq^2Y0_Ny zH&hInFsK^Ao5dMUPW49x%h@lGHNs$CBBg>-4;ZL>qtvJZF%FWNgNG&~F)IBE*aSf| z+GY(B06kmwYP#?HifNo6e%GTh7nmvMZ~rSs-nN`cJJEegR7<%y>zxy@4&{Q!d;=Ht z0rHmxM93FXDyYvmS7?0TDqJEh76#Ux+J{|~arHF1$^y8k&q!rtz)EyFiKlTH^a(c$ zbKtIIKAb)*4!q1a9Zu_fagbk*Jnjr1x8EKfc8SvMDZKNq>%^iS zb6?%BuYvUJIdhB%UY0p%E3RJvYaIilDfJ<>V(cFs22XMdX^Avns$7`Gv$z6p!%r#X z2%zzJ*23p>yXKT^96155MZ52YoYSr7KD%w%#-L5X*4xfbo~o)zPe( zm8mx)^^|ZzFHdp0?OC0ds=3pVKG>E_GGv?lRfI(<4rUAW!E=t>l;L`XO~P8te)8B` zpKXkYZG}l*(*awf=U621qbW;=oJmw8U*;KgO9nal66FsDc4aTF52foK@-vFY8HYpm zyBH*zg6<|`o6|W$*=<+`j1$$)8ZRZAqYx%#@4`|PlpB;~;E=gGwNap_q@=KHWjunr z%$n#O*st(*bSPk29wZK|Q#)lD*5Tz5g{4{ZGmaVCrPJP(rI#+8OGLyo-bj5JQe^?= zeiva#?XYr0Kyp?|zD8LlSk4Morav>KT=Vkma#cWp_}+R@MbZ^i0v%b#|7zQZj!1vrEqJnBc0*aOy{SA zwB>$7<2N% z)#3zx%7zn<9(9)hlWT4gzM3MN;9*~V80V=svJPm|g@L5rxWT|A!5pSLK)Zwp&h-GY zlF?A&=Z!`-jUxe%ln2XXTD$M6m4eiVyAitVh3TqILi>g3OS7wyY+iWS)Qb{NCyGlI zJni*Kj3v~p%*1a|)WL|tg#`zw_ctz4`gSDY-+{YIY#k0of^xA2$TG_m3|)?7z+(fx z@)YZP^OdAP^>^CB`#@}ypC%Cg6^i{#xb~r8zoU29RiS46hSsJLx@@X_yZko3gd+b4 z$0JW@k%OZO7(7f$(s6@DaD$9`^&X91y37o6ACjbtY{@1eGaoKHMkvVFB(zJmTAGXa zgZ=ZpCD;L6|F4QHKevpTP|~6Oylus$VZ~4-O>8cEQguNK%U~QBNEINJRpaM?tedO- z0|-Zi6!urx7daJRnD4QAs9TIR$9nh(XBF#Udqh9AX(sSWgwtH`@2f00heXQyUVOD) z=%IToUK5`eb?NS+g`ku)QNhel`?ExZ|#uOobO7pT}di9Om z!hRk0Aj|@5MAj|Ewg82$PYQdiQcWG9=;5n0TeR8EWQ`-Hy@<#OX2H^Bj;;!D+#)Hf z5^B%OIdg_*tgm=lo+2hiq?k*(JjIjSTm&ii^7GEmCswDcVp7_M5zQKyW(zU)tonf< zNtREJ(-8H>QNdj0=7+Fb+K{hg+hdV~LPH6*YG_yX(r%4gNl^Bdj@sSKD$nY_eao@z zkBx`A0!;miR)<vR|uoke+Vt3?ZL}wH-R`cl9%G5o|s`$GiVw1Un^5D&g;7BkM zjHt^lGOv zajx^8m{&2C5j{E@ba8DWv`SPg2SSeQEqaE)tod44MFza(EQ)18k_l;=?uk%62>@yzM_^oQPv z$9cL_*5vJT!8U0h`A-o=B)$${pV;R}dol_Wi6Bf6excDuv@SZcq3U%M)SZ$8vke?N#~HzY|&I zJ$YPXJZ*BJ{N#tth4OwR@O$xC_{)G+ZPwYq=uAt8J987B@T;;K<{CPdZM<%@ylIQo zp4h*?YjJaIU+{1X?{5UNsv7qJDZ(sF@H1N^g)`o>r=Ru7$+RUb{kx=jBkau`UQNCS z6N{KH6vQ|I$?h6bX7MDp^+>nI>9nGzj28-K!fse)?wOVAjT))J+w?B_04999_+xbubiu|3@c>C?nw{ zgn)b$2zzD#p7f^3Bs6S&zTzVs%d_B=3u9i`tahVmy9+st?qITCh z$)f$YHCS%!Ea4YDI^AQd{_Ai13+bns*0SpEGi8hO790m?$v0UMNTp>o5XO0jK;dn>-7ed-!Rv(HAA zWZ=*!3EJhmjdRKDJ>VhOWC&0@qfv;Z0L_j+^O2M|Yj>kks-V4cK z^XT+a);Xp4T7_R`)~*TDKdq-Y1Xx*fb4IIAbRrF9BPP4b3#7rmS-BpkAN0PFP;JP# zkUPs=xFOi7g_`vwuUB7{f+dIM_+aWmE-*6BD}>j_S>)4^`Pi?771-pnDAKj5C!d|1 z|IsJs8v0RU!x77{3lpH1C|Wt|Zp+WD6A9t5=& zdEY0#%nwM7_^Hb@_^(^LY}Lpz7*&91$At((CtoMyiw$IiJ_}Wq$k~)>+9;hI{X=RhrY zD-7`YM<>eYkS+=XcfsFqi_=ILw{IT4SWoGt;%ieQK)~XRpb4$r0HG#2QgbkAvx-8n z<>)ZEjoC(!Bt&zmQin%^zt@3o81PFaA*zh58o!tGKMnZw``%3&@DW-HW*L863hHTw zv;vcayD2Bml;Dc6D(40bUwqvhCKa00i=a&V`U)n#n><<}v)%EgL-@&Rh z#Nz268_(?TF6aCJPnZN1FyI5|VZguOx<_HLZ{zAxLXaeG`E&!)t3dRy4G>p?EgK{o zX?Ak_VEPfSI7SpPQ(Gf_jjPPWNgC?;LhM%bs%;bRk`H#-MC+S6S-3w+^EmS1sOB_w z9$v*u7^hEIyObZUuwqUlY6R~Zu&PEAuGzd~lk_z%Sj`~A3?npIcs#u!uV4_F4oxOK z1003*yu;tq{i*JP%ehY~Z#OvZT=f3>JZ(C@in$&_kbySJva9Ug1hHZyrfEaKqPXMP z{h?>NvE{7Vk!JPm1{)T{ww!GZ(Y$H*DZ z>u6k0#Vt>7y2D<8#i?F)L{Qd%0s3^Kg~-uCseizHKFX=*tp*58E>2GM%{+#B#q+Ze z#mYB=jcL!_G)Er}N15N0OEEVY`aWmS%(9u*$kRB}@Wt?}-ALLBp>n+d&d8r!zax#` zaY5D~Qq-LQSmt=9l7Jh$)II`h3t?h_B`R$~BtW{Y!`jeRd217Gbx^t_zhKZ~r+l)3 zcSW!Zjl70$n=cAYZ5goXEOp*AFf^UL4w8WwM!^De6weu41a1<4LF^(8hy(ie=^Eoq zaP56KVjo|44t}nQw(Q3z-Mlb__aV*$n^NlT@5;&ecyZgNWNK%{%djqwF00&&tF6Pe z_998k^EqbxyxLt#SCdN!U%L`#B4`L8R>>+1%*Dc_Cin{ycq9?Pi<&4;xDG7BR)sHB zSe{{8#V$#S$K~U0cfDj>DRc9%z%as{YV&!N#AYn7$lNO)w$E+4e{8HyJNT*e3W2 z23I5s>Mh%}bn7PL$bhxzK+0g2>j`iVgm&B-=S^LoGgBhJ7k_5*!u?Q!=%J+sM!X#P z_q@TF*$<9HP>3Y1oFxfo*yF7*6FL~!R^4&F+K2U{wt#YmD~Sf%0%r&Q*KjwwJQsUt21aX<#k7__nF!GK1x1>ex4z# zxYrb79ZI#tQWGu=C1J?6Dv-M<0>=+4`mXIkDWs)3a=EwYSwl% z4ct0BKQvbFx-IAF1!b@M7@C`Ku**7Q`jGHxcSIfiP;`l(UyrTX!H@4JXn@tm!me5$ zCX_v>cAe#RIH9fAg=c9gh^aofjC0n z-_&269Bgjg9?D0?g?d=}f$DNWS45^hlPu&rehbAG;;r+Kj$eqSMop28NwKZ1<5IOP z-YZ>1IZXwP+a+b|*fc73!wlq%6yNLul-^`AeW~{1Ab|*roN!sKg;PN?>*T$Wc7!H{ z<8h~k7`z$0X*PD9T?mStj2jizFT3rHGRPmZf?*Uul^9#m+wbS!4F9r5`}qDXk;3#p zgEiW_&Km|KHTCPCC!Pixq2gUpCB5wwFjuhT+ zeIS)c+>Nu$b0WfRURXOERBz+bXI2AuGy)+#i`1CQQ9H$lYZQ5+FvyJX%z3TI*?{HL zfm^bim1554zMt!69Z@IO&;u5Avo_>m$zvb?x_H0fTIgp3Pbc!xx{3t1r@~5qYXKx##%Q}JS!iI%l8|2*U0%VRDU_4GU zdna~BX}7oCks4|i&p$6?m}8gd&IIv9c)8`k@HzU7dvoFI-jQ~nkbMo&^un`)vG4oe z;#Y@e&n!H-r3%)jxW5?!UA&7z6f9*e(B7zk79xR7WVZUCOU&)CM5U=8O1l=Jjc!Pv zHnB)7?P{!cjprjTqRlo%)L8x*o@%T1f8*0^*4%Wc;@;PDw2}^5gLKRRF#ekZ;AhFF z)C(^o=E$jWeR;xV0h!sg2Rp}rm*ZLBfCkZ=1H==g*ntA^hg zeU-eRfcn>17M|;-){k zeCxQ9fUgN3+6wxX%${j$i!i-&}e6Y|~vRpH5~SmXXac zun}CMY>jnvDPg*{1Q3+_I7kjNvmtvcI*@lhmzmJWUf(|D5Dn4b;&7*c=a7wAQL^WZ zWMG;qbMa0!05Qnr?Y4R8MGemFu}OZA>v6E9-QRaLFmWieUdv`?fv`Y*t>xM9UVj=SjEMtL zN{e)PEk)tvDr=pM!ZASHTQincl`;tp*_)Nq*K3^ymlXw78;BFCFv~{Y>=OC7r^rF= zklep@(oNt;Puc4piB|{B)4-7t1aUW&H41VpzD%y9U02&EgIxckHOkHzZuKT z*X&dpm%sp4oq;d|nhuMl+;<}_qa_RFH3?TZ?o@vhnGS7g%t_kDa8TMCLtNvJyr7Q1`h(#5a7)39H5R{TF+}vcQz6+G}h;fe7)Ti6a(-Go3Zi|7%7Pf z{H&vBIlkcv2B_7(IVjwZkZ_&yC6+frRly-ARsp`MP%g-Rrx^kQAFv)JCTo`6W$AWm zeDX9GFh+r^`BUV+#uYVBmS-J3(cwsVxL*XRK?H=ZrvQ;tKNjsvOACswY+FQiLR7OCZ2G}ET-wZ1%VxM4&VJQ<^`YCbo>|LqR`r}$i zqOSk=#@oMb^UYGQLpBm4K)yZ>*v7st_I~Yk!2(TyHWx%3m*Cl9ccVbk7B7*#4ZAJB z+Rn0&zc`jQ^WK1TB*9;7>rj+@C$vIqu@}YT@<)89AzidM;B`1Bu7JtB(y+>urJz0jxAJTLS%_l_t7K|>DCzBc@s%ik$)vVyE9I?ODG|ho_ zewz`KAwQU&tK~L2o88#eGLF zOV#!&rNec!D3g zLti*}Ue$sTmC-vLJ|zSF5@C$4pdgZjX10D0djq{nu%(kt)L5e1vc7&)4z4v6qoIL~qFSdw zMd&a0Uji2Fp{BuU2#FkwhBu_=RMIWWpllNna|Y`XoIpt+53LKrG8e#x-mU z&8LNQhM15q)w%~32Z?7QZck*E1TXEVm$a3RL%y5q2Bmrs1r6?b6q)?~Q1w*XwLMIW zrkMaR#`PSwvbLU#@bll?92RDy_2~N8*FZs8x=n>FFZq9JBh(9+%U{&*V8*#}T8P=k z*h_^e@cVt4{!{u9;t7_+sfXB()*oucYdcfAs}YaV-00v_=h%;2A|vJ#Ha?hx$N%mn zCu>%o!u2_@=je5*m$DXVOC2|$lfGE}L%b)0afOfceyX3jpUQLv*GldR%-scxl@#2V z`abz^E&Ap{06`Rb+CHY_w)V56V))Emmjz8PG!018FI+Rq&@<;1h#XZW{G;2e+=sBM05z2=~e!{k*O;Vzb= z*o>OreQ}DCR53caFu+?nyin11u+}b}kniuZvT6CZ_{mukyKX(p%RkzWrq6yRB%DDT zMrM9P-wv$BdKB)VQT~epen4yzZEIp2>b;^^CN;3R28JFmH7&~ghPUJ|x?}s+{Qzjj z&<5dwbQk}-?ErEzst)XD9fswJwfdAY66RqNzMb`TcZAeNR3YxGqi z-TD>x;c(;;i4+1Q&OD7SA^CMhc+B&u>YAz}AL<$xKH;UmAOh$MR*zdr8Z+oim066Z z0vvZ7IBs7JQ&yeD7majbrmeUQAQvxb&mNy(xpsyHa+;!$_=+r29efE)oC#kMPf2|y@oGh0XEvHI!FCri zV_&tl;?~e&+e?FRNJrbd{$a5(kuIpHMDA~nyf~5Wep_Dqw2O3vpFPoH`rvF9&v(EM zkNdB(?S#+(E^E%3gZfl3t{i2BV2#R;lTF0zg^-6qa*x9ezUcz<#+GNqo7Oamj8WmlVVB<1_}Cinalq8%&SjG2>MO zW}x!CP*e(ja+#5u-~RV7Xa66=JVJX99v_;eIqZcnvbpRQDBosD;PWsqZ}eN`f}H~; za_VZ#!Ta1``LmFe2^!>-OZy;PGoeY+!!IxZ2Ers=y@ z&t)L^?*QmxSK;!<*b7+!T0kabIYK_%#d!y0g%TPkjY1F-}?Jj0ow&G4o zLQ<`%qQrPc`Go z-X z$+NPa4AZHa%8J_)KPH`8u}%jcd%%0o#r5Gu>MjWatpULtbTwu&&Se@}i`%jGqt}s@ z{7e$E97~$4XD>j74K*c!&1}QYZCxi|8quL{^C`iq8lq~y9zGPT>7ZURznFwBoMd)q0g8EC+La<-(jL<*$2#eZ4r40XtlskT~uPh79*#Z!0PA z`?IRDUHlR1i2rNR_1`s@?#$K>8tl#*!vb7C3pRMn3ep(14@+9>zpxhrfJutDO9MA- zdr$s31#e-9v!U|$8GhB(h^!S&k!kk+pF$9Wh@22nhTwK&ev_z;VbHX~gYj!7au+Nt zG-ZekrNL2%wGPgp2e^Zf0We2lwo^`QpY=rS#!?%1l4Ovk~lTXK&*f30j(tK|h zmp6x&V!x`Q_dXcP;Jlvl9wUG5_py50$+y(+<4oD}-dW)Ka8A_S4$OZmgc1z=U2n`! zZ7;wNlMNdC2A;N`(-J%GM~x*#j> zx3o@?Zniw^f)0{a87G$-Vnru9Sli_(`aa;i&2lXYmJkwZo$d}tR<}Im(KDLg+Ot#d z4OJiHE@;LJi$aj}X=BwaEtBE&Zu~9U=eO6Y^PZhL2FCafwlM~59nFd&l_~bE@@Qka zGXYCSg{mVMK}Iphj;P)@eEihN-UcCtjHN>}cw%Hkp1*Xs{LOGiR$lWTAH4_q{cOtY zZaXH%Q<{xoyM(>WH+SG2`9 zsxF|L?-n#NO6k&-tObP{H}q|QW4BSj3wfyJLG(sqaftojD%DJiRAQCeWpx(;Um{~2 z(i0o6FFpTz-TObc_v2cqWH-SQPJuwm`@*uwR*rlmx>*wE;_R@rldC-V>3AKoW5ESbKdp|DSPm5!YyhfXECS+k>~GkvbR2c4A5PCcm2^Chv=#_%ji4zTy6@fLI@;9!+!^|Nn4-(9_Vnuee} zrfG{W_r_;`8Vw~~sdZP*YtA4fBVC>yHl`_yzhx2SDSk)V`R*=)$_%wfrhA<`^cB9W zn)ihO&v_SL$nwWvky?XsHQej=E6qM z9c};!Px7}PI%?f>M(hL1@;t2UTT-`_XC8i3$?vgwI!Re1Fwq>?j$Ku9piC63WLcq% zYpiIcse*7V958ZAXp41)*?L4bWv>^7<(7jp-j0@j6SvZBfj9RZe0}n|JH|5}xrnEIn=5DSXif)LqU=QeZw2O3t8eD|y9e6tO|Npg$y)$*6BU zi0)+mG_5eMQA`^7`Ht9Cv!cRoD^fO#_jXlicYOThDYE@$HQts#Jac_mQw1ll|5^P_ zK|Ix<%2NA~3?}-VKCv7LN9Na_CNwc5eV)(+I;uL4LM)02Es?#I-Kvcwa4X8Y^nNKQA2eLAHQgy&nX&;%Fewv>g$<% zpKk9mHIQBuY@e6%id#4wmCZf3DG$$j@Y=uGpy&3~?QUd8pFboat!(Bu9ocIWynY8q zl>=|%JEZ<#?J{^Zr?oe17?S9Q?8QBTHOvH8JXTueE+QFdlz5s!& z03k<1w>3$lY_10*BN|Fer!KQOT7gQyQOTBp?HH(&ShUq zheD@BBHJS8Z{4RBLf8?Y1psh5^kEaELY=SS0pUjci%g*%t+7`RB!js^djn&pS7^U9 zMMCZJni+qOyB8&;xFWOhjb#W*BxYlAA^vD?TJd1Xd%1s6e5~0!v1{UJzU|*0tgfu1 zM#Ss)BBihH6#Rafn7#-ki9iCE4{vv}*K{k(TKULW+A7(uFJaQ*^#i~ zh>(TA#JJtfygVo`n}~m2kOz}QLBXxr&3DZ_TRM2prb!Q6$fpfS! z8Z2=K=3^r(AIskHLR@oZxQ;sOCD-^?h}#vAeI}MXxZkN6=o)oRT>|o>1+4YWZ+xPn zVxp}7+V>mnwlz+N$Gkw8aDyguFlD;19H|qX?VBni9Jt#$qUq01;#I+xDN%|85laLVmr24K^@}6o6&+swkkV?4OLY~%h0Yot#@^dQo4w)Y_V3q+n?7ZW;xee zB0bjc+gKW>YHt>DtN78$ycV_X3@2ZM#rvBiw8~J)n-1(}!wni!0c>|llO~y1im|ok zkULm_J8V97q`^PmLJsYUE{!WBu)~(_Zfyq#CnU*}BKhr~i%f#sD3r2z<~B)LNA$Cv z0?(9Ob~a6Ukzq`wa3Q#zyThxuCe@LRIBCt9!n4u{%~@n&b%b4NWKvHZ$mcA;{Sh?w zA{XiB&x>^5Eg!6OOHhrN?zq~g*;!RVyy!du69o~Dh}t`9bDIcQ-0BO^^m8U--d4)6 z#$JjJ#~k&W`T$y+htJgtU@T0OXlI8BY)cMeAsV&MIsBxS(IEL24Q&}IEOomg3c`@a zK(_}+LJJxXPxNI?q(4WJVpfiM|1zj(o}MK{?gq=G>u+l_mcwB>burdth%ICPG#c4?jSzS2^_JRPIe7=T;H?+;;eAnGTZd0ka>;k{H6AR?fh0KxJ?M3i0JyNKO5b3yy}{(!*?QP2edEXA<(>5ekbqOiw`dHAF+inScF z{)*&XY4ur-Y?9Cvq`6;k_uL83Y$S?Q6!QZSG$FZ6f!l;a(D}L7e?1jE9$1!Gog`50m>+*sn?t$Xo>Um;A|C5+?*N&e0 z59#CIhI6zx6UbqO-jmV|Un0OHp}_F602NG5uqCh&=>3+EniM9y7mQiF8DQ3cr72BQ zV@Th$Xvu2#3)FAld#ow>$5O^92i?o!6Vau`5v*%v>ByG-%LF?f(IU#bJ(`!sf6}v% z+HEu#eRZHE<0Lhs#}g8ZWa=!?AL?@TCOtmoo=HltW~$i>df#X4VhmRV1|&dvd8CQ@ z{q>(-9D3A#OIE!@Zp~qD>Ts3L84S!Vipa;VY$)yck*c~aH5L$a5IAR6Ro{_%a{saX z&rPJ_gGPVg_PJ)`DCy&vO$W0Mbno+7{o^#*2Ra`KZ#_$%&6_q1R^XSYP8`xEL)I8= z>ISXbr~YrWmco2&&cNf?%6Lis9ZaxMR(3kZls^9zC+Yde+UoBli&_2yjj=Gp7YdiR zZ|{z}zBk5lM@L{y{cZ<^;(v;KK)CZAGJ&jRoc}*h592QF!T87%{R%<>N`1MV)DvD7+LpRdC_>SY|NGb7Lpvog|m?6;TDG#efFy|}2hJ3;U z>Jd90rB#Wfy9wvUr0~AuY*fB56i}$PoT%mthp7&2(QeoeXnTXgjJ2ll^N}d4lrf>{ zu=`wKEL%TMM`qjrpM=43^Zr`iD>op?m>*F)i5f(5(Por^q_{Fn!W9-$ z71DS&LLO7lZ5tPiF^_1+ANP@7iYeBEnNhy06Y8!+@*LZ}Tw+F!Ij(dmFr)7-F(Rtp zi@**#jF<%g=d^Ly*687^Z&2yTW&L&zTRDD#x^(bNMwNff7Hi*K~X3g}VNb z4lhqsR4Z8z((cHu*NP`jN&G_Wk7hs<^ajtMk{88dnsBAWG-r`87+#No>(}cN%J~@hHXYG62~}s4=?%AK`5y! zT1a|d?n})n96DtkBW~&Vk^H9ujicb%!#cP!kVn%!9C=Vu31yuUxN0E z&K8Qv^M)xLIq$|HUIm@)puGwh_jO`6uzj;HU`3#PC#-`AX8r! zHI9G(dRpA2Vciq>l~0jc7n%}qsCi{V7=;pz&{d*DsktkkRIe^TrgwvXLq)@lQ_nV3 zX8Fe17qSaWf^-d%Gd>wx zK{mt_-NFe6oxkY4Miq-MX2FRL!k%bB@?=)^bp0>bVNH`_t`}5)0d?iPWtgA-Cyw!E ziA&ND3NuUO;^F=&RAD z1VDkv01OH7`=V}9BJF|bL!5pU@1Nbgr&;D$D5^(lr6s~d?#$u{-da?Vwna!u67%Qy z4yasv;_L!|aRz=N^~c6=PPg1xqoAg41kXtf9JNd}5}Fy4OlgzIfBa z@|rd%NbAepL$fF|oAJ^8y3d3AC74159M-nx$C)|3g>BxtvpLUHH9iMRGrIM|MUW$Whj70djvBCqF2fNh+UDM#%pJYWl7+0o?JiSpI*q}*IEbEYc2}SDb=oQ zwdoNgyBo{UgBp%-ad@Atj2&HG-W5>q(Uo&Vy}rzezDefLcDv1W#2Vo2KMX>eQVGft zBtRn59xIH9nY?adq1K@hCITP4@G@1^AS!+{BdH54W#wIJWTCxlzZmg z{jLEzI?9g~^$dk9m#tat<$W`7C{UM|7a{wR{GemfXVg=H#?bMbn#m;R0bd8NV%Js7 zkMU*~L*u6vDMfV(IYq@tCuZW2sDHASq(T~4vo!Tt6qKJ**k1#4*y~2NY_vbuHJ(w5 ztOf;XL+R$zd5oSO#sNRGj(OXqtpfZfUs!m!k#298Y^so;(xwypcba?>0Q-Ln@$br91^(9Ft!b`L*0>%*R3on0f4pnnbRqo1q3X&ID3BMStpPA|9oHOU^Mbk%kHM~l0mQbvsHSD~Y^++8L^G`pRelk-EGnLH zL+x$X%6QblbruChQWtBdmtPYN24-CkpQ0yk=Ftg=EGTji!Ip=O)N1+L4MOQLx_r2v zHkbPS@Wc*r5hwQXglO^{%+}z(Ox5r%q)-ksgKBBL(D*%JFFPKcQ`se~_gImQsDE&= zr5u#qY|D8$W_Dtt{_{DIl-Y_XKWV;fCe>;5RML55A`b}aY?pbjzz+XA)%Q6l@RaEwx(hvP>OD$gErxIwi(fEwGOFWso`4nGrD^ksczaB!6j~h z6~63D@+Evje+@M@!T$wsZL`*XdmDAx{-FtK^4-B_Duo&6)26nc5t100!+L)x&YxUVj*@#)iYRrMj-!oE#?!{5go_Oc_=;?8h-Qw?InUO<=62sw?A5S zw`4KEsM0i;l2aLF?^2aPJw1-J)a3p`p2G{cRd*ph%uHE*Fs*;!Ts7IEE63W;LX<+` zE_C%~__dvwC!P|T_xel8Z^$Ac8Xb%0F?8>~8dV*GcgqW;oY5Ilxs`4*#{mdr@_M@i zj3;kxnl1~jw;_gk83~GjXZ>^|OnMOfwK>-}UD!(AUZ%aFNIE8_u0lx5b|ZlpPvdXi z7jeoe);ijXe8{p3;)ONGhcKHgz1DI-VrY>zRQgz&a^?d>j2V3OLKpf1JVHBKqe>59 z+3c+7IMVL5`Bv``IkYRR%J7JUgsk=d1;y&^Jz_$62GF|B)=Q zJ^1>)#F?+YNTu8tYq#7|_hk6qFmIf2=Z{s<=kEVv0>*Gv%PNA_gN*)XXeqdSBEG3n zo5cpogi55L=-baVLAyJ9w!NUb<3GZvs*~99iO2?_zl?t9YCh-so$f{H<=6A-e)d0g zvYx%Dd*MxzI+-w|z)E#;=YYmqfPMoqbWb*{Bx7sb@f>h%Sp~J?J}tyR`mX^K*}TUz zbTgDhuy{UASx%rIyBc32eJNSOsoz?T8WibXQR)_^8WM2U{6!8K8eg6ANPI#(nVo2lo_5B%CPXu<&^G0TXFk|jxOgxk4snc z6-J2QEfNcS&{iCfXXIy%-T^=%A@R4w7|9X}*#J8pA)Qm8VQ%^Qz4)wlVk(z$y_se` zN-PeYPY?N zW>FQpx+r4?w@(vIh8mjkI=KW8$2?Ve0yHE_u|o(seT*@Sd6JV*=h4T%7lH*f@hqhB z%Y6>X)d#Y@tR>eE74$3(9JY>jj!)ZtZvEy4r{y)(mw~+_MhcQ~Yg4eR{1t6B{2kCt zGf+@N3{5+e0s=)ustK@rXpjujz8qpy(q`~x6VGY)1s?|Pa9y#8I~+%4*oQAD4FPOE zv9~Yh?qHuSDod*V;bsJAIIqxFq~rSv4+hE$ii(Q*s?-WAEXgqA)u`BZ`$tDk+LkF} zF&3CLqJ+SRef#SldAh;Uf0ixgJy(dY&rZ0sh-YkZ=>6W`eU@Bi+-5wW;0w}p_OP87 zIkC-l4u*?78YikhQ}t?WkNr@d%v%IdKfB<-JuYT6r2dR(gaP{`pk{KqdkI_R9f z8eM5JTtPm(C8i-mZD+DN-K0vN7f zFW`E;aJ><_C$y{Pq^fn!>z)W^(NKEo=lGvi+6#6)YH4X2?C!m3`0Kw$q{~`A2(+SF zAz5ODn`&3&Lw@`DO=jMCegl$1tilpiy^et&pB@-aRf*;ud_CwKIk87rHs~&xyLBR7 zozr3DXT1*j4Iz`!GYb4pk1huMB<7;c;L@gv&=%w;EtJSE?DP<||X+TdMI2>5&Pd|A7pp%Eb1v$e%k)B zijJZq6%XB=qP{?;GWQBnYejqiJNFQz_VorWBqbROreKS-dSdTQ!&T{r0)R_~k8)3n zRL7*RopqPNxN$mWt~1kW9&6<0wJ>UI%d11?NEW<~psqmsL=jAgd{Z!!$2@ZBimvgk z%xHbc4N-)Q8r+qL-1$PR>fqA7NQYx=<95xAot(C)%Z2xP=~tuoh*|ms<=oj9P0C| zDFuvZ{7O?PcgOX{jPHGx7pL_%VAUjyl1*!v5J1pS)nB;W3Xq33(X~P+K1~xJYQ6MqZR$K1{STJSlOPjwgBTX zEWzM;pIS4cZ`j(b=gA!opWg1ZAua~eE12!Jk=eI@Hh}U2%ZL_MfM1s?M@e6P9&e=U zmnr};{X9KM`qEgp30?&`6eh?CO&W5UYhuVim^3q}{;OhD21bLNan6-8h0DOg@`oov zep@0qK%Y(5hN(1X%M=TStz-UdEj$R$+!d9=lq=xYAXZa8WZw$6?z@#&tB32P)>5hz zlF>aN1iP^ras{6{n-O+%mTB8)nJ3AbW8ZkDwH~b4hBB9F7Gz9(OHYkq%<*HA&soO)ZMj;D~4yb5VY<$wlsFt_Y2OrP2GS@KyoyF8MX`3&H?r-2ervL zDZAHQr8p}+ES-X}Hzw~C?5ueBqKInOoNdFhsSQ*88DjC^k#^0Ee$Q?1#4o+fb2z4c zRa&>4*jUT6w+A!N3*OZ!jd5Q`{p6Vdnelp%G9=Hke;H!V%^O#s~$f!P4$SH3Rku6ON?5y4h{!=#B z(`gNv0gXLKibu%xe(6XL(=_F{Jvz=G3-%bL&Fqb^vdmmUWg15ULwgV)-TiEJjd7|M;R_<*t-&-8=maY}T$APh_P~OemUk~0+k(FJ1 z{1WoNk>&pvSZGiVDW8$QY-Ue3`}8#S1;xZo?jBHR1#Ez#biV*s zj1|{ac0}u*6s<@ln^yVc!O%!FjkJVv8+OYZ8PDx$bClKl^d2 zm*34nkFbW?dz3n$Svxcb1bS;ylClvMj3@9LE8Q=YRcT0ru^vvn;-vgLeR5agdVHST z&s!pjz#l^S4;LxAOEd9<@Ra}nP5vbXt_G_sbjp~+<2pn=QzS@&L~i^Q~i;&?+3Tuv=Vbi43>tPmgN|S=xm&RkYchyaU=Uky`joxctQB zoIy}Ohx}j`G;)!SQH?8U(Gb;HT#m&XcE6aycsCOWHU-GjJ-jL4D*GFEH3a<&hI<7D z>hKr1YZ4M*r+PIhFwM}KCY>RTpo79z>^aF|{68qAK8<#^&lwdq`N&v=^)SsWnr@B_ zH%k_2t&&`kY2auKe#zA*bFu8^K6{YM@kI9B?~Ep!hkGQYM&Vn+h`frUR&SjA3!r13wB^&r?qJfNal_!i>=2S zi_9DBR%okU19*QX^y=%(*m~()PC=vGkojJ|d_WO?xnfPjyCG3FB zfuejGMeBp_n{TBWXdVU~AI3r#a;m~w8s5Vj zjCqpgyYE554%>`M80W1+1x-J2omGF<%;hB1%3|9Y!@QX3}?bc+$^K zh6ZcSAuSkgG%;*?I2tu;AaUu#WR)m+G}@2KGOn*ApCt#pjAk#0+_~a+o~&p%WJ!6+ zANt*o^xl`!a^L z&CR|OJ`pbuVm{a(Ik6*K_mAJ`1J=pe8UG;a#Q*vE_hoo3y*2dk33V>GoNIOmU zR5uaDJkyzbgXvXp`d7i_$H!05UiQ~YtlDuBBmuKHj@vRn_HRd#q%n$VguwdZ@6Ybi z^Dx9$+;;q(BtzcUCJ#cMu!`*C@|{MU{&Tup?#MafX#HFC)i1E_(?8O*kl_In&Tzf2 z=BrU&yrndx;2pWg7bHWZAXc>s=NSI14paNZ)e<(?Fgkt!waZouXE)}x7IU~mQ&)Wu z2RR1&_o?f~Kb%P{DwO$~7t+|JL22tn(O>c|5wDE#RZL_;0x<*fm$yy?U-;=yZ#mgnC87!Fr|x+Q%HZzjH(4lkezhvMqoUg8CL9xak4r}PVd<04x7-B^4+z7qJ% zuu$hpz)Z5%Qevg0X5VD;R>|-q1=w&c&q7u#{uHk&TG9Mt8|ZHPdDOj(u^I8%-Fd9T zsWAF?(kG>Wpif_k^wIDIrG(jZ!yQ@#xrePg4}XC}6QE2_t{2K})BzVI>xcRlW8)|vbJm}hR>$hvZ}c-TsCA3M|Zao}KDJ+b`Z{j?uDex$uT0d^KsC;v!m z?bR~;DkD{>+sO@sy|Bw+XxNN_je0s#n;{i5Qeb#&pO*YVrr$-h3TV@~$Tqcs-oTs6 zxa5)C5O1Z<73MPA%mN3tXyc{7Nz6I?x&WDcMNVwoo!a8}vJ8J>X-SD##;7M23U>+W z<^M7H2wOf4$P9nNajS*086Y^obPz*0I71N%*Gagy)&)_%V9RMbmZ5?cCYAeu{#Oof zW+ZuzGz{UVQs-d(*;tg)LUZXeVvSX(X?2PP@3YonajM?!18Q9?} zGqvdr$E+FxX&1#K4xrcb{T>F|loNG_Q{GkIc&c*xz}f2EQ;0SDCYxVbQgGNrgGl=2 zw5ih13_>R9pNNmA!Xu?Pz=vlm#-eBsD1-&sR{Evkzg}p!zan6+3)rYgs>~pl`ixMN zq>EpT78X&L(&b=#xyB? z4l+=jK;{ivT5qk9K-csR$NN9%rCVyLoe^@mm)8kEMr2Oi zK-9ZAy%>)|pMGk5aPS$h6P$;;EajwBD1apV+vH*Ds`kUIrE5>$8V(_gC2>w$Uda{j zE3cN{UQ;IO*9b0dnQDW`5o{fs*fpSs6t4T-Fm2)v)hs~AU|a;&1Q}ay){M`0(0NHQ zD>Xg!cYHTEqT8$OvSDhbNqnnzdBty&!_!nUU@D2aADybcvHX#QWOIY#a-MNpwRNyv>bl!ZiJ>(W;8fZBs3$BK|JWYg;hcZ(A!xW9j%U zp_*b4(0@}*WI(U&6Jz8$1b!AQ17}c*?xnnnQ^$U|7SlhhNjjrQp}3G2Vm6IvU@emK z3LCF==NEv0Q{x$KMuk;u*1Qf%RH$gp<>k4hY$mOz?902%_77Ilx;%UXz?#!-fhuqf zF{C2sU2FuvBl}_Ej8Xh5h|r$MlOXuSBv_Xp3|YwM6_|)o=Z6#taT^5$hSgfYd_L9F zY`p@sY5k-m;3KlIPwt6heqnx)1i4`WDr7om&OlH#DjyVp$G9xnE2t35+gqcxD~Kr$ z2?o^pAUWv<65}L({vaM#a^X8BtdKHS@>XLYTuhc{>tZ{2)1@gV2Czf!O})Nq z|1|AkMP0>G&oo=-<_yU+Y_o_%)ihW;`>wehQ;hLGKHH)F{sF(?7c92UXCK=%{4x1? z06vdS?1t2N+FQ8F9%Ez(pArE9B@iNRK}--7*)GLgl6A=db)F!fqlryMw&}NWf$lj^ zPs6f*#tKwyQ+!I5w&{UdWC<#{Svg0*yw!?Pg{7jhy38$!%EameF|_%Li2D69u?@?E zW;S?A?ov6-si+tBEjO~jqmbHR#m97ZcLmYPua!>RqJ?`9O-!xyzsF$zw0i<#)_{It zv!3>zJRc16v=n&U1{S&By)HOz|Jh=$>0QpiMpSX3da1+?OEb$58`_| z4;1MP1AMKzEe}_FZ9I|}!eLIzi7YxJ_|E}jOGNZ9{lE2#H<&v9zxB)imM>W}5rQp= z#pku|hrOopp3~;y7FuI)RT)}ToOhc;--NnUcBS6N z3iidei3U1AonTm3$*JrB=3jL0th%19@$_ILgW~pumB#o<$mNu87+?Gfsw>rvkPlqIE|(dmaB-|_c!troBR_T^V27s+J%>1^hEeM^F|+ydQ!Hr_&7ic4*OuaBzbt5Kt9F7{DYcYDaWP^i)E z?4Qp!Vt2IvF_@h>6mY@q+>fN4a5f)F-#mq{lLA%4b{-@~LwSab0_=e3b2BWT8p3@s z{OetE3ID@OG+QZZGDVW$BCw<#c?F`b#QcNYUF&*5@U|ksYhyNMUPoQqE?GKcXO7l@ zQjQDX98`y=zVO7Po%zg`nfp z%qC8f86P=DhJJXw)yP{5sSRA!2A(4_bk6V>trZyzWyo&u04%MA%2g@Yh14!X4m&yP zm>s-j+q87GLmIJ!DYcQTv(wZxu7+1Bpm0MERD<6NKVp5#SRj~uBZtY3}^=U`#`+FjCYm}d4r zi&Oq$ZK^PzdE&&fKO%x#^?ou8RM|Jf7r81$cPbKr#x_3Ud5b~(!yw~F*oF8pEE(xf zH)&*;O3X13?O>b(tY$W6xG__Df$NOc{NbtuDY z?ra9Od;niyuj{~|3Y4V$R^-Q8eIA>Cjs{5Ia zft>_FQ`#J!*(=_^SytF*N-}qfC^E#Xx{JC3ldAKYDajQRT{Xp=GXwO_&uL7ODaXX; z!HcniXOCQZR|K{E`ceKWII{gI%~-1Z3qgG;*h+c>(xX9i^wM&}WgeB|;D-YJ0~X2L zYmZuS0hz^(LtASzP0mvnbNYYO#->s}lgzRD`bz)5DjahoqBiCZ{_aQ329Y^7i+Q{@ zCxiSxh#F#xtJ45R+F39=VjzlkX$p0bf?Y4ouC@hx+hbYa*&Pc zo7W^L9VrQz0TsIl%ra=+z+Bl=C; z;~=^J4R0wopq+KROk*)bSRmDyKuc5U3S!tu*ledZM}hz`2V=3vE$H=%pD4sx2+H(3hte=PwBum+4_vYYlw; zFH&Wv;x}NzToR(<&muWPnZSut5AdlA-k-@S7O59T`;t>2-kDRs9U{mtcQ^RWkoJ4q zK=5kM_*5_7E1Q2XG9xhedd|VfJ+&;|W74NCK!qO~3Xt~$v~W7^O`cEtaz~`06SL)p z5?TZ!ojSwtNHQA_#>DE@RNgn3!x2=_%rIN3eV!kqj%U{gwl-XoIQpnxEz#PQy7q}r zW-Ab1V9kFtTRNm;I@+`&-nTcgbNApbNPP{$mQJiy^Z=+bFT3C8tI^thh8~CuS;&3< zNPymhW2Nhb!PH@;CXJZGVfGgZ>x9O*TN`iL`$|y)msvPOHd~J6I9zb`70wv$^0ywn zdUF}!)>a5&7rVseM8~6wj*#b8NNGjV&>LfH|76WL-Q4hy=LLd6txd2pVsew7zX%S> z^nsMm?_N-sVbZ$F1y%u@Ej?|`>v`lZ7lYMLFUN-W7kzMPq4<@a>w4NG|7O>Yx{-;? zaDC4(NRZ!?dV&k2Pa=M@pMlK9;BZ@=t!5yzA~V)S zgskj{@%0UQpC>M>X^&y9S9U}X_v{a=c=+?WiUC0rpcpVgxFA*;+elfAFVlv7VCc6p zK^)yGh2llKJIo*vv+=Fq;F=yoizlgK5&F2%-{cB$2pBFc^tRlbUgvFh6&O zk$~IBn<|!PY zO8!oI_jP1tk+Sr@tf-JRy~WV+DUIpKjN59QQlz}FV5`jV0D&z^vRjGu>X#B7@a^$A z8KizjnzWSPOiHqw>*uf0(|nNy9KA}Aimy_A?hxVmlKTJ1%ZtJ(6t8Q%mJ>&>KfMA~ z+!_gBnCHTf+D%+@HQb2qA%ve8_txc zFVAUd;@@QkdUt2F?!Ihgpbz6!G^GsTV=J0R>Tkg@JwVLnz2s31*FiRT8BEfw$Kup| zRa#ObV`247wKP$?V=`8laQ6ZjU`DF5%v$%d?0T!^u&mEs8(K5+Huv)EF8P*(oT17~ z1*gMSwH3KCKAZm!An*fCSqxSKycN8E73j}jje2PP&9yaPFo-ZNkj)@eq=ldXsnc`B zW0@;TKD`=z8R@T+U4C|4cItP9pVQxeY*B<&?msd2^yO9L?3gc*(oYY^rf4a;JXmxV z0r^VlKy3?517kuhT`5Z8{-C^Jts`vLm<1g)KM?6Q!R{F~=^h!V@m=IJV*TsyHLrj7 zS$#jd?CunVaFHp>DI@IskiJNFshBik>OErUY#ez#&`)XD`mZctkIScLC6sx~wjRCO zt=p24_V?0zznOe4f>bb`=%I;e7ZJ876&`?nN#ZtniIeI`3wf=CD|RcNa|9GzJGsZY zvfNP`4CLN^pZPpBABLEBb(HzDC=K$fxkwHx?{z&he`&Y)U&``3J7^ic8q5Jy`$&E@ z)$Oomhg#($CkodTuO8h*E=_JS{bzGyp=es2O%=_gH6mv2<%K>cnO_P~ zIE2*RW}tL-kAsRaqR@Kku46SIIO7Y1`0I7TDcn%>9*eG4ocG#{4(#v3!zRanIHZGZ zg#fxLEF;c@h>E)95fUq@FrbrFlyu|pr|uxrYsJSh78=oBmQ4DUDoF%souLH$9TLfw z075FdWh!xNvHs+XS8#&tS_wp9! zL-aKYt@PHz$||Jlqn$c<7~cr>=s{kmbSz3(=0Mo-Q^|N+z&O;_CKq|Nh*;Dty#RdN z5i95q@h=hUa1T^e9~`DKN$tmiR4wdg-kKL)!Ex%|u*S;hOhE$fnl}_U6mFe#)g^UC z&Gb9Exr)G+Fl70iKf2=!u$zE~ogoy+*bymGZbI1mH1UbUca8PxE0eeO5h!MsKH$m8 z=))K`7SY^JuUmmtJFWJ)AXP0Ui*aG8R40<-=~DDBVbWbE{)IY(X&XiT_7j|*8;pTQ z5O_MN_XjHBbKVOnc80nP`LvDkKF(pXi}oBq7~g)wc@X|h(C=-|B_nBO=wFdtVL@=0 zi_3&%YjrqL{bR?yv}7Q%*bx53hgk}^q+(m4@SuPzaOARkCK678w0D!(nCwrgip zos0+O=Ke?z&t}_qe+Y!#A72N`d%pp-tcs7{>VqLBY?87Rml$QOO`24~(~u`|LF#AxY+HH*~99s3bsE!h^g#!jmh$<|hN6w^knPkpBBtXHrIE2V2g zuBuzM2Yc2MAmBN2y3EGRY3Vj`_sEh5SrhLZJ*}$#aI9zIS9qXXza3PMrL~A02}4seR768r%;^t{em{z1di69o4!68S@VF>nKa#{`BLD zq_-zVtiv93I<0o~WL7!;LV}jdISQzrOOI=k$>?7Gt7ENO4B3R!(1d%VP#&vfJVsJN z7Hjh^akup&W}NFPVG}*gZZ-^(MhA_UHd<%p=b5HP-`J&bN%vo9-w_=g`D{mLwEnMR zC#%>G12}k(A4iBzJCZ>vvEjA!%>XAJIGMoC_gaLS*3!(QMYCbx_KfJ9e7H%nUWP~A z8P0rOk&pI}#tS8t*vGY-8&yI_|*Z1!l%{lS~dRWsU;JGJv??r4L!yAj2H@s05JSw-pT^w`xsrLjR%l%lYw;D@ z*vH5W?wX2D&|RZ7fGJ13RTFiFjLR+#J2Zu5J@i*mx+-SP zPowgu9d!MAoKgDQ>)?NX|7k^aH5y-Yqj1Z%1>2%xyxd-_2+ayt`$G|Tn2aqOcuY=Z zl*n-NIGwveNd12BRHj1%l)*4`?W_{ngm<~4Lm9|~e`R`1d{myP{uigwV7XP|ErQ~~)_o1hyP4xLglG$*C1DdQCJg6-K{b4Dbxoo=2+Sm1S3T|zo2rNB+ga>2-mPULwSdXYg}!EmW~4|Pr6ygZI&c)a983#u zsx2pBmv)WxWG=p2Tb!6j&B=f5+Ay5=zS2z`niJvw{U7(``y+H2IuKTAoW1MbO_7|9 zxee$(8boDm%rWf46ST5lcMkF)VKSnyb8Vf=^#;oanl0AM&;k(wJ0q(NS*6{adak`; zA}QZaaBn};WBJv_tfD+q=BwXdZ+Q3DiDiNK)k!6)X{E5~7s0pjEWmn_6$ZG8BZk3f zg}S7J>v@mC@c|mFbP*OZG3%p#o$2x(M&O3ENO?{9L-F4ZS5HoR;B1fhNYQ8t%)#l@Q0pm+ zXx6|J5KZB=2rVT}_)D>oZ+U9d%Ho{Ew-Mzad!xNqq!#+Qrs zV^`|j-ODpTkELfwJ|{0H(qrG-H$r(vCgYP{1!F~Qe1T6B;fS`LL&nIjVs#?owKg`z z1)2R`(L*-kt;C|X5uG4K98aMW?OKx*ueG`19+pB~)m=$XAu}*N=}KpUXZu`#@(h5<2bAMsS@h2WLl5PiP2CYU>I}f zY1fRlZtoaF_C&<1ZmiHWPSms*p5M6j`au2aweoH^)_?Z%QK$d!RWAJBs~qkw6PQ9< zk1$0kLLl1!x$7*?itE%tIAX23m1t}|eK!6JnpeMHD@93GazR3w*_tN5 zBTXK`f(vDFDWgZ*RwG$DZx3d??}rsVBTg)Q(f?KWf^gSm1sa|=vQMkg{SDvnSMKIg zOry05#;%$hWZIv1=5 zHCAda^44N>|}AZV}8A|Wh#`m!5w3($hGntS$Rl) zL|29V_D4cG^PfN3PM;`xO#6+7G5PWe;!>3EJY=OWwf3M14ycYlg1J#3pYGA`14c2` ziZcKrqpp+`i8G2ckW+-STv`_l$1Rtgz-^Vj=_^|6E#+Nz793=HBnd;KyI2K`YT39O z+EeuJ$FdCtxR|EulU67MZ}(3n0SL24fw?8?c4C8ia4=p)SmEx5Th&%kD72s*wc`z> zw7DGGo?l!IpwaCu++K~ov0HKfRY`az-gPSJ-T03vtW)pLYf*^Fwe+*(xe{mYN&t}JUh<|yR*+}nYm4y>KsO=2#gHhq zPZKGHMM|muR>kt%BP%nQ8~~>82ziTkX?N?LZFeIACD-mA9Fz&W*G4LuX}G6Mr-x>c zt>HdRYfW`QK3V)o9=-zCfxRJ{_dTx*C``gvJ^hMisby=wG>y6rjlC4Du*8}^%DZ}Y zY3(r+HDvj;Wa~8r1!?P%TS>z~m8j-C$WwiGwtmgDuIOS2I$mGlS{JpBX7H&K40Vk(E$BqP?LWgh9p1rEt;V1%8iHfgxwzOjRHJS1pES6K~M z^D&l0+Ev zL3j5CG&3wfjnG>9AxcGF#1SdiJm@dCTixZgHa-e4BwB-W=-r{pmsQZ!$lj=KC3O$^ zS01Z4d76E^lo8mN#(cDtTqpfMR*TW+gWJH)O&c&2^8hQY{Z0y_J9aiOMJDpc{M1Mf ztq$fMAvrhY`kouFB^)XrC9A9&=*cthaZ2kei~bC3J?|1HUBJ1})%TorxXJNZnqyGQ zw$;1_rxjIT7+M{nkLGnt=W-9J`L>uymuI-?3h?c-25V_5z9AkO9JYfmgNHrd;u9G; zQ5H!ziV^vsZdXg}Nyp}hP!^kcBIoeAh3ZWk3_xit5>^C^{31mIL}$rU#eBFt@lZ5R zm;8eFJr<=-PN4wpu)w=Q>LPDM6WT6ZRH^Swb5GsaDD zx*)>c$tf%QJ5oIxV)$CPRi0~Ob$Vz)xIA5V?QYOtqlLwJ6&XL+jQ0>`@G74>!F%mN#w|*f zv6DK)-Fa$F9s9hnv^-H8+d!U+AJ8T|rmn^gv6f)zsIxk+jz9B#pG5_t%}BfR8Y>8v zXenuhqbH7+rR1ggzH~9rcXbpOEs7qF-((CuF6CP(Xj0U4fGaz>j9=$VlbfiK^TcA@ zxEhT;%Ui_pea!n_R!zDSUb&(a?XRV}cIH%;qcy}|m6D>n!xM%NKnJUO-dpb6MbJ1qw>5up&iFZO@|sSlFj!AG!EKNt)P(p4sZ=o`sad$V!!X+c^0tC z2dyO)r*no*ozSL?7P3c9IGwl|mK1(%;RVAe1e`yl%1mj+=2qN-ue*SCDxoXO?>_vik6-s|iAdS!k3 zD}BqW%~c?dcNUK9GuYxJg$F2zu=E);*?Q%}FpCLYM$8`R=UXu7c$vL|NfbYKeY0=a z=1@t6a3v$PLHAVb-LIa_6l?kZ9#V0!X;`Pr@zhHXROCUhT8OYt(FLi9SU9Q&AJ3Og zG^8Ziufxs;$-NmDeNcXjN!#4_uf%rrrVP$yTTQ7|(}2O_7{3mG;G+s(SmQdFMfI-f z2+w@(YWLfCmy>qVChNn?iJ}}_cTff&o^wZek1U;J?gG`kn>-$ZS%duah}=gJpalOS zx^sb7$GgH^gJ88*qw#k`@%dPfxGnCSb^NuOfZPGbL>+rx4e|bnzkxJ6IpAywR@l(a zNO^G~Evs-~sM5>IXJ4qbQ*OkO>Yc-0GeJ@afR|1w0T4CRf}A8MoKljBil)7)Gl585 z9=n19q+t2@qM!n*f`!EtG57T13-L~X#4QpbR1NGVr4mac$NB4ER4KLDe|1O1hjV^{ zlmvjr?QEI(%syHe`u#(w%e|cszV7?rx*(mN^0k_mfnXL^GCL(yRRU75^<#L9+?k=` z0fee;$?S3%wsV{N%2C!Wc=OaH+@;V)OJ%e=Wc#iRsDOF))iD*7CHdf+HYHdQy9M4BW{xo-b6 zwi6NoG*c3vq@atvuS3W|0Ac8Jynvtr%Tx(vX=qqTVem{Z`@EiSTV88kD9oXL`7#Q< z)A=ivRk#Y?&QrxrnPg=?Zeg5ct2c;bX?=G~YUC}q^#;`Ha&}|Tt z?0;bt?vZY&h!VSb`J=2vNsPGGU~nJ0H2!p8F(v=2;NS>FI^rzB}L(K&IkJQ`7;+CqFng%39t zB79MNXjXksOMuxK-`ilmDU0oWAU zdn7eDB_#66E^yfCSiWpI5-TAJ@*8yzleNNySGtB}e~i%c>g2u*;De){Yy<{69VPir~fP9jk zM6Dv!2?ddeXyOGak-W-&1)Rz<;T6`>tes-g73NM<`*3^BX%S}?NW41b1W73BjQy@X z7Rf|+UWejnzFW@7HlNDO^L8aUA?x2%bbF$Hh>?sfb?0F}t55Y*z#!drxxZf0^MtS? z8u%NbPVxP1hj*3!ZuIn-G~83_*rQi9oc( zz`7(*L2Du<@KlQ|fTuQV%(N($e>)GR>ayVZ+t5M`Jd;@ zo{@7aet>P67o*JP8so32Qa8f8FCbjo@xTW|BZO?{uK-XZR~UW??@ zO!@}22U^^Re|)NOa>losDM*K7GaE! zP%eX~Lgr0pwjxGME9kGRN)I@H%n2noXdP-*wvRsgM|d?{kwxKi@fs+W!?~`p zR!!oG;#p7&jpkxGBKOA@^tcSumUxA?G{6C!e=9AEqwuW&?qAN8w06eD#+l3AlHu;m1H&*JsN*j9@gq%ys3`-3~`&w}^qe7u#r$}v&Db^dS8()=C7 zVW=%Hm;ga*o(=+amX%yf)?0Qtr;Z)RU4T_dZF|_{NG`=0@m?&C! zqSrrvBun@41Wrfw{!W$Yq7UdlJ8gMzQ^8bb`~2L8R`@%{ok(_BGO{w(VIj2B+;!*t zJSzbDO^Bn`ClphFE0!hg<3I_4=aA<|w}2U)3Up@e^f z7UJyKU;-8DYVo}C4YyID*f@q|zzkO2IU&t1lj!@3H(7s(Zvf>fD~TSX3SWM%qnxCJ zRW%#@(q7~CpjA{G+Cr`1qFS6>s#EwIUkb0BCa=|Y`FGCxtM~+*rI#;raRhcGI?MIb zt54)hyc17vx+Q-qFNi>u85XRSFAI%MzSKJt|Jxrr8gn7i1tsRGiT)=OlJx6AN;Yz$ zt#8{7>=&lPd18a#wn5?*NLijsuCcVP6j?{{uUoXuXGTdWu^aePYxRb`(yR&bce~2> zpQ|>qzRpwLVLCjDdOmyp0c8?X#tRh+zQ~4vch3|$WiV~pN}s2QLN)UnC~Ay!*+nQN zHX;kL{Cayb|I|_H(h(m8bveEnILw(2B3W>WxsvAAb+>)0aLdJ|RRPGutSt(1A(B0L z|LfV$G6vRT-2t=MTY}GswYIYy)pBRvUyk zeo+uG3yB-;1+FfIev%1ePw z_Hty{=T0>@|DihBy~8}yZ^hJEa**7>yjv z#;<0`^>DRm`X6j*7)7zH&wtU4V&;0ePH|_sd-2Hz*((0gb$=Yt+4WaKLTbqOg_N2d z9Fgo2`5n;As>6;UAl<4tAv=l+8;M|Tm)Me}U4zDTh`tZDLSoGz$el=WEaS|>U3q+k zT?xY&aYq-_N+>IbPA9K-Oe?!TQr-KccP6$vfpy(mk*~qdim)hS$w?|y$AXf$!OWW#CNcF#@RFE}?xSFWd6~$4BU!+$|%glmA zvAXsySZRKLYeW%)?ajY!YY`|W=>W}BgkmA=-S_3Y!(m?5yxwYaOO*Wh@87Nj=(`4zIDiBUOkdF`*7VL5g?7yQM0tE=L#2b62 zXVY38IDTU!2_+r|byH!z7%d@TU}?MU`jX)RV=Pi8SGxK=0m-IG zq0Z1uTGl;A+PCS$(x%C3cAQxRN3$+RfHgFdB}(eBGdP9g&PGtZ5+?kER&Nqk@|wP` z3qqVcHU)^CU;KLQ#?3k58jeO6yPr%XciG(d3*_O-|2G;N;wb59ZG|>p0yyw(hcRs_ zB0GnXrQch4IxN$n*})1*6Is{P)}@GP(Qc~(K#lk~Ug;=+B@jKviY8rl(v;t+ckdxf zI?RX%ju5|JT7%wIjMP#Q+z9!H{r}_q`r!Ca>wE&&{Zg5H;+yalxy&lzzQ!nh(@Wk; z#f~>RNcCY86V11xq6aV|w*sVBxD+^pd?^g7)v19Bv@TD@BBhEKhdl8TmGXU@j%C5HT!e^B@36NB}170DO*+z3hdJq4uBjiG=qpO{(0eoVl1+XTko zmh-sHHw9ntHRmqdw^;)C)IIra+b?c#C84jH=63qaSU=ksMnibtPnW$?ejdBSmtF*4 z439ADivla(eGBodvwk-+&+qZoLRZV}6|&WGggUWJnSgCEtYq zL!Qq91Y}D(d+8+ms&E--tqi&W=!pq1Yt-`|Cm7e2PJo7qR1VWBZ;sQpe8A}YxF94< z_r9oODrl~6aIQ%k9^CsF=b9)U^F-ftK2k9gB>q$IhhwQSC+yeKmmbl(I-o4W`+NCI zL6cRU^uew7&r6>o$5pyEfnNiDCPifLzEF1Am(f+{^PD43&Lro?!+a9KjayyneWPTo zs6sDAKsy*+KYsDFk>_;T*9(7HmTjf1kCiy}enaj@j=IS`f~l39(M+kD3>d3mVsriw zPoKLwm}Ork&3=A1=;9;B`3m!!HnqQemQw;kn3d_3)5w;7|Hj99&pqZhR;Hb4U0*~N z9|Xlw2zPa^@8XHB^Q4qRpOPgvkRa-kQ!bh}_*R0#C53aG9OfNAv(Qd`4(n-$dr@_kKR+$NHVbhB*WCLqDzM$9R#l!{7NGbd3REnD8L*G{tby9=<#$F_t ztQ%sW$Df$YeO}QqHMYMFH|avklYjHQC>JiJo$LJ;QboQ4OiWkywZJXvQ9kwG_0chl zeB%7kAg6;FYvpU0bFaKwzGl%fwap?jV(cFPcWIh1dXX_h% zJXl;5Y==GX1WC{!qSUKVbQL?4<>vGSG~ohsc9M&gV(q~E4uvZsA`=wuH8oh)0~oKh zcCRWL_T=jEO(&DW2=&~JPbyLm-=sZb?yA(i;82Jd|NlhvMP$)uy^C;fqk)D=Ev9a( z9zB-5`2MpGD>!{0$xqT74lHlGWw3w3)9q$h*z4A7(I(jbyM!sQM@Y0EQWw5o)&swL z5bt11_mZ!%RekII&6LUImW+s?ikQRi-C?HHjSq}!NFJbWBH9%Gui1h2}vzf z%d)bXlr$wzERSM-c;_ln_i~TZKk?;jr0vshHkwz&iSkk`;|cX;nPB)UG#JIDV0{mjZXQ#%;Q$Jn=^vktcAm1 zdDaA0G{EMTfl)sE`!3^i&|VlzGxa!jBjY3c-NpL$%cp)y-LlQZF`Uiq3Yzt{nLd`)br-fK1yERB(65``1yi^|M z+-SeC178!2EN5+CI@ffMRTLY{k5ciLe$LHneK72)O6d-F^W^*Ja^fM?y$X?S8)ja) zortSZq%NIS$CJcfV?T9P=WJ~*mX`TDhUomXY}=FjX=}8Oy7f(PDJ08~shI}vYu2Z=nyBf7&X1sNe?|ITxW?|uN)1t2|U^ivJ*W3pDS`lU_ zMV`yDm2k#wCaw@y+D3`n;&nf?yg<|x2c+?hS=#$hE+RR4D!Kr+Zd+){yU-Wr=&ZDGePkiEZtRFxZFwowNH9C>az4 z++1Z87d)`>e&p3dxlY%2eN4VubH;Ga%b^#AIdfjWh@QK0E*=eVC<y+6FJD~J1*bJLU)(%b6MXuEBgn=;R8nV2cx(wTD zlb7j^YNZ6wkbAVW_RuYic()$<$97@;z*_tVO|cIVC8DoL3e6UL(Lbfl-!zpR)}mc? z%kLDOW_0bj>9TO|u3a78A6_m_$o>1_ycAjG{|fv5cWY8y1_uz%kq6#23+e7awPib$ ze{ofqi+ak1m!-iMtQrv=ygh^1NI*1&jGSiBS^OM7s6&Vb97Q})%YL=PJuGN<`;^lD zt#?JAMe=^H9v`%`->JOx(lBzHx)c#j39+9K=BL;io(5s2FlBPYMBEnW6Ceh`P++am zN2}`nZF$lvL4ag2JlI`U(fm6d{Ld8mnF&4 zXz_be!SnSV4@*O20|NZ#lC%MdWUlqDb=ikxFFzzRNTZL6*iT@7X5f0!b@p5qq0r=~ zq9PuFX}!wZeesh&9p?SCu#R)#F5x!sWSbzsL79vMQd8I!z!_mxh&B9%5u~k9Zk{5j z1@G?@3=0$&=4Vp0ZUsd?bkWbs891zS^kPA4ak;6{2%_H5{MZGLuufOZGDOd}P50G#zU#o=Lx0Y-Re7<2 z*L3|7LhfqC)fJFW-mtfU7vKRPAG$97nq5Jtqg>%?pamFGG}|2-L@rg+ufhD!@eYOI z>0ECtvjMkgw_nS2#1ac#3l4Luj-J0q^U>m{7aW?{WTCFR?(y(e=3IEB?eO6>JQ}sH zMJ7?+LxKo>J7pJ?S%}yhe+%^w4ckDe)j>Wln7UN48%3_y1fbgD$g5Lrs+hdV8c~>} zToW#xBrL^#`R?EOrBoUhe>S9KBwg1jQ#)}gD}Yji>Va@LJB=z03O{3|&>VYk=tEA5 z%opo4`qO0PHEyA3&gFONj}8~kTjDg9zp9P{h7&V6un%0wd=L2k=vzjmHG2?xieP5% z6GKCt&mexi=FO5zo98`fe|fVJ>o+=e{XyYRJIHy&z80LYM_I)I*(g#M5<=>GF7xcg z7Yq+<>Y79nWYl@K#wfj>s3~c$NZGc>&#TUwpJH!|jnu1pwsXcuE6Q?cTmwtihO(~l znynUXi{2DAA(C2cr4db`&Ly@O4cJNdcsPA)HBPFrywRe&?Ui9=+3`mS32HWF%QO#Y z8C}}WnSS+uU-gdx_)-nnU{DB!x3>{QA7Ee_N=$?dSw%u<_;7}@+_FHdPW6D8Hpth{vc4|7UrpWc;M4ALPC4bv z>n@z&1?TGi`kpc&t4;XWt?B0B-}q@VwmK5*YEm%f>1egA-``D6p*Dq_-sI4!cl#tg zAlk{>zf-)oUXh|O2wR;58-4_fFmaM>Bp++LlDIln$7ur5ZX#cAc62tRnIGHL`sn6f z>r*OczWmsDmoV+Dj737n)MOu(f8ujlz1e!wykb$9Aut4NvJE1{OtrS!Iq#E>b*Fn} zoXF30efaJe-y^#JKo@6U!`?$-M%>48)1IcdtkdR|kggo!L%gqtuD${6Q;-3$|8r2H z3pcfI8X8_2*o^Mq_{)oBs}5~NU;pVm&ucU=_41JjgtZ>2`-4S>ja|rG8T5}6QhzeO7M ziF-G7_(^78jUk7j>d{r~9bFV~{5WLX15XQo&v5C9MDq$QM||8f6YF3<4cZ#agtXjw*FYnH$WTA7Mk# z2!XyG4~DO;wF<>;N&f3fllDC6ge0!Ox5?iajZdg?la9DPt*vP{uM3pu6>HZGtnY>j zKydZ^>FaB2QFpKO!>K>DdA*Sl6)T}0zE-{jbg{@{Y6y_7ky9#O)&!zfV_w-hu;*WX zi*E+PC-JH$^1~}I;UVepa7I8dS3Sz}t>1zMpYBsfbl+B{r&$~6rpQPM$2-X{_0sR{ zQdxm~CE*0mStbL83fROsm^ZM}Xvz=nRh?{vu!yW!j^6ef6P63ck37#5B|kp+s`;~F z^MhD@mpOmhcuj7kpO#zVACR(8iVCI%W5Tz|!6|d~i{|-EM=LIhYA>w2e*K!^(~9R6 zPm^=fHfmE)LTrs=e$5%P8xsAsX~r-#H6rTk)|i0GvM`G2K&q}Op4DKxS3XG?@wwtR=Qdf1Ba z2#eXjeEk5YXvF6eUd$QsYF=nMI`+9I!nOHH*H~Ei2&wE{zXP4vg+Tl0V|+6ZHmNE! zY=DVxLKk=u3gycbTToJCN<#HmTO#Nzn1lsMjN_mzr6-E_=sguqUj(x9?Q6Ruw*NUW zT;+Meue>@h-)^Arp~c(Wi;)rJd~g@iT?n?}%cvu2M!kkS`7)X%ofbPc{xDZ}ZdlIf zgt)XBXz`IEb3ebk*144_R>@3?WLNq2sfES8Z@+tVNJ_g)%=_r(&rbEb@oFocD%ae< zWWPfB7^!BW#<$l0!bhQ(=bkxx|o}v>Rf5#20w*VoSdII2E5&|MdCK8GcN~ z#M00UX*Wqnttx%|izA|r<_`FUdR+*t&1x<#c!flM^MXKKG(f=CO~+1+t!S)v9wkVN zLb(P#lON9Rv1TW5H_`;b-R7Au>7&>0-5VLO?=H7H@$ztgi>%nhyNfef>_phw+IV`DColuQ54A#!yY0g{w@(kgG?LyY(r1MABoJ5Pz9Wa|~% zuk{VtlvmE{q))aMGR(qj_dC}-?w)M@J0QD$eBgi=@s=?Capq6j$amfg;U%PuI%*@` zN{FM*V-iG^OT@X{)rUY1g>M~|^?C?j)xPP{(_RHd!dZc(Hfn!=-}bS;$zr@rLR~SU zz0LP)@9CsMY*M~ZgYZJA1xDoqRqD)MjKNfwPEr$ygxrx@r|5La+`0A+$Bu3iBlB%Y*_InEDKeu6B3T0yk?Hdn?cgD zkOBQFl-@{=^}v9ZTfI5oy5AS}#y`GEH#K<1_N*-}vcTf6_P(4S(*}nZZ7Nlkf{UAl zq=;^T_XW&`atoZchZm?o#Ai4}&dsONpPV zB{Xh9>P_wscMf2w%@?#2%>C?J>o0t0i?6UdCL!qgKsOB;B&N@GSJ6cvuu_Mr#{x(X zI}(}Tn(K`L+5lx)!jCpSu`dN$r7+=ba~w8|0TA#Jxe2_{^<^ri@xSKEV-d(a;BTG5D2KDD%S-uy)7v>L>7y6f zFLqp?EU7bTtnxhLHmG+#yH~U(?ZfBjMLxzmjXX+^Q#*N>F-TE!WNNVN2Q>ff()7`~ zLE&=9Gvs?cZEZST_|>r zg*R3N#8}+TAp(BVxJ6qmbxC-X-hLR?GjUCdvL9~oM$=LeLLAdoOjbAAK4jsNB2ZNK1z(?F^F52OvF z?`H9~g`SUF8J-`UKtmNGzF?KeRbykuYcZWo4!W$_gY{VPGlIW? zu)&SBnscS*vbHSYFQk5fJu&JMtyKb_ys)%O+joB`Icehm?c#e<@dM#Nod=~5{%tpE z{d#DY3qi?976;#JqwvKjfER)4ii>dG5O1M*?TRKT4bimM2^Co8ia{pW$_w$b)T;Ec zx>k{3>n5RSF;WjplQ>zq!Rbkyfe>r6`xYh#pLCvzI2J+v0F(cgZP?arI7NGQMyo7X z+JYCGYVkVVp&EOdEP9TV^AyWk#A*3buNUy82ixMR=`AFpvsM`gU@+7x<)*YoqJ~0G z{KmntLiVhvO-$5AFw&t8TO!BbM~$Xu>UAis{F401 z%tu?9L8@ab_W*sO>d=PE@?bcd8Cz)8o6>-bVY<=JUSj9#&LB;t%w>94cWyf2`qIbd z%{~Egz51HAZbVsZhHO!}d?>fl#qj3aPpj@BV`m2`$k>EjhxM9Tn#&tY9VA>GSl9s< zAV{i-l?@Bu zWthblQ@^ssZPX8tzPRECAux#cK>9qDY-Gxl)fGNh4)1h1RQKwLw%h->t6f1=?JNT_s=1M+pjsJsa><3Ec=n7w@p2RAi892Y{3Nu3bcDUXDz2Gq8 zdqbob-*_jKKnFm)j_m;@N7V}tI`m7w!=z>duLh_HWhWc?e^1)s2vrATWMRK z{-8#SO;VgLlojIYFn0V`59Ftu znBLzz9?m#ZZPB?c(*y%MI zH|iSspUXX8G?dYtvLQ7`S!q+dPcO=OG+o%zqOhbZRzbGtv>t`$T*yxPcaCxy`3w1Q z6u1`E+F3~lRs&Jyhi?EM&+9T#3_?$~61*E5(dHc?Ci6;Rpurhm)RNDj{HFYS@=yh7 z$yeUe@-Qp8@jgu2f+n!MrYy&-@5E+pJd z_vQP;E;2t8=dL32VtayF7v)Jsd7RD)v<1*2@Z!(o-H7abbqiiVh&iZCZ&$5nmrg5I zY?CchUXCEI8eu;nj`oS@(bW@;dg@lY(Q~!;4>Qt1GThJD#B9t2hZB3Qu(7MP+!H=U#+%}TJiX#@8Rv6Wk zRw(;9s4u+jLF6^WMxy&{Sp;<@Z*m%p^!=(jHf9ZUk7@`C6JD3OEBlx`K+8@hZ{e;1 zxkUCL@hVR}OhJ@>ckGVPM?=10oF=ODuh7ldMc3+@Z<~C*YAbcauJHeOA6n(9donVz zSJ)Z)q`EL?fz#Yuf5mxvu7W@FmO0E@z5FFdx{xzH)QNo5%tPMUxBffFCFidC;_vJv zp*D9F05Y^Kp7kf%O!u532Gs; z5;t<7*TS{X&e)}_wtFJXbi-t*!W=g5FW#qxNL9Wvk*!9ay35jNS8X=N2@7Ed?0<6Z zM?xB+ot*3RPOjG2x^3`iU9-~;(e*onUQ<^1BCzoDQ!`~BNwlg~Ze)r7+M)P$CNEDQj9E}{-XJEbIHYP@vbOI<8~rot=70cB zg&0cHI{nC6AWCAvSee^@N)i?Uy^g;21v_kII*XO2&F3n9aQCg=;`;)#f%)M z_WMszN`hgJ?e7`as|A@Og7oP?s)(?I_!VM+c}yjQRef1!Wvx)Ih$A0yBV9I;+vDB# zCQ^0+uyDXpXKU7Mk-X8NwRohuyX~(tP=PsBl-S~PE`5-v5i(+K&Ggiou`J^ROGj8b zk9*AYe4cY3_k`Md7H)R{}y>|Gf*Tn4Rb=$6zkk#K5 z3HsiSDV>4B;ICUx-QzTl_V!TO#_q{o+&UgCrH2fSw3s z8PpNBMvZGo{z!)7YyXg{&LRJw(PpGftfY)qd9>J`6I9-cI??HLO*Ve;v|P<`4wDFX zZyvmr8#u%V(mm{$O0q6FRH)x3exv8lAG6&iYFmh%swXdJI-#<%O|qPQF?9nPzO5Ul zT&e0I)2_t~_Q8zLZ7v<=;(3u?4F`I{?W_(cAiPMZM?@*???2@3{~~k5H!1>*G zlJXB3vvYlVfZYlRMue1M9d&CEA z4~QGCQZ;ZyK$f2Pi)+aLRIN;4trDFt}%%&L^14l7qs~R@ch% z&XSL&_UtfvUP!w`h|w0uqd~fA4A74M6ZuvUq|j2qU-QPc{X3^dZ0ej`vn~Jp4c|~M z@fnI-v!xY|*sp9qHU3*@#M2VYWj>Xi6{d?wCr{R>53-kGM2Xks@+_Lfm>5(3Zp=LS z4?n?(H9u|=o21Sg7Sz_Z#r8L36Y(=hdZDih2`OULlPlX4x?V>?AXxR8k_}aXO!j4= zo^$g3cyUlu$~F6C=$n*=iFuf0#H~AwNq#h93H9XJ zgC>`|Bmc)S8)}g;l=rwx@g8JBjVdY^8DBR?U;vS86#RH38z^X#tXr3KY!5y)IU;BN znDKv>BXmBUJCb4XZRbOf<1X%r9o<4vc?jVXn1wdJH$`ray!$S9M23H$$~lM= zGWkDEU*=Ky5zy#Eji1@q&0 zVz;0jd~LVxeR?6J^2CT$=7oiYt|qUFvd3R{GFS!>E5@8fL|4ywdGg`rJhp0}*mp|w z{syC_qc~C>|0dcojckeZj!4XVtF}xyaMk_}j1K(gOfy=xQTZ53ZikcwCYvRfz`L*&J*#R`-5qVifB_pVJR$8u}adS40{UgEZfltq0}HzFv@LQi{dNEfr0 zglut`r`6ke_^{>eyI(KF-3!3Lw*B9!i(we@KdH;=Dusauu({rpvX$ASx;Etw);6yn zfzZW+uDl&0$|F93(Uk9E&?h@9+lzi`ODLaM%($%BHkh&iV^fkE*vl2Fk9P4XIaW!` zb^I#6^{Grrrk+hQrM~(fFKr+9XJKIv`+fYgyng4GkC7%G;c(wX&197yuZ`1Lb#pIx zW81-uL5d!&!?b~)FX5T=-?TLAQ$^%vQgqdYp8OalD|=O{L^ZC&cCF-9d4Gx~cGh2N ztS}l_u3hHu7c4mV`BqUM7DK7+G7DR^Uc1+8MIY~bYWmSym}mIL_;(IW;;5iGVzy$r zu?#5I6&h!usvhk!oV^CcL2|KqK<_D4pSh!=C*5V1D0SWUCS9w>RrZF#!LJ^E^)bqz zHLQ{2Es5U>3Odn3j3W|i4ure; zESJ?1_jcPFO0pt%#~kVktmrjQ=#JcWqB3rXn{^&HG7=qi`Q z3?Y|W@W-~H9En$arZ|FqEu5{Ek2LV+G05Vx!~R<6iF|P(Rn-@3Z29R_ebBNXpQx4O&m1+MWok-^|Ffd=c69<34?F?BO$$AN!u-KCa-SS z{{9_batCWccMD~2l@F1iAw`6Qgzc;5jW(1pkJVy9>%X^&w*Wy3Pm`;Lne0J((r*8H z!@O-daS@j9)ox)kwpn2RJTG~J@UU(IFi>7kS3rJLx66JrlY)JcpTm0wY_Vs9$TE24 zO>fXaNqSrt;)`mI_`UCET&!nV@NI3x#Y>wv8&-BrCKDXHMkmLt+)ASd-u5osaeY~v z;jgOG+vIBSk!=wn5lJHy z2H)!@<_-Jg$8&so{V4SY+p%~tZwdCi!RL;VU1je5kr{)W$N(gNpuw)@@?e&SS#fcG z{0xNPF?{Y8g$HQ5!$LWIGGsM&Nuk+}9Oo@m1a#T!=8EI%OTmvtHj`2Y7W0{B&*s;c zmgGnlDU3zzrO1$$RYs~`mY#c6_nHnbimr?sv6&D-!M2|2KQjUAeT2~-)4q)fGr*GCrhk8~-vW2WK&aq&;nO}go_ zs)ULV&MZXD8p=`w4=j7f9?}L3dn2Mwd>*LpY(-p4FAfGgc=zsWWpCx5Q0LYyGc?KB zTKzaMaJ{#;`@vgr3y5k?>xOUtX3BO~O&uKQO_gWPX=2i}NwUVS?ERnU!hvA^&2{`=BgBXDmf9L%O+P_+@9 z3!E%K#7o(Y16JsQQfZfHx_;sc(kg9zG3`|hM_Cwi9OYlKwS$QY{}A27zuAO$*SHk# zHmiv8Jf69)<|^O4G;QeXjDIGLrr-@-_*@~*qU}PBo%6PE2F?fGNc*)tGU$lv*+W$6-+!QyNLJ{#%md7sbM+wz+UqT9U@f zYu#yDyp4O(f@VIjpo-2l5n&nB>;`4ObRoV zt{%17wCDY;i6y53f$;UE>P$RR2LgHT*iZ(Qy0Hb^epxMwj0HdYjK^PzoWB9f(x zDRVjXZA%#|Jkc+42-8)BtYTy~Swf{(!+z}tM*Ym}LB$Kj6YB2{gY&k$X<pMugAPk!=5U|i@E zsD;3|;RdAc81Qw`mtfP)_!`tXo<+d&cSuzm)YT5uhM1?QU_0?;$YNUOP_{u=yE83d zes5V}sP3)FSIeLCs-J!*oig6y@M#X>01W{|^qL;uO9l?dC=_}!&N)mcEGkH1YB|w_ zt4OIYT7iK(r)fsB1{tvcad^6x8_I7Q5zDjO%`J-`!7U3JyL;YW@WITsK#V7nMP+2^ z@8oEn^dP0LO$4IR!mHK!qdx{y`dA@jtVN2wH|rE3;x~kPd_$$w|MDPpF{7)F57)Gw zalft6njCFw7XHS=*7g2~fRr%lX7)|p{mbqebZHC+LH=YnuoDJsFvmgOA_WBonQPC! z;4K3!=&$$&Da`v?A(ppG7>HkIzH6qwGo|+OElJwOy{b+hmPpIfrkA(UHInmMGlNiDurCkZD@}S7e2)hxB_rQg*6PR5+p45QwZ) zFAy*;9zv(DT0Ga+a@HlvK7G1gVVP-Ga2J$PUFUoUg`ZP|ncY07+bf)YhjF>vaAZO zV2EP&!sI+aijY;zQ`z>)I~MF@sv5okYxbjz*&AX(P+oDAlIu*yRO$y-^^_b-`_{il z`krfr8X4$#=Vj9&#ZIeskF{jy4$YWm-{6d$ zVp(AAz`d}bFXX=BiITg@oT9bQYU1>7ynnwlAaG@u;Vb>zC%?v#-;=Pl(+a&(DLnp8 zV67sKtw7|iujK9k>Ly$%;p#C04z)84*y=TgBF=o8myZ)i;%dTP>M+P7zTz-qe7h&@ zHXx2Y^ok^gxzfJb=AydU#u5_U40(lr`}W=Ad3CpiT4CmheB|~7iB3088y^2QKJ1zO zd3gBqx$`Avr#3`poc|n|=I!_3#_e~hN)|lDZK967@emjU4<6iHl}Vu<7R?uHC-Y9U z5gF4>M1s~7yMBg~Y^w)!Sk}}y1+sVMWTDYH!9Zpn7UR{`d@oQIGCo?8CipRaw4%)Q z55i^Ct`+Kp+=47)Yk(Cqd#f0pb>=-j^RXx7K58-+Gu`ztj67X3t1c!O7|fO$lFW5o%u zF(t709TwE9{#K~!byAI*+?UlZ1Zuhs&oA(|KoXKt&$(uA`5$f7JO-=d_j4KUm&H87 z*R>|DK)!FlbU6~SsOv^lMi~;+n5L!y~6+mXxzwNJi=aDVbl8N#P^13b~IUR zBK6%_C>WVmdl&)$ApTgk$@vO3{oO4GuDaPxwtZuEp01EYYP;2@|jxJ^VE`a7XQ&63v$7$R$_!;90HL|WIZ+ipr zRLL7juk_g^#!1-Tre969oqt-cC_oxl{DE{HoRy%LY&C|Ccne>VP0k}@+bM&{Y_I}o zVJAc|*uq}Oqyt!rWqurU6iuR(*a=@ZeprnSsygV^Frk9g*Ej4-X7%LTSBU+PLmeNv zk8k#Sl*i4ujGB#^b?PO+y^jXWe-geh5Y|k@eBozYiex&)6u9c~PNELW%(oA7-iLn9 z5?4sIVTTpnuMqSANa+{wmWwypbtLnKY$- z34zj$3lk#h(Aey2Gq33{_d2R5+$y zfWPJGFt>}Lse9BgKqi@PU0D1RYC;&Ck$%)c&x;Z)PQe6qd;}{cHp3!#xTf-udhUrxN(v>gKnO6 z3JbPo$63%!2|k@^xc)u+AVfQ-V4pB%w*1;h4L+P ztAkF77X~$sgqLjP3Nmo=gD9C)toWaLAoIN&=|{#Nx9yn z&=$XmY%tKTK-uv%dSdE)hIK=u{|2g>Hv@4*If-%kkAQcx7V$whls)oqD-eA3%r*c= z5Dy3O0@W&4+SK<^Tw1VOntd8(_L9ehdeiBIn$)78w7M1RB*tHOYE10 zSQD>BxLM10J_8WB8C&9&GR}0Phus^#5L=RUzu7#={CTxiRU7iLzkx9I!Ew%sVcoYk z9{kvOuqXV(=C+y_7HT8DWSiH0pU+c6(#XiR*6%A=|BMb~mOedk2Q&a`|LO4U$5I`# zCFeDC*2Ev``jiopd*nvbN|!&{jBeV@``vNV=x+~=E>k}WA?Bbm1F?K)?_rxJ##Trh zn*FVTK+oQQxuD%%6$=tgM^A7TNxw8GiGbLvsuLzcjK8B(e5m+nEee%eN+AvL8RwXv z9iCw;c2ABx{~BJb;!xU!WRald+?c%(CB@eSiN-D-l5&!2))#;EUoWjnRMt6R-zDrbhOQ~*m%4@LvM<+WB`|>{+zYlt|sSrQm7 zqn%kui}%DhiMB<`f}&N_INBx6GZ3>4SsGP4Gu*(r+Y8OdRvz>YY9l(J`DzQ+ zA0$)$rLy-7b{pPUcvf=R?puVD9wo0$9S=b$nGkUW!I_LR79@e{-s3c zi`o6Eu2=3`jlGQb4~%Wm(E!XNX&3nP4gwpfaos${2n~Z4F(g4~zMQmz(WI7>Tj=R? zEw{seW(eEFH*P-+H(_KAm?HO?NxyebJ2JfT2u{L>mFCggX z=Ldjx0*sL#abW6flW^#WN`&4~9|2xQ)lzNlq`+kaIN7DMbdVB}Gy2$E((HF&>;szC zJYw1R7m)D!^3}UDwmo~+P&S_D8RAJg9_&-CJz%&!!E>D-_GWzEW?d@f&KLNO;PZ!& zBA>I?qmpL3Z9Fs-DTP`j%P>1J&9&S!Cz)uAdLJo}h(=6d<+Y_s=Z3YSx6faMN1n-= zinV#8JdF94ADEk*JdGRekwO})lQ3^TS|fD~QUHSNo*w|$R^*E;sVG#CQ|)w70$$W% zhK}!hcGBrAJi<}X(Ga^Gd7@t<4!5kZE-nm}4Ym6_Cqs#LcPU8(^FP5SUd+g%BQ>1E(C;n`X=a zDM=-Shypuf^K>^lWKu7O7bh_af;_zs@~?gJ>#H8hYbPA7<^yMka!~BVNA5hiv1elA zoy=E`(4Ded-?g3`SsWI6S@&dyht7A>LrcVeXR6w@w*@O%?*YvOFh$8gPDir-S$uWrX?mQ6x>A*vB^8z>x?t~Tt}>ljD)BOMUsZgPy5OU)o=3Z zeFXUrE|WU`171KDYs0G{P|i4ZHhqxd2ca#RUw6D*&+2-uzv!{fLhs%G!3}qyYe21U zBZ9}Vr85diCf=q7Vq0^Spi!5Tu5LsFiWm=u8l#y`a9T2ECZQ3{kc+%~L5v-k8GaS9 zQcgBzh#JL4_aC90!vnVGr`vG{x~m=S_q*Dys~-S#-q)KJp>f(5khB9zfhc$=f|4n* z!OXB>6{QLwu>wc5>NuM5h)F=&nqpAS(~B|o9%@N#eD zlu*J4EZqW6`JAgO8h#SAp@H8)b}El?H;j7Qtv+7$v}zx1WbYNgHY z&gpSqLlU_%$sj!2_4n3+9q;{|fShsYzc>+eMZ(LO$Ww5yvOwi~+2aJuQ{wmc^#;FS-?4`*i&ooxELy}rzML-WnT{jXNtFy8j#qX}K}^jCl;*VaUucAh6y>p}5h zU`H?D?P z;KuJcZBkq!HE%@T8w4c59iz9!JL|Tu?jiS^|OTpbbg-2WJV8_-$i*c*FLp@fWBcoOuda z(qAH_q!xUG8pI(a3QWvmRCWO9t*?V!h;~RvJ;>A|Xt<%Tmi>qlZXxEWwo_GeeUnwC zdlJec@FVjZypd3&3s7`|7Gce#8c=l$#ijCDWkRBQR~t+Ys6_@u9Xe>J%&@I#shOOW zk$Co!jAX*dq1=|U*^{w}i*{wZmwuJyIXJE!t1F8?5@B6d=OQS2FeVvvTmjtsvMJEK zcN@AZxnca-u!;sZ&X$l%5`Mm&rxrI|2qYE<1|~ z2Z1(Qja;mIi7ij}YuK)j=v4s6QV*z$zL72W7To3+IyE95;ykz30F_H~44haPd z&LVkEi%-l9{4rVXFl^{z=+LCHgUsRc)}JyQ`7RGX$9ui+?@1e&Dfd3o7bfKMds!VS zLw7$QbCL@d5`jZI=ie){iuH%ECQd|H$pBeoJ&bSc$Shb`3*_hXf1KgQ{G+pQkTD<< zZ0P6vmoVbq8eTY41ZK3qxjn24^wV;5@6E<#XLp{8u>5jL2Z}hF^Q>{Q*?RFY7$9QZJj%$b3`#6@7!YmGdT+>AFMI%I7In#cKeJ*`y?8W{l<2$(n|u#Fjk zl4f8O9wRgwU38$1huEc*ehyjNSXYg7owc`Nmh$HLPj zbVFRyZ8<3;l5el}Vo;iaFrMPxiD2&%s$_+SpLBkKRtKm+26b38h8jNaEq5jx&udmM zDY(|+yse_+OqoxK&*Shw^S5~zt9JqRnp6p%zq=|zdk2!{6nKCyLT#4|Etq83zD?`U zYRzq%f--y{`?MMg9-RV~Fkst>W6Iz*xh3A2ETKxXOb5JSPRjI@J|q6M`j{^mKt0O~ z>htg^m(X%Y$~El~6gc!^>BhoGK7}BW_*AR?)=Wild~14zXMOYc_}BL2#_ilWm!jn= z7ZnFlLPY07Z;XAB`1MF%5Pa4y>X6vwKi7g|o&4Qx~ zd_vP9UCfegGE!S~K{}OEXWs-}0agndgW`}si)S>R_r_T}k#(PU2IN@!KA`6G&*}cw zpR0T4A@w4@&r1i|PJ+Ei=uXK?JnTcxDsa&jGX2$i$DnFT%P^(MwE&=`kSbqlC0M}- zY803aoUN=w4h*9V^i$hy#_gbfuzujOx;LsO$k%WC>%qd>9%A1G$3Zj{TechYoi{Wa zwWWXoMY;|g3P6?gi$oPwjn2rkXma5VffcHdC25W^z#f!>(l-NI9U?GHOFxI!04f?8 z7GA3v8my^@AVUguj;FHY#+D6H1DjcPk@(%6!kR!pw1QAaVJrO_WDUwC@OyY4A0t6+ zA#0GnPoVA>3qg(D$X&FvK0eA(vktD3dACTc8Sb*#E6neH$~4br%fOOpcwo%7-DgAT zIt{%QE`HDv+AtK@@8T}Sjnh@A(N3bPKN(#Eq6`(v2W{i;-1}oQRip0 zgjoa30F2E5TO$Zuql6xT2?Ml=iiUv+1p-OuBq+@b(Spko(8hv{`}hXjV4o)u1y+50 zD^e!YwB#5zaLqt#A6$u;7q!$Gya{^acVHwa7E}V8=z}@`-uTOuYRkrXkXZL$d;)Lg zVW1dZ1eVjU^S8zKy9sW`y2z;m>Fl)BNzu{nDSCAx%}0RoK1K`Zn=)8#~olJ*5@M6je8C7#Ir)wEp`ON^L1-$51D-m_&+|5 zI4(YRW0-WHroh2w+lO5s<)$j32Vp6s^Eh!l9^K*IO0s)^N)*#*UtwS)oa~Dh*Zs+X z^G`56pKEKG4zSC#1Ko55)_WrsJ@R-2{oasq3TpDAQMXHiBB0JOxbe~61zG_5G=<9W zbJoo7z`6 zG6a8TLHb~wK6)QoQpSuz8-R3?omC&vf%!vS=TGp#D~W{~54uGoJR}OSfOQbPYir>d zPTF}*q@0#dKJ;Q>CX3SS*8d{$LUe-eP3pbyT!OL>qU`l#S zsd6DrXfn*m)~}dR?jNp6$>wY*O6A!0NqZxqU)gA;9%euVTG^Jc8n8_iQZ}$OJGua4 zL-5ItL7Y5C34wsJ_|<$wJ4;Twg(ecWXvlC$@}!3eU7oe~^W4tzGL0utv!h%ykd4-a zF7KZ^`zfPbV}3!V-1v{qp)byPev~7XoRfl{>PJ%qrSNqXw#pk^|5VTnGg#f=PV50( z!3_g*x1dI2q4ZwJJaC!=9|Z(x?peGLdPPll%a!2@!aDaGhk zYJm0IqUdw6e$gjxyxE(42x~D1#xi;Ox6WlDTCB6Jbx9P|ZNOd@(a(U5n!W_V9i z&v-m9Qxhf&$`A7IV*+&Aw=}qA`9-(0883Vt3|2n4{OTLM*CdeK(AB`ZEA*tcj&#m2 zpiS~DteJfUbUO)19+;|9EH(Sk`iH_){tx`9frJCj#N9M6n13mFa>kLO$a_c(iQ zf7?&3;7*28J@WQPMXk|(0?*(XNqUpQBPo*sB%q@$Bb}uu*A29F=nZOgI!R&d6(9?} zCp=;9g4r1!8xTtnu=60(G4YDh^S2r9&2E(D@acNnpo4Y%NK#;D#l%c$qDMsCaws?F zL*>5Y`aIvdhMoeT zJ=7h)N$0_}QYcpCVDqNC;Bv{DmUZ#gwYp_Gs% zG=PA*z{*kK(hrrDuwD{t<#@=_0VX+iSvcm=HN#oVh{r%!$R`*_(c6tKtFX@v9JZgfyn>R5j=@hK|(!?OR|Rcr63dqWnzO28U!5* z>f6K%D4vz?LIE;7!n8oX*AUqL&z>a)PN{?Zv5}q_t$$43@Sc4&x!V*sgPz32++62wHqL*0*7vKZ3XJo(1e$ao5892m?9hJW}ldMxpwBuS zvI4y=)8I#3^EhUH$)4-WZq-Lywl)u@;khuEgdf+uI^_N=Rk;c*A>bVBc~(xDGo+bMDfZy z<0r=hjUGm3eY{y3xFaaSjoCl@v)KwD&xsRChe)8yZDXTb*w91~vL{&ka2hi5LBgc= z&B5+3>js($%LB#X$05_L9)1tWa&X)P@Ws0&gP$>*zUuHrqn2U&eg%(an>cu5e#!8L zo)KtY%mOTu0!IR)W-#X`z+lL71uj*UYac*il6e-V;<~tN#JDe{3YD2jqO6e zM$v-=8<=Z5nJ!>j;CnfQ40r^IpRVIFJa#9_i)G~G!%3fV4B!?nzf$!JsfxWlQx~W zT|?p_Vd>6FU>y$%nrai7WHIBgSQUFi5rPnF)_(;`6n@En*45uxlN+~RQ_28Bq2CUv zw}W_nPbj@Dsl_Q%)b?0oV9A6?`a9+f;;U;nKNNrn93v0AS+IOJ-h7c`HjPgr+EgQ9 zSDf>nJBAL#u#Ltf22pzHWqvJ!7m>w`q_&g1o+#&G0)_Jf_KNm57%?aRha{~e0qZzZ zu5@ZbB4ZDR`ogU_NRaA3h<$T5IWuHa&orpcS^(^iUjmx)NnUZdU~zxUoAyOP10Hdt z=ENNbtw*2+$G)`GX-9{UYXiFk1F`M+W;-(gWD!B-Z~?Eeu)%K1)V=_3GLmR=DDT@e zW?-r~t`63K{~`JSYmR?h)Pwzw`fAFdYM33wSR=3Vay4 z#(|GQsJ$Qy`J{kSr)@3e1D63zl_&LPyGWMbq!lvv_cP680Fuujt`o`LDIJx}HHmUt z_tySz#tT;Ugz>vV2d0%rj&=rkb;SmgpeFHBy}$2LphjJi^r^~{mklo3;#qBBRPNg` zc~`}GrXBBMfFXTHQ&?5~HMk{~wwb(rM7}mO*TTp6&S}~l7`7%K1AJ`>uVmt}I$Yzc zeF7Hqa7m!rStRwtNlUH?OsLkl=F+$>O_&02!uS*nhcwuNJQ#B#rQOvpGMvN+xh#|9Swiyn%R|FciuZ@`M`G zSyhhqE^BeygJI?|B(g*VelF^JV1S0f^8GYAlk#2Pj}} zvKe4PN_X{MRv)y?g>t2i+>BcY)`gjCq$JyQpxHK_p=k-tbzzyYc*rc@$fdxYV6bPg z|7h*NK+ew{k0Q3e&feK0I(um=X{h|)egQy)9VMDoLiJ*-6Rg{?(T(|^MC8H3JzO!3E|< zIL$YG)d}3I{lCz}!pk$16rmo`nz?#_zeAzjekN?^?MIRk6vVb&4K?bho28#~`9BXy zY=ia>#Aw}N4%j{2ML@TDXYvDNoMrYCCWu1RA*UOM&n={3&{lZDS}$dKU%!h%qGv2* zO5^2}QVYY~E+kCz;a>HoS+a zb!Y!`K}(!0smy0ul+wZB*?mG`dPjjqYvbZe)>uSWWZ z_wD+2Y{^M5OWgF|KcrXS)p~)T25PyavdUC4>>dXx>21zRFt1 zT-!})E->z!SBPs7cU*TKqSU%s*e_Qv7^DzAfeoZRPS4}-|skVEl!-+nxs9^|V za}t+vTh*h@D?)@5w-E;^JK19m68(~6;t_zXlYjgbD3nH}-D8pvK!lu`w(oyal~BJF_csW z?BNS?VA26C2Y9`8q}{A7L2ym1weT#PwT0hhHUF%z8tq{K6o;w4Ti* zt~jA#A#E*FCTPO<=#?iCb^Ca7Q27B(R&wf`{Z;0%LJ1@8`=^zBcxRzJ5o(U!SXxeV zwPe}$IA_BxH?p`m%YSqTB*X%zvFO2W5M;C1y0%6<_9OIYkrp&P(k%QlLJR&KSXs_d zZ(_X`S{K6_jycg1^=`S%5aSHS97{jf9j{VMWsH1_=3@BdW5y!gqBvP8WYs9s9>+u- z-BB}<+vgJPW)F22yq!}_j|qQ=vVcN0B~!2p3}b&EHTOU)tAyY{k~j@pmi2(uO<7K&*^rl^ zx8=B(M`7>G7~s!k)$PoI^nBzLBaN2;apKTrSz(RyE~GI_2_CMovV@GtSmm~In?seo zXQ~A~1(UC496HA4{(G{qpMjt&=t-uOR^YWQ1)qe0(i$aIxYTk@EMg|*w6Tpe(J#=1 zS-SW zPw)ggdi<>i_T>fhdjl2TJ=mbzM?nuYjicIKvcNt*Rxb>mz|X3(DClV!2nifYT8lTX)W^u#XsDQd~9%6gMaraJNM z=9piBGkDEl2maZ}YG7^UA~9fwpcEQK;C_O&ZzEwS zM?7^DCbi;pu}(K!vkln*&DjqJTm^ly#K6N3Zj+_4iH`Xp!ye|caT}5))oxR_#xhZs zcw}$?kEtJDtG;>-Oy@_uc65LOx|jzRAQa6i7Z9Sp2e0v#21MZ`j^?7aP_U5o*3Mab zPhgI9enA6T3vBx_(|4rw4REyp4->9x3?8Flt=Ye5ZzQe@3X&KKil~)QTM5%|KCiFl z-wG$3;c0%EpRVfu4K<#|)-b*{5%qFZ*1(*K!9ON9+yN{+A);renP_A=-0YU}=g)o> z3{Ft)$Gtr2_2x>k-f`iC&-wM+tC>y^Rn)jEUOu!6pX)g=I$*6sf#_SwDjZ)p8a#lK( z!LcH`Geq->EGeuUN5HpD!&U)fdP$0%ewW8K#hnu=`RwZ|=69UnzL*4?(iwo#)En9# zex4js)i*ta`y3@Sgrp-K=;oXj2!*yl8Z#CC25FUM$Gvc&C2=M56apla6hdS4ba`}g$i91Mdli&qbns{^uR5@udp&17y*Zowm0fymEt$_W# z1?dC+mbJgX8wtmDz_%lweI{<`c2XWA_MNpWEjkrpZB+VjEPylC0rXzMg#h!S}$0$7otG>1@M zX=Jp@4K0G>_VjCL5A*5rw3c!UU_R;r@5?IsBQ`%E7;)m-jDSV_T29G(17BO>+pj?2 z!2Vg+bg6s!0jyu2R0{TEd@=oF>TrlP#M+|GVUe)c(6Sj))*K`v=`(U#CG93W=DEELgG0bd`8)(q!lX^z348Z7|hW&=Q` zbB)rZzMrSR)9jH==|q&cv3)W3f0bt#2Hl-X}#W0ZVEGWM}tT}}gUy&x_+v4p3vMfvR{rZUE zez@Op=44FpK8InT4Rbgm&?BRSGC~z&Hx^*OAI<49S&#rh1!)?*flOj7Y@W~knMc0w zr!>_9n{1_Qh-2n-mpOvxAxl&|`0J5pohAKy?GL7%#bR2D@3J)`8?UfVHfd3}5sGX#MPf zr-1AP$ZkKOM33F~X>KM1V1#uZvAE`v<)r?>O_(-ASy@lboy<|aTa9&g&6r<1{GQJb;psz+o(&&;)_`h}1r5q9S%@SM_e)JF*Vk z37--MU1YZs7N$DecpZN(?4>({Ef`->yl1XsYY|j5YVl3$hOB_U_kpV|hE8|PIZMRj z*xx8fza3jHld87>k_us{R3umpbCjj(pm4j*oi!XoS}mzMtrf~{=!(^DpY`r5>z|d| zr_(3I;|F_B*YbIm7hS<(ck78mWa@<_%|Z9GUFkER2u%6`oau|uPXKQe+(%rS@JpuI z>7?bvtxTt?%;sEf6X#lO{Cc|${X{!I;!V)&HZ{RBKmaij+T9!DPB5iejI8Q+)Ge{& z7E0ca?tmyK+QKSEQ5t?d=Hibw;yT8i4+bHk2Dqpk@!TDDMDIAXk@UIt8cLd*> z)9-d_;eg*WK0?0Ev?&|UIi4H)q`W4JKigsWaM>E(e#HVXpg1Fx<`ZoZ+%>RUC3#o4 z7Z#+Hp|=A$swb%aC8L}7sBz-f$1cS;Az(q})>$(Y&0eGZOp zuP5jJlGpNbj{>V=@!uqw<%FerIo2_dKrJF^A`JG(^h9*4 zpwZmi^}yCqFGFJ^Rayq6iVp2p_>O-&l|7;~v0!{yb}#YFe+^^L(Zat#;o|BGg+A+Z z&(3*UZw`w~IC;$9oOSNpj|(4txTW*48bArK-N3YALQ}koC!??UoXbn)`^m~FhdVvI zW$ln3ziVcAQE(S`V^2+@Wnj0<%E%c*hvVHf1~g!ceo(+m`qKJEzr5niweWn)89KIf z2e@kJZOlL)9A7mLh9*I*`#MUg4ol*DJr+F}jT4vE#idQ_f9#zZzKFT6bh7L4?uz+r z-|G^2SQh%M=luQb$uEr4Ssy4f4QLklA)mpSU^34D)+XX0mrQI>+b|EKnezJI=}dpc zR63~N8wob(KyyyE>Y_NYVMGX2BpLW41F@OEZ{4bdww~em3!i5@xChaGicMX5Gkz+G zQ|S%#HC#~kVT3*LNxh!+Cr5`FTo(R?5f^}v8_Ac2%P@14eGJwNWXE3@TNG6=ESqwT zN)ij-Jff;ByQw(=hX+0X%F2JU>s)P0=c3U%hrcoKDT~3$ZN*oHX-umW1Tgptn@S%6 z$|{e6`(xi!+I@tS2?&BWdgjOQM$YAjZQO+hICBarqQHM$>GkkGb75yu$!Oit+1cab z^WTI-=+i6;PoLBD18_)VR|B5JTwkox?1%pOje<6j^}fQ=(OPS62K_wIB4THVT~E~1 z8lGUlX&UIHEm3iy$cIBdjv7c6cb9 z)x*z$Z%ZD6fo6SYEMY}N9qVpFLNQ7|*vneMBzMNBwX8X@zGF;+gEIPPPYlhy{QDq= zQ*7|}Z3k0ECMTEv^pDPV5CbkH1fXlw0UE=u8k;QpmlRu4*c2ORGFRixPEfH;uB`m`@9wCmNac)IW$wkN1&pV}s*=RJWT+<0iQ9IZPr7u|lI^ zGby1_xD;grZBnp3zU@9^kxVG+&<(e!%elrRKR$`lo8~U&-~SxwmBNp&;619yFWF-i z8h5da-@U&>laKN++s=;4)kCvskvd-rt?Yu z&#Qa*z9rPUS$4HDnp`<>UY2&Wy0Sk;ZFszUWb9iP19w_=?#h>>XVwmXe*ixeV9u{N zm6jTFoI=?#(IKViGf$xFkT`!BU@GZETLvW8a#OC7R@@fNT`a9Yh5KPByHGl1jNa;U znKD4>2VNj9{dY=Vm2^A`-zl!RU!*N}_VYW9rGD1oN((XEEm)FX!3XHV;&(`5?UnLV z4Dud?pbU~V&|8eDaXpMUZ^ZIiwQz9!C@cU(UjLWkC{8)!wjZ$Yn=#b$t$&fKK2Zr) zex0UKr`4S^-#J21L%arH%Sk=pa_s#_=QR7Br7FDpi0TV^<^k3Pv^odmzKI5n!YaGR zQrr^FL3(BYaaq5ItK2a9L|zkQBBw8?&4S-1bQq$!vjND~&OIgLzo6#~H*+Yx&cFYm zhrSA!`w!HZ7h(&%O2G%eg&e$J9v=9XeMT-BGEhLkEZiaS2?)oDoD4n*cbR3#_%S$6 zG~oL49;2E3Gag2o9jJ#tonq&f2Ra z^e-rcQYis0Fe(Fd{z?w_2qQa^a|<%zWYqz5htw*DZjOHE;TS6W779y2X<*va6@gzG z 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≠=JT; z3F2nrDt?F!A!HfF@8D=;!O0OIzzCbkdUQ8(@=t;f=Fiev13T<24V=9X3fk6tm*GRL ztAA~|x!3snmu+1grvKC6-o;5t_qMFtwD|G8Esr!b=4-fCDJnFd&q010;=~~rAlqV% z`ND1xq0pQ=rTm2v^b`v|Ggxvuz@Wi#Z%(|72#!PK9MOPB)@VPmeqc!_w_U&O{_x|Q z&4m&Q;$-&VfVYi-QY;O6A1gRWTC8;N8E{ zS9$aPeJ(j9DC9%xHJ|F3v4c0PI8;^!HV6?5_IUjgWaw`@U6ngo1aBidMgy@Sqi2Iv zIX2@EO3rmiS!zE&E97A|W4BTdssD+8dh#IfvEz8&656{#$)yzNB+4r*bJobIofMcO z09tPMqn%XKyuYqgrMj-c;+g|(c%4)4APFpsaF1;1s3-&imF=i@=(NeOwd+8Jmp2r% z|Eux22B(hy3sBTK?gjM6f=S=*!~1hO`zOy=Jo|V(bM}Mz z&CwaH7v%4r>3XbI=Bh4+hsAiIgg?*q!CWtf)1Y`MCfqHzXFm&m*bHY7Dn=D-z&5-;rgF_lAvT%OH#{v8zN%tI{Q*ehY37W{n3X^7C##BMZlwDh#w?=c(65P#<9RC7T8GN9osWk2hK!vL#2~~~bJDudA%S7@sNN`H zIy%H!Ca~x-RJV&v=z=a?YnPpV7unTA?RSt8iAOLPfCehVJ2 z2gs=NkFuC8%6&ankwo1vZ}QfVGV+G>JbFY4b?XuTx3mq$>DM2H&ff42uy{4reEr<| za(#E_TkbC{-sgnO$uNAaN%&9PDwwR(Fc%trn5uMyq26`6);A45U_@{R8~E=Dihp3S zf|!kL^stroi&?Gl4Af{hR;$|>YUr)sx%wWQgCl*<`VfYvRvCnwX7At(TE&^#@`)v5&!GVTR@S> zmH5$n>O1_gBEN4Mqh}f6VD!U(pQy-3Sz`|9ZN)l}Mj(i24eUkEx=LL-2*P8eW~P?F zmZGC8ZTTIYbniMs>QwEJwrTB8=&2a3`6-vnMPpd;YvTecdmeN_^L({l*iBJ2P(6r! z;7_Mlpk7Q~hM*Ss;-GIDZFU-aGfb=JuSAeCj!Mr0-niJG2bgd0A5!o|^C?mnqV#N_ zBM(Uu{J>5skR8UV_nRAj!KUlB^#?MJrwcsh??o#z)qiL18cynaIY&ktqvrLic3~MZZO#fhjBgtdBUfjJLx&w%FHCz?k6jW2F+oRVNIcIp zk4!Og-&k1U`7p%Zs+baWC84S?rs#M#hhwcl{96lOSO7r;%81zC;NeLakW6-q4}8-g zN(Fq;2p!@Y&ZlIAB)$=IQ$PJum+xnT z!#uQ&)l4SX)4L7;W9lMBbGV=>b3 zw!Q8uF^i9_C#W7n7@2<+PRJ5O3B$foNBlMNi9bbXXhW(wjqKh&aNK3_gZ`|WOShCx zHbNO)#%?6er8u4|kXjvgIwMITY1!rVw5LQ8SC0f*rZed)x{D;~>W{(>lFQBNXuQp`;5tXaV;Z6H2!hn_JA)1em<+cyhUj4z zo5JZ=Tvp@{L_BrkC@rf<~7sX6+^tu+vwED1SEBO6Tjjh ziR{f3!izt$u5lf=)F#8NGF)=U#Y zeWC+sUu-sQgd=fKA*)$%_m|ZV@E@S*6x>J_Wgdm(qX`P)?b3xj8W8MS8`(Ny%8dPt zK`0o4-e81F`n4Nok4=<^&kglk@0U{)5=s4S#%LwUJ7`uNp$xh>5{*}&RE)P!!A!hC0RP+{MI=2@!e&FFw#ooavIn4pKT$OnJFbMrJ+J4co9 zCdO?b{xgeE*kGTHs85<{aDMv^sEjiu4jsmR7vq75B%iG6VjffffYdLEgybbv3@yJD z=oz7I{x;gfna1>2U}aUVbD*m|(t8rS`AwrRy_~!c;K|5`4RC3%Nu?{R7Xp{hOyle~ zjpH1V5w7zAR+z}_X0K#W<)6Uu5y=~YV-jCC;!#3x_d_|&v^3(9sWzIQ(%F8-@$Ufx zEL3jU5U3yL)BX3lFMi!6wocLgw4>+JM}2w4p8N1}^aS!QN{VoeG)tNu- z9G~tpv25Gb`T9^*RkzKVzcdw#GydqGN6TRM&s&D**0Zr4t@t5|@)GinAR*#hLj7WB zal=AiMEbrc>`)ukawCN+_CaaJ*e};jf-w>?m1*1+aaVOr8Xmf_>bv@N;HI~{i&E1| zBEeVVuESNGJhC)qvGPdA$LM2%if1+vHy?#^u2jcYp->Vm+k*?(Q_;loDqN3%=<~5w zp=wIwvo5xcg&!s>-H_gt@7bZx%-AUT_2hU4Sc)LZ<4EsI#zpXlV#|^N7Y>}GM3qSg z{K32gAfB-XTbpgtt^~IbQ^|TNQ;PdsIQi z^zw!)2lj4r$;miGQzBK4U>J?1uMmw08JHM5<_t!kWHsM?O@ z4hCG5&1c2qmf$Jk$4{9ur8Li`zNwXY+EaZR?T7P5jL$muOiMehowK4jPi=n;*tOwC z%~gQ4XYl9G+ccMdAS1h3^Dv@@pdAZ4w7zBrL49b+#D#FO#FJM~apL%^a00eKC=p&S z;Xzx(Z{{kvB{aIrjZ&Ry(&4Gs)VXtthh2Z@bUp@5`_@S3vVh@CK}um?%gg-!5O4d( zbMQmccngA33skuT1w6!36PXXrVJCA{_du$}KBPo1ASz?;LE^hAMJMK=Jc_?%b9Lif%|i*Ac1g}L z60(-c0-Z^!a8e(U+23@wk_zS4d9tji-<_?_)Qu=`A?Z3-XCK|_{9*t7))qBaHI?Dt zyH+L$oMecknDF^uP_2>G?3r;m-b)PR#&vF(6FAshlm@H+MVp6=c;@@eF8E&2Ba*ke zrg%5IGOCp=@>g6|KUV2TY_ynfiOZai$upXmOR-WZ-+~8r4mX^g(1M16t)09mj{lGsP`lkAsWR|)e=xlJa8Jp3s-4b(kJ^P>BVJyZ zDA_tOF__T*!WBAYaqp3Tz7WrlmG;DC-!w*gTDV2ogCicPI}itzR;C03i#7$&s|D#rhR$GTH~I?WLx!>!5FT{J^WNm zLBHqxrsHM3jT_p(r2OAbkN-S8ax{qfY|M^=9|~8l1p;xj0?ur~Ul^b4|Mmj|)_I;- zoyc1SpM&ZfOpx}>nJ2fe8uT=g?z{cOZZgGgx(d#eCfCjKjAT2GDZTwza;LyH#jDH4 z*+=Ginrh-9zPRyjo?Z40VMbYBZ8tys`}8E?8R= zeYsGCp#6h;&qe+6c+nak?CRT$Z+>$&WFW^iS0hj1 z$=qL+g)e3)e?GZ&8<$6ZYST0n^w++RD?bAAgBa8ryLp4=X^(CGC!M{^7u>Y^cy`IU z-(b8k{$G=9tuH39uJANot-{wqHXDKIK-fClgOlv=_b~L(#VkPfUQ&RRMm6z!w>P`~ z0Pt#u>l3!*<@DXnduPvj2e{p*Pu9%KvmvD>p-D*PN9ne7+-R<{7{tX9QJ1AH-wh zNWL-S6c?+PKN!qzO^o2{hnX36acs$nvo>)IgYe=2I+@flH6BP9FG>xg@Rx-N{odq7 z*<4kND!e93*H;yeL1+*I-wK-+|U(}*I88WrIv zZ9GP4r#i3xlesp`Sa4IqPb2C&?*?%hP#QQ_6?d}hyq(g%%3=0=#-Wq`G5MY3Es<#< znLDmg7yUT-#R${x=1cy~5+#K>UWL4j%DGB=sNI*FSD6P&VByG!Rl(&+g<-cjH_hX0 zt^I&+wa4Z4zXuOIt#RW%(gX?f?Tz?h6>&@eRumx_dnZ;%NUHYs8QYo1;q<0o{%P4v^go(jUkVYAh8 zTT!GKNsiQoiHVzbm|sq*vuIy7EpPW(YROiK|MiczWWMQ}a3ffsTS)yqrk(mR@jL)n z*%j?$pOi;2ix+0g)b|*XGTII17Bsk-zm$Iaq%cA^nW)EbFUAUF>mAKFQVM-+LS-7m z8Mz2LPqB&t(M;xP|lX&*ns1o8?+yL^=2v|5M z9VU~Av!xHf6_n^YP_K0SE{kfw`w`%C8P|~za-X)eHOX}e7Ts*`ILV68jSpNQzmPEq z)^nYPp1Z_*(1{|Mf2eK1MymhOs;6C(k(6iFeFr-yY-r;?S!s%R9rg)Xf6$y9S)tBI`fwo?B3{+q_DL zQV*9qZGV;bo!qI}xV-Z!a?2EnV#48Y$JBKl(z~vAF=_1;tYcJvwlTo*VC(qc+g)(Ngyv1gP@31aSLB{r3;ai3;`5%h0P=E*DI_XFpCTg?bbcgJK94jGXQNv=SPV<` zaLICCf8NnQ!KsE%)p{x{3kM628;SuIY?4%v1`Y@ z6f5vIWYnM5yL-@Ry#6?REw1_ra?a+e@r9HpjMG@ElsG>SGisbwJP(!@5%j|lUDvI2 zO<7E9!Wv_;VX!`!@rq-h8sLo`r%iTb(~$tV>=A>B<> ztqM|lsIK&U)1b7Aq7VRG3gn5<>rIQVvo#aju;nnjD%Kf^> z8CMb$r#<*u80W}a-p`q)6YOAFkq1eKZQwW3g%KNm8{!XrAis!G8SBCAzfxC0T!qy9 zjDn>uB8C!rzsMZMCQ$)0DG4l6x?mUgVIQP~ z=b;XX6$CAl0G}37UjCijFh9JeU?_0eGx=UTaqu0_M2)?jL;g4kr~5;^i(odz?>nQQ zs_#y*l9^T=uJeb)Wx{K?qo2z0EmwEU(|AzUo`PF3E+goI(!qd0>SilDtLP)FdVKy{ zTbHFSzbOxuqP0boNokhn#=+(R%PhZd8v7yo5&4)?7nq4D!@hq=78dmRriUY^$a#TOJS9-^plplx%sm^Y}dS*=0Yk zNxPcdJr~v$~6R%n`H!+T) z3-a>A0@qY^$@rc}PNJ;1DHj6L#I&qP?`zIkzo&bG+fO@GC*<~Jdthu5wqC4Sv7`48 zQg273y$y(sg*>@^T+cYRNyAqx?YwVIvL1W&|kW#_(q1CeEz5F2=A0E z8s@-cKaawZs`%w8F-{)08uxchy|$#6OpTlD>Nsz!HrUlk-5L>NLVDUcMzvb2J_cbX zh8RW^nMe`UiYkR2SVhW_R%6#xP^S_nFqbhrJI)C%*u|K%u$M9dkj0dol|tKWiqef# zU76|c?=)({FSmXGCRoQ2;}+KCQ;$!G0(XCDZ((EWd{irsQx+gjVTZ>wFek`P>V#yvB7}DmI13z zAlYtya>MfVhao|FI-5|eus8N%nf`Odm6dm*7kb*R1gu>i*8sE<8G8xiDCj{#rHD~{ zx*~ydLJ<*Do!tt;W;Z^65V-Tvuwz{YLV47D@yw&)DurMs-68$6n+x}EJx}e2H;Z(~ zZ1AOjLBu}B6pNQ5k~~MwI1xphK-{4bD1hs+UUX85-@P}&E{6kgwd$#Wdo*QF%wkPcG# z>P&W$%CjWY%&cQ;dJM_8XRGZfCF{{uB6vV8`mS+@tR_m>;-`}>tYpVg`U4rcxxLpw z@hf&$uHD2rl^;rz5lrZ#0OgHSkz^&*sW$!Bo*fk*dm`JsX1^8#$*t9syZz6tVCZn` zrz2y@$XAj=AG3nC5Q4TyUs zzWA&1DBY-;Xyi+sTy7 zmfS}pekJdH9P0|4OSP5Cq;V^K-Dwf$Gox*K47>F4L&?v?Om+gXg55e9M+QG0xsR09LP!G6ahybVS?Itfly$v=zkEbY?cmmK ziF{UGljf9_mE!SdqfN%eplI5~Gco7C_zv`)^z*c}QHzm!F@cMW8%WvjMqwU|4niHW zLPe4y3&4>SF~w4#!NhWONH|-Leg;(yi$>xS=9?>%u}k^U5Bz+?i0}N&YUKt^18Yc& zPW1O;hiAx+OREY)sxk|%Id6!%kF15a3X!BB=&{KZf*xZP+6Qqf_6q(kP_NsWA~SVr zRN$MjJmn5ZYR-V@XThQD)3^QPY!)wAQj3^3x*DR2EWKNk-hiIQZvC__Wc)!j@76#k zGJYFh`Uox+^dB@4&KOj@^S;X6#y+4~{@8T1(sT&7LkUpKYp6DKMNXP9NNlip{2jDo z{cKX4=Sy_V_hvN(I-zwe4&z6Dnf&mOVTReJx5ZqwK~ouaj=XRfx6=};*37|wL#np- zX%=fDW9|PF4h^64b&eeDny%gM-NB_;26!Ck8MB^hBYD+TC6Byk~`a92I!c}Oi& z{;A43imJO6pcbE&}G%HXu4H#$`pl1^J}OJKmS5L>ZntfFEJ5d#e07eA zd_59ck$9|>c0F*dJgw&O>yR$@f{{RD^waNle`;lguWLmSl|Hyq&XZgJE@LfGL5n6f zccjjAe?&qTw@q7d0$s|d$WDujxBCZd;I|M=hxgHR-Z!4>c)v4JO4Ar|^;y$N1MUGa z)tZ|6CS5GHzkWZ|%%o$AuS-Wf%Q`At3`iB76)_f`RcW1#o?YkHcu*`&oI;MBjunXp zev&3^>fW+$zGkvU_y6=0|NhBAZ)DVb&^&_lzu`Au9Ap7@9$#Dwl8|Cn-Let>Q_}b` z`Nqi?%r(zDw3>8(7i+lwz#wDpgC2JKpG1faFm0Jd97Fd}NX}ZAMHZa&Blj(cwz|0g z@`tx7pIj`e+3#HWz0iAR5HjX1RTmONkofP)OYidAUm!-R@cU5b8sZL^_IKg+k}aNL zgFYRqP_$L1b$u$^sX0(zSdj{On`dol?fw#*^Pi&0RYTPS*$+Xev8F4s-@>z-ioBcU zzih=9QsIAKz>?kxMCzdtah9Frx3L9{Qf2vFn=eh26UEwf{vMWIX+G%@i(cO>Qc#JQg7l~bI>Ap_=evlP6v`#&Uy zy#G^rXb!%gfL_cP->KY*4dU9GN>AXpS~RyC_hY&ePq-P?A~d7n_!XYWK2E|}G9uP- z))}<&Yd_dkK^{|HzA#PGr-;(>9#b5Kk*Jce`NiWLyG6xQZQBOA&?;&B_$v7)DS_o| zE(cvJIOvmd^yHIQr`6k=&GhB=w`FbTTz2mfMx^{XF{M<}>?ckH4p>2EqCk8TBTE^!HJm2*!R#lk<%brm&=S= zvOKMIG+(&tqSmF-NzdL}zh;*N78s99c=9X~|jA z!LyQ*bVEnRk;uYPM?oGXG?cPDwc4HW>>(U`=V*|GHXDCRbJb+%1E6OTso;Ir!n%g& z@VsTFd;I-;b#|WwdO}2i|M6PGD-6R*Lv|Ry<{5iTaJ*9R$K{-370Vi~8_#~YoLEm- z5XN81T*6p~ML&6cCcFgGl_p+gtSWBL_i$;+LhOsA$l_4PjSsBZE^XZGCc(CVcYiBx zz4!BL+fcLbbKlQn9X17TwLS%_R`3UnnmD~mFlkU;V1!^UvaEJZ6$!ftMgB}v&O$je zn|3Pl5j`rfvWr&}aJ<3Ft8@0>fucn@9=>_pJ0%5r@9RIj-@E)eZh0l6nUkFP=D{Nh zVqDe4F&puFQWPCo7=Gwn)1Mk=Mh7G<5EbAJpBlsfdf6YJEG40~!4yS1>1s~WR5E>=C68U#E zc`QRNKH0V5S=qB&qEOp$%3gFMSP3g^)*lpP=HEu5^T~aGPK;SkP=k_#HSk!XlsT)75{^xRhs*hm5|$R$*EqYcN~U9ZpyI7{zHoY~sSD8)y*XAWd+ZFTL|#NttyYpq7tqrN=%9=^Hi}-9MYV|MjxgtAJ@W0%0}gK^ z+KvRG7T4@fsO$#WI{A4HiM$|5yWZ}6Kwoh3UltBO9d%tS;F-%N^);9~n6KH~C=k1M zVRjI-nA=pQSUY68^Hlp$i?k_Ex5hy8a^b{eThYKt9rGh)bx0>SvD-G_;vT_Nz;5ri zyq!|@MX&A%n`I$>X%Fs?`o$)~nkz>JqFG5ylj6g%S9ZHNoQIhoNuLM)t9R2Ah3nwzo30MbMlcNH_qIy3#`W%<0DAtLyB19(6M9O+ zrOf28j6lOgFfp~!r_bz1Cu+l&xQ7~DpGvhLmzT*mq}~uJu3Rcz(VtqE$N1yn?0 z|2atdnvc|fBve$j7SZ}XNm|E-&&Wy5BA)!Y)(-Pm*mmM6Dw7!l3BmRY1x zZMIIVt*)r9lAo*9Y38=^z(cu7KW~G~h|7g#J9 zU9$i@BPs$I#9wur;DKsSv#$bs&Jv-Z<}Yv?taOmGkltSJ3gArH>+1}%1erhNSXrA# zr}%d=f=KfAMp$1L_Q?9<@SvdU-qz7`bY4^UDk7NBl~x`9HJDfXysDxy$yT_%d9m|%k0fx623J>PIX{Gh zZD5a)VASPv!;X*yo!?0GT!XU3%vNAt#`2My z@kdtw;qeXKk(+@ZQ&d#QCsDrCfI%sYvb+F#CHad`Mi>*ybNXENmSF{GmeeXCZZMVD zI9o;$9l!fzAWT$Q6P~xTc;^!P>Kn6uVI83}Gsg)bU1z(kp76_UxF&O`$i-Rn+a2dG zBLpq*sz9iF;4q3}HV`XF-+`_RcQf2-FdJ#=;>mrekK)gc60TUE-rj0vgoR7#To=-- z9Df)6uBN~rI;VB6c%0b4ceKiz4VBk^@h|+Q;Yh@8_<(EXHw{;ISq8p}3N5Z2Pvim{ zy=a*IXoQXKZttrT9(>B{zHk~r#r3KCJfcl}CoNbcC~iWDaekF047vZqTN`pR@_YkF zuB3KtN_b7GcwYOQUNZjSCb2d4DzV!V*WtlE<9G*x9u=pES0m>*q6aUS8i_L9-|I}? ztA0a+n~?a~0AalCB8Evmo&6$bjzDD~(_K8`SXtvVx@?rWy~_^iH)%5T`xEL}>KU)@ zt<-@=&u2W9xB59>GD*TVS;WG;fRM)2P-pSxqZTj33+q-hcFT#49GzjP^mQKtIl|~S z%*3}AeN*^kL^Ph5!XutF@$ata%TiJu`$(Usl)k5`91Q;bWV#-9?(Nx2xqzTS6o2+S zc*?tdpbSwbsg4x{hJ{j`h839!g>tdP?#N<#cS#~yQ+Q)E8i#zOH!8j*P; z%z1L=ok#F(7u=P4>NaIwC4dV#| ztAhV1_O7u}{srTW+kDiIgKf&^!_{iP_YaueW=m1ej5O7yOdx$h(_i~w&%_clLYFgn z@b)ABP_y;&)!tRET*2`kqw>XLF2VRQuotq3$K9rhkD(MT(} z@$nx%4~r{|>a9GGLlh_9iM?rToli-pGjX&b&8k?IPB%{$Zz-Ge3z=x5iF8Au+omyC;gLE&WSeKW>N}96wa>)x~E9@~#Cx z4U1yw5AelKMK1BOP#;IFvE~B*5h}%T_{)f)AFI_Q>-9p~0qszll@$ks+Np>4#H$Nm#&E5X z``ite60}B*a*XAKlqV{0nbo=h*_)ZbO0ZD0LMXux{RVnt3cPD79E^SyJ!~U45mfu2 zsWJaD;PlD89^W*UJb?$+zJ-4uK+sWM-8LZfMl5&vkQe`HPFYd(&Vb?(aDw=GKB5T{4r0vFQqiG?qKd{&A(lmr!0HzCOmP9AFMS=OwWqvPPy#*W&4!AJj|lyO zbwT>F)4*A0aB&AB8lIN@2;Nc@s_2)TF`M%~-dL8j-;G3h?K_^XBxOkpV%H_$fB&6i zef~rGB3asqDeC;ktK<(4e$^lz#J@{Sd;qolY!k3v;F6LDmcYS9MFp>$j3e-$(1HZ! zY6??dZdfDD%idRXDi=d;(M(2{4b2v(=ZRWA$87sN|Ji;)2(RE0d1@H7NG<^dM%Dss zptF;rO$NRD#(^!BrOzU6$=-;YfK|fLdH8v+^7+Q9H2JAJgI`y5+;u%{sak49y})@w zEiNu8;PH4lCjK5CC(b^5Uj6*p&F_7Pg?B#WT>cP|yz=zETkgmz?-h5i5+^Ouxr@e` znks|J=L!DNSqPMOl4n&p4heLN#QJL6-x_TX&%d?#^iALOsXYOv2cFK;``PR{c>}oJ zaAEg*DoydXz-Wy56o0`v+~@^Ap<0-ByWzO7W4Memb7`YCMaPBn!QHTPz<;#nV$=_* z|9BkBdbuy-7b4;({3kLg_ns;p4Iq$GuH0 zmXx!%LgyXYyS4LQyFWCqzGbhs>Z%rc1UhXX$TVk>Uo{ z@NT{7bk80iKU+gfdTLTob?u?mZ>O%k`FGHd`}}{w6A%f~Pr`nRQhaY1-o-m%42A@e zMO82OOV`V{5|>P~Q=o?qu=alaFZHvRvh(O3M?pA(gPmy#By>k|mbs{#Rrsc--L@ru zZxp)gcSchp!+gd*SAG3cIQ_k?NeLBQgUxi@W@Xr~Cjve5;hie8Y0ipP@3Qp$r?m80 zH{wh~@4ef3`BKs^m6=n%{aO3-Z<_t%_x|e^d{52Kt}uFyWy7B8MXC~CO!n0%($(n#0-eEtf#-wXZt;VEXn)~{M>65I@^?IyNsL4EGk zZOg~Dfc;xe-;t-ht|vii9-l(p=M+8aJ8p5CB!KzK7oScV(rI}u&8@u0wrJFfPOI7LZRuytc*v(Zs5YEVhJ`Gme-JjNWPs53KePAB9w5wv&cap}r5G3E1Ug|IM z^IxA~WbmqK>#q$g^U^s-Gof+eg-OIzay;m2#+unsw-T&qSncRatputY|q|ZtFAr zCm@gg2=W!}s#)eMG8V*z6}W6Sdjy7#y8*K2S|6m2FIHFK`f)NFqznlfP(PwW+pd1! zN4bXZpqX2iIxRxq4hnDfX}s0^@E5#UIf5@9(o~ZWNgp4xo@|}Owg{5|xM-*@VCYCF zgzM}@Jg0odK_K*@LeJ+gkmi!LMrNB?31lGh$8)AVFMQOYc=c>FQp|EI^A$6dQQLRA zl<+E_?uA9{=|{3)#%@#)rlsJjDbT9(GSSlsQ9#_=4Vg(OHbW$V4`AC4%rwtrFOgj+b62+j4Ge-ktoB=)V#KbkN-Hi zwL|#D060gwR&d3%(eER}%SDX&sP32=ossm5ldM^dY%L5F@2XN&!4D7&2;5>4Kq&9O zZnP?A4M|CF?dgnmK_)>eQn}^-YE>NPD_0x9yYFx zpSV*L>Qm)+p%6F*$IIW1I9_;g#yP#J(n#Us-Fz`PI{4!9%9-O;dTuT}`OD>{50hSL z5P3G(dbY|mNckJHlTgKfq1#)fCT z5*UFqnXF`Iw@^@92B6oI^4!|mhh6W;p3%WqY_=l5R6bXl;7fJj>p^iICdMqt;~=;G zZ*dvzcTf%M#~fbNnE+@RTt#3MH<@}yZYN|_^u>^OcB&j63|G%2?z(m2^@s2fBRov?zaVtcURDCJ85Y{VzD9QeHxY8)|ngHTGyS{Co%+kbn;-5GY(&g-b zDyES9_aAQNdxHylce;ES?~0G@6s)>naqFiuQRGfG=Jdd;8!lGH)}uWswxJetNwLV> zye=DStKbBoce1d)Ff?*Qd{a_s;0A8@Z~k72$#sq4CXH)-SNu!kN?;-cdiZAbzUR3m zW?ur{5`$7N?y2apRc4>rTZJJcnW@>}iOzd92?4u3;(SO+Mx4C1M}F%CH^VjuT@u)A zdtXvgpR@-Mg$1e=y14dj8N1g4U#M2vecuO6ZN{cDw#;JuZobN_4D*$WP5Sc-?lFE4 zxLT_=3G1wM{96oX8I^~z`{Kq-LPDgAqm_eK^;6@VQkUN|M?|g2_%@rb8+~SqASnkR zMG}=L5sEQ$7p@?_xbGx(8hQ5~OCK0KSet<8&QY2&4s|XPuDvz?w!7x>|3TNgheNgh zf5SRaNmGtROr^5RX;)-#r%5Hr)>MiRvrBSHvkRFrW)(#U#je@0nM#tJ3PT|i}oQU`BcF z0Ujy65bq~Ow3*JG@o|%`hrb{nN{o3F3wq8Dzgk{jf#`D;^EWxOKOSpE`L z&hbgL3lqc=n5Zv$z9Yk|C>+qbLyc9wS7g!FUr!VVA7Zugw2R&gCJ{4jqO&$dD*H>N zy!2b1XE8ExY?s#uLk)qKCzXRqpJtr{gZ zb}8&614+XlI?6+x8jPAR(PSTFnR(zS(^E8?c|!JG8g(n+%!aS&Q{RZiR`BLQs*zHO zjDH8Fu|QUan%5`$x!|AfF+SKEF!&OWIHW{Dx+tb=GA;3lAzETaI-b&e7#hq&7_{D~ z%NaI@I@fEL9-{?W|EKfQ(dyDFp4{rY@IPsLCNv*oul35$JFPFZJ$tnvCr9b8kyi!A zN=ITAFZ7sHg)ELcEW9?Y(4ok-yeNHIQ(f6Hjs%%Hxj*M}v!!b0)N~mY|L_oLMjp3l z()Dluneiz9QBXp}`Hr2!xE#OJrFu!#jQ5a{(3bx2;4zJ`2$_nn1u_yrN)d3Eu*g8zo;p@& zzOi$R=_jWi^Bp-Pd?VA26lN!(X3;y|j9>=`2HuUV2=e-~XUEt7r28~J61|?k?@pvL zsb|BmdLxs!M9DgcAWvg zZIRB|l5qKeO=y5@7%zi7Q^!4hpzg_V2UC6aPm7E6$^J*lhP9=s?wwp$GnWGQjD=m{ z$bek?=JEcwjyhAVP$g!8-#VFC{?}|(tupH!NDHNo$>qkliWq3k+AJ8tXeuI5tpTu5 zp91+FFrM9tyAJ6ib=h07@{G7lDQCH7kgom9qG68(eBna(N;l8XAh7E(-I`DH^@6ng zfj@G+S)n!XOIJAurXbK$%I-ZXmvjxgdw(Rg9n!13M2myw;IVgh+@>#!q&uc1skV5Ip&QGOX9|Uo?BR6Kf zgy9EhYv(D}B*15O;ZxAfdBDRLph+@+)GEf3lt^9`9!i%k&l_q>u#RX;u-*Cq-_5To zV6dp9KHQ6cq~mNcx5(}AT{F%Rj$4Yb=}n)L*2{+8kFBAUTskb5*1N=j2{}{A8;n^xR1~AUxqcc(Y@bXjSJ5e*eD_8z$K)d1S!)xaH&YwiVm{a%))! zsQ&-c+x+jXAioS1WG-=r5Eip2gp3}unTWYbNwKw)G@|38`sWdC#{6lK7J2A&y4=R$ ziQ|)1*I~7b3hWozwGBOf=-*+(*!B@$Z_d}BN+#U+2Kv|%2xDZ{+P-8}PpeYK zgMo%a{mEvN?JffAJhW`0J97 zF`XZeb>zgjA0$ma_?H4b1fO$*Ck74bfH_G@N0{#2q4-nhE36Gny6ts>$g682FcoP^ zQlxZt%eP$^a^U3;C)cVbv_KE5Yb0D-3 zq}A{UpEpK?nhs{lxA+K~&n;u`$VbB(wA;eYrWw1QWgAK@9=&^X!y@FT#N8`TtWB1U z+kwt#L|*WZ5Q2_^xncS-U&lwmVvFc&L!Cf%(d|I0hq#B1#;ApARBWPhK*$6 zio)edajt|DFIrI(>Nn({-r&1Ft#^#g+KBeXZ$dY5qx~Lwz0MmPHMdXoj|w}njEzjM zfSwoyTLmu2pNMG0-(0b+Y7RJL`(#!VE^&(y)&R^{4i7zGe6ig0^y*34byx+KJ|~xi zSNDDF46iOggv!RDE{7AuB)=2q%!+cG7k%h#q?=^ij&U-y{O~*--wF0SP#DJ2DdKVj z3LSAVZ3!cd-069Unji;@$=cy zawH8ZnG#F=EhJ&a=#i(~mw}Dk#xI$ng~K{JvS<6h_17(XGFU;MjJ45 zGLr9Op2D@{_KL-L0k}GQpk;j;g(M}^=dp!YfdD@b*j(hJNDC;0_fJo+%?m{`UP#_13e>+szHa&tyJ-)o8u zkZ8qXjB?p_Gb)feC5DG2Q}(roi;^yAvve>dWs=L++Td6K({2E5*^U5Q~4m~D%X?iuL`2!3;DW!>bjkgG( zq;2(=Dig|${lN=UnW6)v1ap217;V*RXK2IS9&X!HEzmnup4}tx>S`@4HF{5B7p2cP zLGFRorxmfZ44y$N9xH3cJtWlrRGDcP$$2*Q4<$5jShEzfE)aLu0Wbd+ATmfFlwU0=Nxh6{%7p9k~4p#w|8dm?MK17Z#Xg^#EdpK9lqZS z0kSL=*ZW5)*I?5<62AnG3Q*|6EN@>jUIYcK2sMX;H^z_m1YqJubGX5URkQGQ1L#`A z9P}3?UPynHs-m#LZg+r?J0%##7N7VN;GovS-1n_57q4u9q4Y$8tE@w_O9nvW z_%#|SQ|_+)ndqNMy+T;9UQ={xD3U8;@bL`nVHx0m@XAlwZx=`~^O(t!t22A5xGAYa z#zm2iN3jF93muAss?4(PdA(%-MzKNHJ<0!EzN3PP@CPX3PV<>P^hUa3?>|ackXcPF zxa~#XfesZ+5nn?@vLA>h%1nLL6tXyc9??xfp(!B*+qH4|QQ|)a1|Your-6P)tu9js zbuW8@(>-u9Ij)?(5E5jggfC{Fx&k`Es6$Q1y|-{|f-Ar1;nO))U<#!f_noywErlCq zcWpMCGsB@lpFy!2Qr}J0a_DdLGjaI&1o`@v_k5|6v6L?KAeOw8FJ|@Cnsg{4dyL*)#bXV zwWzy%1byp$eAmP>^p{l4XEu=RZ&hy9ocDbW5V{b~lo*GR$li?GfKlj`1wDV+WmarX zH=@sPhzlurksXz=zxn(vBtOkl|E~vI=Jy>vszfT{ou>4~%J$ACO@g+-FvzToO`&|T zBRkpLWJ2Ld=C`R01a^i@doec+Hcr{(ZhLop5j%F*W47 zkUH3CFs|Vu4W5J@vA!?(8>k|8g=YYIIiJzUD!ICZNHI08`7zx^ks@kd`<$fwCZ^+%1N%V z5rZs68%gAI?Co$TR24ev$2S&<%W9sd=#}3%?o|^L6}0Yo%Fk|-zKKfT1+w2~Z}Cpk z26T|R8IQ|Iuz3ubWdAdRva3hXkYRQWQ{7qFGj5S}-o|q*E+@}6)6C>kl=t0V|8%f+ z`Ymp&^}Z8Vn=!KDr&MAwY|*$fIAPa|APdM^F2-~&bIC#8<%%hwUFX)Brj^4*jx z_6DrK5Y^f>&JP%6d%7(`HruYRRSo%xX^A{yEZB$M(4H`O5h=B*dWN#EDOTt9y%jNH z-MhlaNiL<&YieS&p=OedbI>52+4WY^O2QC%#Um1z5;qX>2(H{va&tVE%~mz~Felp#NWH{fm((N%3deTu8#*LXKD zbfY4=!3W<_;%apjJJ9#+3#L-KKI4(M&22%U!8dBBMb?fs4~(8P#ao8!@mWUL-lVBU zWCI1gIIVaPFNvJxsMJ$t-FW4uO3=N$-boSPCQmw1&WaRJt}j4lHik??WGjGpb6_@& zA}*k)M2KJomNqAF|54gr$6AjdstRY^1JiKrtg*x*C3o_}Hcn>iahmLPIxQzOf|Bmx zL;Id`KmjPJz;AmTi-Y~^eUZ!%MM z|6hjcE3(K(^U%LfguD=`+rtw-1U8*Q9d^bcd`f-@=OFH-9aCz-1AO15C^^<7d^}PL z{OOf%I$od4!8Q26!oBAitRAEr=293^b*G@{bCq`TqgTuC7dTvu%Jm!}UqLWkCxvKC-H!jc%gTZ~^Y>m~ug_zp7 zJj7$rpc;MG$Vsq>rn|M9hgxptEoC~uWiydFBa6@|?~cidcuqxSlEtjC2VfUSnm8gM z)fa@SG*n!$O7{}tv3n{pLuzx08Y5fH&@*O3S9l-ONzz6d?eKhCpHZ8T8?1Wngv$F$ z_HxdAJwLw^d{d-i4GGpT0io?NlZsL6XjgQ2TC6opFO-O>HR`BYfKBs#N$BYpnBJ}m zkjjnwCh5vn2mOu(`+L7I4VSr3*RJn48)3S=VP%j)f%gwQ~#_4e&r?Q)HVc+B~ z(nLmG|7!{V?-#cS{w7$iJ%jXyBxxNw-xcs5|ywe5AlX_N`+z;Z+Q{&q;GE#-8oVqNb) zN@=50U_5}gffsq39D%UGd-kc3vWDo%7r#`I9Y;&9kn6JzQTO}zhAXR_63RoWf$v9> z1YE6Ik-w<8vH-tqT6;x8R~^P2bYH)Jp6)6rG&3LL#dH4exdx9x5r5%)-%V+t$;~+P zcvB`Byw@KuQ~JTp3y~uM5Hn1WV9w=i%ML{RhiG~D#ARPSR3+)t3p@#3a0CsABMiXI z>tn7|so4d4R4oyy;h9aktb%)ZsqtoMyTUmS&-5(Y24B{+Ww(-WtN>qq9Hd1D`7g*3qvW)c6X zwTGt1m_Tl*(8RZ6x}lI#^G$~+ix_)_d|K+R?$hO*!-Arc4^auB>uaO*PQ8e7Tc#@5 znR@1HX3(d^0|K_W*1}y2Mlb!qPSskt8?kFsXj#c_&q6TuC-qfIoo>KJzGqE~uW^-I zkGjiPi=N|7QIc8XMz6NUS;Q?RB#R`eyj-L8cyQPq>ZU~V^#U`l%f2@`4}%E(&dE@h zM(a0~_X2g7=M^1eC3|44uzgC9zbtvDMRI$JGHlwbv-dtP_@?)7aeye_LjIrPK=t3^ zV6>N5{7DYe&N>?;nAfkdzZ;lotW_PL;cS2!U5`j!&_D)H)Kwi!wISNo!5_NJi>G$P zHW4d3EIBLs9Vm(8nQ~+g29mF&6~nq8vkbk2A=Jl#8BOUqNYX~+!GvPf?3^&A42Ph} zM^IQA2TNA9t4ux{BID~*73z`Agb8Mz0=>8SMC;6_B-<0kBek0_Gj5pJ=2)?1Xr%Yc z%tPG~tB1H5gZ-OoUOvx|I>szML0Y%6De0QQH~Cih>bAU-sW+YKe5XHLqu zBq}N3p~OIEsO~=qdVr2^Drr7Fn@O0tVwuv)#$b6RIBTf->lY? z3Vjd#5npqhjm`)TUm6cQdHMUw0-M!?nNmQ)YXU|O|DP*yOa_JBc! z#=}7`Mwu@*{wYymBYA1HuUata*jr+|dk9&Kdp9Dl+Z1-7fVci1rH6YJ&}C_$<8Z4T z4o(VvSc{uHU@Jj7Uh0NHk#ZEFjB#o8tx8CR&X}?M92f-DM8PkNqU>=yxhbRU6=ex> zGWBZqg03@D$+}#2o99Q_HH<2b{iZ!Pkq0O2cK&y=%_;CWtc| zK$NAll89A>|+^yNyuG4mR?q{noZ!bp7!tU{@qKy-0iob;47MR_yMGByg42W+C zc)j3Dc3q?m*Ea806{j|tC!~GO2#7a={8zCqG+L@xq~mBs>EB^hiP_au=I7y1>QS){gbb@1Ci5A)tdtC;gRwL2cX4wrcR)QqH;i3cLP4VK?Wz4;J zf-zv0cVy%>xY+453~oW)v+gly`+@}fUiOqIsE^bR6Ec09{pCGpO(y1~9WNLw#60md z=esqhFP3PPOG!jwAfOGXZBJF;HIUB0<^p)c@JgN*57Q$Sf$+*fk7I>nK!y#- zWlqG3QAnU7WX)q8^0ma@lxGOm!}K=JHn?-ji)P>6^n9wL-W=)|^&8gIqL$lm?F14}5r3n7cQN9NkRwulqRvEHdZEqhbZ#M~vOCGWLL;FE6piO8icF zv$%rojk&*g#E9CL@^LuP+xvBX@U;5{XFGn(%{_u88-I1T^lQ9`C@e{1&ClPmq{kT0RcB)YJI-Oz?P(X_HhY*ji$oCf~KFV{vYnO1vdc6OTl^Q0=?- zMynp()hl`_yZ`XmsBUg8$MP>{+0okPficx*$*&-^R`JFo5bhdcVneEbXrcXHB)7WoXB;y)se9Rgmt@a8A;Wve!WPH0fM!qpM8|C&wzC2bEtxFUY&rpRntV3$=R;x0;G zq!bya0=PInjv{_oOP)PJJUJpyS`e$bV8&hDTDJc zJv{1okJ*G}v;qHEr}^3NJ~QDPyO2ZN#J7#JExq_$^1F-Jz>rIM`m{Xhly*f0)~Fd=kf=nA zz|-Z3&|qZD0YzlUGXO)!H-O{PxQeWjcC~h0DV~0xsRrY)PmkXX&VQ z6Hh`(;jFOTL+GPDFGBqbN30Lr%?%oV(gH-bsKSBa)aU?Ywvf@>Of1AB0l^CpDBBQ; z`TSukcK#z_Fbw<*QYFdOqleO#d`YVxoc69MMJlqV#*5CLRqxsCbsYf~H+o?mPmW9=c|4(p>gOQe-19 z*{dQ6C~Ao_UlNJMPhX;+Vnt8|T-AY*0v;J8rE3mbtEI+6<8obZiFt%4H<{*(^m2Vp z%R-9wiuP4@gqxCOf0aetJr%pJJK&n|=Mw^YZldE)r_yXZee|iP-RE5yb5y!md7VW8 zL*SIMXo*Nm5sW)aQdb0}IcewKXT(b>vE|?@bLZH}7l}6G=7)PiOF}oK4F(1zo2^aG zUVZYmgR@pjNo?(piVb)I5|+zED0nXVso5*{_T4UX3y}&##J4x%l|S{oUlB5zX{X^< z`#h%gteL6UYFpFIguJ-*@(+%56v5*$QZr4vCQQmp;Ak*COBfSUm6V83?|u(!vXEwFLK)ZL{l^#*ZIhUgo zOkVtjwTfY(ut8&ky*#elIxgd=hyRY6RI*m}6xwVB&JcKBU04(@ z>e2l#RR8>^OjpojVVFDCVCFZNPWNysKjU15ZX8bbSd@IPraQs~N%Gcn zi--yf4G8`8COf*jK^r@pajXMc5zyq16@w0sL%aQIq|V-6>oWwS<>}}rDFsTiAVi%Z zue-#`3k!n6Yt<-cxhYG@QS1sXpG>oxBc1p`aJ4)fn9tZ4_t%hDRaJ_4d7*m4yx9j( zxE~~1$&=^E^WlIQM{vjzGM36h&5sByTelzRDfiji?9suE(sOll?k=ycj^6!Cw?*r& zL7Qqdrvtq5v=HwBy#db$FUqSc?f@42qF=^p11oom*qAl~;|S#_g)=tY01&q9oD)MF z9T4@u!&wG>Q)(hbt!CbG9>jdVdf#~h<~$T>$e>?EEOt~~G-%<^T$q!Dp#{yH3YCTE zbepZ`?kiF}_fsMsKmKplU*8R7!?QQ+S3vA=;Q7;&Xr)fg8+-lmD9q348Mmt2cIy-B zLzvgG)}lmN9wVm?Mi9k@^s?i@1|VOToD(9OIbA|W?u|_3C2#i;Sn6*=M!vkE5BdF1 z>qMC%PNJX=NDL2k`iW(X=k39{QY2NRTWhx4U-B+M>Ruvg!XqAhhD4^kASu+1ie5-S zWzCI)=7OFtp_evWh6XguWOMU_tzz`I@%P8>Dy!@gc+Gx-n=(!Yyd6S|7cCDpnIP7I zq+{*EXI*Cv1k@UgoiIsN71Df~S7*3*+a#ZzY0}HQdUdt$D*gAz>Mk72vF!KwehHq! zN3uFIx&WzjWs#YtT{SeRYFoSkY4#67pOMEmSj;*60#O6KYruZ9)(P>{<4-#yXVXkQ zj#aczCPfWQn7l=t7hPTPJ;rj&PDs`!albt1OMEn>mL)v0F=ut|)Q6sIC*JK*d;Jd<;TV&HL zTE;SD_qVSSYtmEu^a4%f3iL z(Q%aUbaA@EspyMQdA44G=c9@j6&W^3QArmKR*!$W*0b+)TwwpH#K4C~8SnRcI34x> zb|2U@j1j`X>fwIxLw=dwCDWrNlE!@^Aoz**~gRyZ=< zlW{uzVGvD2vGQ^7rAN7l?S%?!R@Mn5sMs|kDmp66#9emhPKOJG{9F8Gbi*f{uQ?Ea zitINjc3~WacC{=Ur6el+K@PJ{wAg}t*5K^7@zJ=|#!WyUKLyM`qUp=3vqfg(A5x;CuIFSJIj=kh7$K5E3;G)CP!Kno zmfHp*%C#F9Z4HPjGYFM8lF_?xR+dNxoNae8R~P&8!47wE1ZM%Y=(DxcjY;Z?z`!0W zZ7Z*Afa)UC=Z-;6$UV8Pw!G^fr9Wu+l@dOAGVL8SWL6qcp*fgRGA58|Vn_n9mS~_n zhLTLS?jf5$-sp#hy-Ao55qOLGR;O z{}<2GcDHzRADM?k8}Q$@yBYB}`6+u0S%4YI=B3CRM5~xu@}0v5_NB9rY-uH;QG4|+ zmKlwC01Zb-Oqy9{ZlJ-sEJ81cDvpXGFZ-MkdCl_Kb1g% zarb6wNR-cRuTr!vKGf3X`J>v(_VViiuJ$5%Gn|)qDY#*qLhol3r05`lAElKO&p=b7 zM0OkJhDhL9Em4?PKS#-mEo3!?btz_=`!RDU^uXv&f&E(iLrq9=NXDM_&%#qx6*&Pp z3|4*U$#eHdyt)T(A%%>nnKS=yi0A3@#Ly5-FtEd2;6a_8t+I_kTNdp^4^Dno zdLEurKXd!dt>V5Q!VG_beybxkR6gq}GO#J2j?2)P6XHHescJtLy&8EmblT6L@^J?PR*=wQAW z%&mw`f})63;>xPDdg=6@nwJ%EQ>Mk^XAOFs-6LW?4xYZ@WN}6$4tCyQh-Q0p{B9xB-six3{4g|buCfqIqK(i-MK^Lg z?zTR_;7yhqM8u}vS1TJ0#SdA6~tejf415zq{q(+!)WX)IEojkPC15ORh! zNV+a6MVbnVkbP)x@?rkb_L`%Tr2e7^-Xg{*zswSD)0P$xY!uUc+(+ZcfaRxn?k%(1 zMaN#Y{$4Mk`5zLc6>tEGfD;@8$$}GES>cvc&(Q#Wd<&E|O;NM!Y%gM9 z!yPuUrfpO<#QuL;wW8D)lmiy${9Z?i;u*?5lq<`)}U(gOt z;h-V@9W{rsarbnb!W+wzSY4}ed}OgXKM;a8gh~xB!Ju?duO*_kbIR^?&rC;N`SX|& zt?b2rSA64JE2r!^J(U5i7}|#wRHJhbUUX9@f0; zwoWC%>6VSM-24$8`UXLpMe?=PD;<_L(-Ha_cg#^;390Ult=$R^Q3;RV20{~!*uwk? z>nRC3HFJBAEuL7z@B?3riY!f;tTb3h+j>)eRG8ZDkyaNfCL%Vt%%_IYRrhVku%tVc zV6YitmNOv>;?r^Ck})mHWhSJ^i(e0!3%5%u`6b%=Du-VPdKh=+K+I}C=zi{3GXA!kee5Jf`GVWo}7ZGON{s>dW=*lFV%hn{(z#RI1XZ|z|zLX zd%*efCgbad=l*I}2`FH7AZ1^!JPEemRE_nD2a}3|bSkaS7Cj6WY1C-s1!wr?_-`K= zJQi#n{jpN*-R<#**lR#%{C$YJ@IP3KuJvt1pArKv=12VI%vIWQ!Vp(&E)7}|x~YI1 zqo7G9G;9B0KZ+=ecCfJ9`@Jx2yr|f{zj)8CF#1o|UnIKn#N3h)p(rr|=&^$vySe&E zgYVj|32s*8@g8$COMQ=H-CX0l=9X$Mhj~?L-kB|<1Nphnfft$PoR{I0=5+dG2r{Dw z;q{0;bTuHkK)t~|u`XL1WAX0cF48v%@XE>R3advW9y^5QDHi1AM5}0(ZS=k2>

0$VG|OJ&!vLl`fCZhI7>rWQCE&3sbmlH_Zko?PJxk1vog2&}j{l5y3&%ASkz zB%!jhhXc)#+Z-MR_dX*8mrNovvaULW(vdIedpi$HM>hNWY<u++Hwh9JaSEW<Y0aS& zLd_Kd&n-q(o3uIQ9_I9ltj+2)PzLSFl~UD@J@%^^_ATO)SBTaz2E9%g`=4H~9k=or zvCa0TU6TVgp6;gBq31CZs09u=5{Q+RhgGGO4x6p<ZqTRAv=xMnN~vLCNSJN1$uw{& z+e`$l#^q@(h8Ai2{{GUKuwlV0IK0Hcp4f@>Q-~!c(62EXCyQyqH@&jIaqdM)zr8*K zY0TCw?XV?FsBtWv-ef2j6D(Rd%V6(N8@zqO8tgZ*T?IZcatrQg%nPcRZ)&tR=3`XR ze)?xnTU%XAQXqC+k`CKw%v(}<Whyi22kuW6sjC>H^~CC~PlAAKsZec~o?6I}((1Io zLNaZt8O5EOVw}g%u6JH`bAJKhPeD&lK!Yw&gdU-4GJgc;6R9dGp0#84Pu9j!!UR29 zn|nT6uXn0#X>j&(gEie#rW>H7=_>c{wp{qEW^2-Q#IHJI>$>EM-XR@@cDj__Ku!c% z$$ymaA5IrwdQJB0ut)bLv;kFF5*{GQ<g5&iv`z=C#x$Rn1dX$R%QgBrN%5xyHFgr( zvLId7t%pH(lC-!lU%rS=wiUGgpd#OfhYYp766uCq0A0GsCi*qN4;kdjIAarY!$~9L zs^vQXB8^>07rmQJ+TS^qR3I-Y?GwK9efxcd^LsCEYM5EyiCeuGl~9H3eWAzfAFx95 z2Up>ThDwrX<p4TH8hBT&JbYK7pPE7sS-eB)y;5UqoGgTQ?u%W+>R{r`yJBISylcq4 zBGtW}+iKIGd%7&JBX~#UxsVI$$Ut2ll1>}@ju|$K7Tf;>PULsd>PBKMs?z{KC}=I{ zwp{~1nb7^8_Vt$A?bmk~ZVfwu1Y)f#(^j9Zcz*p^+(_reaWlU=!iy)7aSA~ShnD|m z`|U&{ZOJ|8mxHWOG+NN?Yg3C_Sl|BJclApwx$;I%hiQ%T#oP@^NN7ZXchJT{)8bFb zgM;th@7sSd_2yFE|2Ihd?-+rcBR{2#`J(_v90x5mquD#GMLr{yJhfWg&ulgTh8ixl z1Pk+$EZfv+@9>;P!m6u8H$X5B$MM>3OH;=1%r;Lv$~g`*2@kRHu^wF9j0YmUGa9%D z$azfbfMWR&J@J;p7n@=37wn-d1)P+Dd4lCS=>R3(gH}nsN|2}$z38YnL}$|ebV*!p zv^dq&Y+=DYALr9)kJnvRkyV4X@NT&U($T<#1{l9mOd<k}I~D7ON){>{I^Xx&E*c`M zGDMDXG9T+uzj{!l20n6b<6{R+jkXh;?mu`pDBL*qqmNgsX~8c>DaL<n2w!61&wEwR zdk+3QRH;*H{J68+S78VVAg@71a#d7DWMXOxW#R1>{5a!d3p5@THlfnzlW}1|%b#ax z=SSz-DYiD-_PBl5C{NH_|08K~14e14#n><IIm`Tch<XD-mMRRTdK)U!zByo&2J^+A zbz2`s{^k@BJZ<e;P#~o3WbY_*3_IKTD#=2<*}^|3!q2asqFcM10YmRl)mMmi?P`3$ z+r_zLU@w}QYHKQWJ$fc|@P{wv-qa4R_rSBDh3T)-kFVOZ)waX9s{A?$KTq^=pu>UD zWqH7TL=cd&BGzJ}<cNu+K}skb<VG-52|WpWf@RcIXwn(dl6TQI(?-ub#CYF|LhXvD zyXgzhHR`UN*$$HYbE_7fwD>XSsX-+=F}H3{7Zd~Zx~%zfMD+v-GQ~49yqMaxOm843 z)r3ew_YG8;%HpLowGm-ldO;A<yWS-#Gvv<Om_3Pml>SlLFTza#3#(Uzx<DO@8A*7} zn{Wv*2U~{dF-%i3zX)lZiHuEcq>vb&ByTuKVF*jVy@aXt7$2&0pV8(@UPjS+CSd8P z2UeADnrZd~q$0AvN7qz&dY8hz9Fh$^<{GwpTQsEnMwltmns$6w?;RPh^bR{9>|Ai^ z=I3#PUui=18Dq~6*R6MlZM^;dw|y<f!na%g;K+L(T(%|4euZJ54(mUZvgvwyKSx}g z^&|-Jlgr?QL<6pxMzo&%)}Y&UN88o3v5|OINVCd}dJ-&5Zv9g5*mkQ|5cY1+Rrz*a zP{t|ujOfaFHSCU+^CA(%fP+TFE7U2ndN_09hXL5|sfpCDS69K)w&Y!r)!L!dM3$Ck zaK_#QseG0qHOQ4D@9%B2-Z+t9X-DQyyL*`o>W27u)ejxwzFbLjj0}-oS@25K<e}IN zdCnq8F>;h~sIRXz*CE8kh}mMKu9Jd#nhWPN{c;iFZ_yH-Uuys#b`UcJ6Oz_2T4?y| z$BdAAU)$breO1ZF`+(6PR+^J=ic}>I`qB5HMAdIg$l)C6oeDK}t1d4SwBETL5)g4A z!i7GRN34f)G-ecX>97ANeJo=&b7o29^8gl#I*|L^q<}sEAaw!Iqb}g1=a2^=`p`iP zVtEaSX5ftG$FIHhk+c<4=Zc#pdPNSNMN|47I}_5{z<Am8#KzCJO%0Cuc|M$Fmo&ps zu`XbI3foOAh0}WBd)ygp4+7gK)yUH1rZPwl;#n5eXnkfVx$#mPzx80C8{VnE)<KOI zUu=6|WrXppa$}yr0+A1!Y5c-4F&O+yAe(;~no&QP56B6%r*I1u$A~`22=?ZoY}uJE zmOb-lEU%HOtJsaQ8+h~Zf>*k}2?aq0_Lz3Av><AAdD|)m-=Sx6mbO<nPj<Q>)bjT` z(l7n*xz*3(98VA>T;dp~vRR?O5#Gv$Bv~E!lGve$#t;YcumJj1Vgb$>BE{R2aDT}} zEN@<|p`%|Ou0rdw-4c;SZ6a$C8hB-+SH%nI=JFSo(Z3{(>aXt!xgU@lq`jk^WU=55 zeB<9B0ji{+?uX=bqSas%-YEYmn5@Lo1*#@bOiF|rKY6H46IVIf8ER1y2MygC<b+wW ze=cs-I74uwiy4=PU2=mCnf|%sR_kxQ9_(|DXAjff!jrbnBo<f7!-#iCdD|<RkSE)P zN9Z)`TRG!e_lY+9tvLODh^}(%5LK1NaFeP408=+ISxD0!wbo8}g42#y;;EZrEiPP& z8L6@_$nmzg6Iqebp6Ln>)bOptk-82LN}ctP_0Iljt0mvXq_<N7c~?1_EE9&F#43rk z23FHz`-KWC<zA^rqDAWB-O^~iM$r;Nli}1b!>X2qB)gmX)~hD9?%vatrkZSXxbvaT z!NEQxUtbAHnd>k<2mcLie;WR4>h+`-m){QljQ^;0{@jb}2>$zljxMoA%H`gf-{JQ* z@=nq8=h~g<0ZxB_>;Rrs6lef`x@giXVG$d&%~ZL6*OGlA8+|P?r<94wR<n;^ehR3q zXwSu*+hMU}sD^jF7jO>7359xph*Kxs0WqI_|4eUH^i;;v<l?a}p||_gSLR)lF5kKK zX71OW$l9AfS)NaX0JxTSgM*&&P-xpoIA9-uli*8~7C*ysfXIt2`j{zcORQap#q&eV zmog?C50?(tuI)Ty>oyds)1Z5P(91<<z~Y_Tfw-XqJeLCUjxRRhHP2@?;OWA0s{`!^ z^oV(!2t7>e&l}1E>3#K4*+tw#4%4S^`*7fYaGDJ^pzh_&eay`lRfP<5z85_PjLwXQ zBWBM_B?}{Uu>PP5?H_$bu3;v3Fi%($d1{(Uhbq0C7p|Cx%)wFr{eu`mG@~oj*|K7| z^i9Q@**i5C<r^7i`BLf)Pz9l>D?GTEO8pO0`ZkVpBwTWGp2RV6bWpu(YIJrxztmOS z(iXoGROxje<>a+q%o+92P05Pb*pw(dn=^Z&_bWUU6`1CLI7BdlZ=9~<^=H=ogD91n zisEZlVm7iY&;<KVx3?J8)rqeOdZMsK+v;}UKGBaq6PhR`*#gkghKGA>qrxzQmIE&_ zmBjMavjRS`rjuLkYRvCsBsJ(lYgTuj!W?P%iGzBRXMUuE`O|Tx4p0i&w9k-eD@&0& zI~JJySRvv+b!qh^A+p2Ha6evxL6w<gZ#P=Iw6i#;D*&g{hv<tH_UOkf(9RC7C~bQb zY`<Ax%3Q$~Dl#mltzDCe{7tC=(fMW>!rP+n)6i=<!~?$Ej6F@!L+|_ckJ5b^uf2d! zlfbh~Q3S!yEVy+W3k1S>xmlKE3a<mtS9T&|KOb*vBy_ozohG$E4C03MAf}^L*-#)- z?K{@@c~BiwxqgNj8lsX=PEl(nUmIx9J)2vI2mFH8#uxN4Z20%cRdekyO^@%1Z~+fE z*FVCh-!gF-{&k<mjfBUXGent-Q|7UvvQJGD38FGlT#eQP=0@BQ+lGwN(#375Oc${o zRce%AdlU<k&~HTa_A1Uj4it<`<OF^jw%0>H-URXCpu7>?Y)%2Apn+peycVGmG=(;q zFDj3$8<<F@7$_+6-3pTz*qek043R`7fwhd)1}&uUf0T3r?blWLEd64P=qgNbM<{9; zVV(k=gSt&uko>;jNbMCN?%FCo?Fy&+S-W?2DN(vR6A3Gx7{*DsX~6h>I3q+@9=JkL zSWUI-p*>koTcL18FXUtNM$xKbg^?JzzZ;^>f5DUsB*x}Sp9wAemV^p2i>`vRXHDPt zOl4gf+Se7)l7CWQVsUrUI3)BCm-jvsj=Q-a_WwKYct<=wd{V4r%f#4(LX$=;;mLm{ zdPJIFF$C=gg(H3~k1?<Z%jQUe%&74tHIBfAGfE^@kFX3oL(O{II!$elhDnUD4VM-i z4_3VgBN_emc5ZGdt@OOv-5_{otbkhzZo{q12t-vwN)?(9Dd(VmL!ahe6X)7fT;$oI zAfX7Kwg~<MhzMShv6K4q#h)+yMg`hFvKWjpXzHWZX~Un6!7V=yz96%*{&@fKj@bJ+ z-GOLDiHvzfiI!p&tOy9tdrAgGvd-COwfd}X2ux1(7$)Lh;?+$oQ*1zhex>-i2oKwZ z(PO5l3q}Z&o_oEPB>Wf4)&(wI;ifE-=4{5GtM$}`Ny!;q<0PlV4@ODf*u)Y$*v>y8 zbsn}C;ZRfsNR=A6{j9E`$ek1hv-ep$6kBJrYn;VJbct0RInIM+z#gKc)>w+|%%xUw zHAm4TNn|Xy8^KhrOsSnz^9=Vn9ZKV~Hgv?5-SY{WIhzmL-V|*c@ldQ8fD1@xUxFXw zoVRfA*j@Q*ycd$51%<G-uGZ?~)kBm73JPgFm@KF29cK&xg)AYDaV&_U#nc)iFT+E2 zp<fwZA=589Ctq8(LY{muo|S*X&u?Qgf~$P(I02F29a0xEQBqjQk*Q;Y*=+WOo)QEZ z;>G6sovPH>;2L=exQr{$2DrHV`Z45A%%If&+M4EN#aeo^M|t8*a+wkAK0ot58`-hh zJBo;+&%}C+T}Qd<tB-mla5<!+`p@i)?mVH{1hDti`n(fDO~05MbQ&ihhqs?!E8m%7 z`TP2Rl-SYn3Vl<!+m13`Y4vlEA+_|5@~#fKI|)Tnf1YfA3jc82k~GgV=BGM|ML|`6 zUSMA@qMx6+kaL3d^grW4Q6aw>W>cmNU`<D`y1XXF3*^g*ziC`rg2FTxH*Vmpsnuj4 z&DPV`CT&KGoc4BFMj;(`6*<bcBce4ulJi<3f=pW<TJ7>q+GphATwLO-T>a;Qaa~Yk z_*a$T4LCjQfX`ln)HzXt$oOWYVM6#x=7C#7Or!AWLi4#yl)L1Nf?R^TIrI{%Lqw`9 zb>`1FfLxKrJ1`2<iGT1i)t9F1AWbEW%xvhriBLlyi-ID1daSH}eBK4)<GDgjSpW++ z2=x7crV}ieZN{jvV>P>_YUDlD7(LPUBARW!4EG&4+GRQx0O}R~oy^FBk!PPXEZiz` zp1giE>~(wQ*ua6F3p(uiHWw)@o>)r4e~KlR+Q~P;2Rqe*ya4h{)IcK<ZHU4A*=$=1 z`c**d9k(}N!fabgPa50=lBFe*iN=1**7MXP5ve9152re#cQ`}Fl%<R){-9=A`Sa(` z;XJ$!E<<XemM>Lzp)_g0Rf>27!Oe8yZ9M_o62jv)#Seio5T=}57-Fypvl{y#-#R>~ zhh?)LjqI3a6PgGHTRa9lGF_V7t<A58o?tt7n<<Q>Kw`Ng{Oilba=WN~U7Z65D<REr zrvsWDmd7je{sKq=w4RJ=XRqrbmMYY6SCoCIcTyfyIA+-b>9fJSc3b_6g`Qr=ZQ0rt z8Cgxq8^^gdUv1lWTRR$;6=-}pIv0WJB6woY3Q*1+gTiWI<7^^KhWaBNfe&h)#H*ym z+b)nEV|o|t&Qa`27vJF&E6mW!#%Omw|B_LnVtbf`x@zQ`S_IQvxlCF@vd^TS(E50< zc6IdLFMqij;dSiaPA$|yO1iZaB}g5aB8Ce&joZM5XtU2Kp(3GSeXS0D4J(srq0`NK zyQb_+`Wd>-Lp7qs#NuPjz>X<X1g1do()JqFf{;VOPZ<Hn=}SL%20jY;+47z7E`edS z&+=Xz@&JXt18kX=o``Y!F#+$4k72zRtDs=S>vsG#013%=uv$E<NpWW;N;Xw-Q6k}- zv?LbhmX1xgavZh<3D1CFzR`8d;WOj+qy4<QBb1-l{<&b5@wr6oM$D--#r}Jc0;vfZ z|D-dPAV0-)fCH!*&H+#e(g9S9gs$_JNdeCkUIUPVr+9ILYr2_-eiW<&31|wU97t^L zsPPWa7|y8WNJCV!cGZ6o?d!I1rBf>_x+jy7_hj6Zq*#*=x8OYIu)0WRHU|lq==YOO z{Cj9azoIwA5{a#fjGs)s#!+S&jrdx0DK<u*)6U0K8V1+mJhZLp((U4+lH&*bD%)$U zh4xo+0%ey%Z*jY(5XKaB#fX?Z?B<B1aq3d%wkn}&1$Q9LyTbIxcFV4}+uK2^1&55O zIgE`X;U*SR0+0nk<Q29*f8#$1G#@gL<ESvV4$)ObEg{Bl1BG9bpWJ#BvSTKCFz+EA zKj+29pW)1OdQoKsF5X$y_8s(P^6k|DS-P()kqbH(-u1mvKKJMH-{+m={R~M-5wBDS zs!TcsH=>NY$~P%~?3pBy6ua;j;w;_Xx|!5O-H+y^+LsdY8bHO%bba%~g4PK7yhj1d z6G#(=EUFJWAME_NH8f1E{HF8FRvL$l9z_Q98vhqx-xbzW-o1;8q9Q~@h=LG1DkY<W zC_-WxMMa1e6oibT(u^5L7=nas73pHE@CgG^5D+Pmu9Sq%SddO62q}z(zM~NKkm6Z< z=YMm~bDneO%tgq~{*|@f^%mr%mQsH}o}%K~FOXPjHJRFAwuw(%H60LT0Ss;7*PL6R z$;N&|qVsO<cf3Pd^qY8cIrdN&cl55V-3@9NRcTZN5AP1Xx@Rdu>VRO>8*rnYQQ|*S za+ulzvJYE}X#+ZYEiV~2mIzlcTt1>DL4*|+yd~X1eYitVl4;fbr+C?#%1*+`?jYL5 z!Sten;<iP|H0OskBK|gxS-oD%?!CKV?cH?wMHTz^C#m7VBg*C*t(VVT>3q<P97E86 zG9`Ye{2w?IqF8NI6aM+}-n|VZ0@fD%glW)?_#(5FzeUwR!%2f9q8Fn!k+n(20c4#L zf)4W+H24<NL`udDeP`}31Z`tJbcFq2j-^h|816Q@^AzZW^>*v{H+PXs{bvq!b1VZE zcpAKv*Mu^hQomwAP)+XfkpCicjAAVkF*p)Kj$#)!Jg`))i`v$63<KolUvI0|DtCZ> z+K*)fvBg9~%)E|g+L3`ejK|uHUJ0a88tmj5A(D4GX2;Ql2T!n@0;Gxj7Vx2cCh!IX zfN6aGsnHCNPo5_D&QY<Rb3bsgwfxZ(DO7tKDDW-h=Tt9AD;bV;{qtlkjkpEa9V#BX zTI*&P(Gnq+ncd*QnwMl2Wn&>WKCim0M~&;<qr!b;rYTST@zc#;qi8|C#m4N#j95>b zlIFb=uE-2Fu!;~5J({i3&|DJSfS5Ao)Q}7N^JI0kz!#m{og(4Hv1ZGH>yqKmP3`G5 zX?zT>Mb0v6d?01<AT7wp-s!oYduGatX|HUPvD7j=+Up2b@Kqr4BN+3=BSg%CUr&i6 z*~_@KY@s&XgGJ+Y)WwXOLqDp2A{z7TH=`7Jh)n(fRn#3Q==F0aDA$YhdAgu;>o@P3 zTf88ci$SouB<iFfzr?(muou>nRk9>y<(y#xwnj^7Tm!$IBZ^lpjf1q#4#jTetO`{s zYpJ?Lv_wgiRn+r{GC>SeFE_J1RYMTi=OU;vHSVxCkaG3>Z4odZUiT)pUl{Q5QgM}C zt)=7f*OkFVk+oAQha6=aUmOpnfDkdbQwoW|9&&$7M{$h#8ypFv1b})Xuqa8(P4}`$ z8%_juu6h|2BOjHGm1Pc=Q|jr97!M>`br4z6G0a+4q5b8X*Qy7=aH@FPA7B*laWLuq zv~4UL!tyEQipTH-N?6wstOi1vL)J*V!kD3eA4)@MgAPl+-h%Nk-)OHG7m4UADKc|6 zEbf^{fU8^}7a3M+4>;)8P7@<&-iFtLvZ4a9Acy_mhmUsJ(ENvl&N~maUnf_Un=2!W ziI`m|7=JwrxJJr(4yihBu@=_Nh#RGo$h65<HzS2rycnsulliO5mjfNnA&Zm-;6f&D zpY-(()!9Rb=&rrp1I4c%Pvm(Y=A9z`_EX~o`(u{;I$xE;KacB)k|%dDZT!Te6xjRK zNCan@4Pn7Y|E>OV(uyi9LflMfz`I+fW?%91COr+vqGUVd;;)|WMV#!9THN1r`@j=i z79=qGiXx`?#!%RE*@U^peXfE%oh%}AIrbFgQv}4kFXe@8TqYupzgnA6p7%MgV(rwn z^1$ih;9x_v+{yY#<(0yUji$|By8&!DmMErIlkX6WByIZjMN9&=8zQzJdaX!M6+fT+ z7mkgs?y_^fNTGk6&+~0bNjZMA8^*h9|IO{b`n$#0VU`nQ$(qHJ#X<^ZX?9CVm9n)5 z`31o-N_rL+T5rUQ^B$N%EF?~tPlIdHg$ene>Q$&>m}J*(bFpnQ`mB>``_MJllrZ^0 zsp90L;$A7GmyOx(RL?hucTG?w^TnB*h5P`7DohsF^C2(~qA_=4iP_Q2v%r=ODF0d? zPnXhb$<h4z6XQ6BOG^!FjclPia*nh7jnp&vWOqi<O>Jj8flJ{{`S8gf#XmLv9ydm6 zz|+BFT%S?bKw6D{9lh{WxdT;KNOh>LZT7Py>>h#qLKn#Q^URL$Q->*NX%zB;F(4j~ zv%m0s&|N3Dz%K#czez@h0EQqMZN+qH!E>tp)G_r;%~^?{zfaBVu|im=_n6jTWPgbm z<3)gehQMkq$JKk7;N2^+t7w)aIEJJTi$AKCA5Flo5(IAKEmk_9o}5_g@ujD+EyK0t z@*C0|k2kCCPa*pR#U=s!)s5ZOY(ov?>4g~u@Y`Uei8EX`T-6J{IQ^%_EdE8Vq54hg zikBFB0O=`eB<QW}Rxeqfp5@zbE=vfP-5{+P)N(@;GV(=!sV1Rptv-g8d9%-<E*le) zQx*Pb1;1nLS$X@s<mXUD6J)w(O2a1gZDXme8o?8w$o%)SVCn+&Y<s~57`jg~wqC0m z>FKQ#6*`Eblm<`=B*+jpEemVT`Q~LROU;HlkTc)0u8(@GqtaZ=UjB`=65XGb%NU{> zxewx#M6nZjms@tBpHpAYd-TLMdNL2+Kw7K@cL3=I5oX0#5s2yl+Y#svAx?J}0Yq~8 z=IZ$c3W{<$TsqKq`*S`Lq+?tOVYa%JQ+gg?0@7;XH`}dmUJQyB5W(hYD|S}Q)@7JC zmS;Y)Y)DP#x)bY81re5<@m<n4nB?5e6Q0cLeqkL#97keOC~BQo@X`c+!aGm1niLBe zUr*d=l#eyrSZ)Q^Ijv_MB}+?F4;U&a_lKn>8Si>LUuefUzA7nAd2)YSQvY)8{rqom z8O{R`zvd8W8)P%_#i>V?zN)l95e|2oypXpUOP7br*teB70krfUrfWG$zX5?5?JZKz zsffT~Z5@1#dX>O>Ji0q6cm}LV!@Ow7{>~tHex1R7!#10Dgt=J2ROEH?O#-$Jq|q@7 z7ORn8%85t7ps-i+8>S<n>oC{J-0E;Rz($k4MwW;SFp(<^A8v%K&&lA1m-Hdd9TT3j zbu=>mnNIn~%`MpN-tGXu!i{F+MDVmDVzyh=3)8{=YXg5YK;|JPj;KN)v=Z3>2};|U zM^#TN*9~xHVae6gHl>L^kZ`IMqr7T+qjKLfOwhnG?XplF7r%o@48qa--P(QTCyL`J z-6QlP{(tzDBhz<b(f;H8Us6CJf}SE0m|+-2LX9RAs&i0cjS>6yM7GjR7SPmhi3LAr zxK&T<J{-;UcQ1X4WMYnuNnfK&oFYp){wZvmsIqklv9a_YcPU63b}4zqIa;jF1m}ry zY^~O43LG7CWQ37s*;o0ms0|-fcM+)|>8OPSE~Zt74b6D3?Dp@BI8V~LLRn-=kTlF~ zUn?;%^x}<)zC*1;zxZ$Ka0;;B?Ac&(6F1U5gU%W!-an_g3{l)cMM^z4`Qvc4PO1#2 zTvGa@o;9~vm09VS<|tdZ_Wsad<(do9Z-}3Xwmcz9Qqo^yM4moV@Qsh&aFU*S^5Ae; z*H~n}<(H19zM|<u?)w(Zf;w6>z`@MGYA9Hqq~gL@kGe~S)Dt7h2{)}_HTjutN*W{U z?v+wv5dKo89-4emF!|hKPv1~E5<#@TijTFjo^KT>kS+0r^xK}c{b@k~zu>;Z@Fn^R zbWG%l-$Z(R^c6+~U85C0pNiQy%?PwFv8us-1M$NNcgibKP4Zm6#10GbL#a?DEDDOJ z?iDb7Ui_z$BH<YOn{Ipc3eD%{A>Z7Cqs|(gaksp+WN6D!QiL+G-68K$Y3h9{VnL8C z4OZf*YIfpJ4Vz@SJ@7rlxWi^@o!?<g^{9n>h(?NrCZ94gsJO^Tgj*Y5!jIz3DZ!e> zqT8<hOHfK3SO4^25rbXFTG&Z4_3y`HeiHHm%-pT5d*B2>vxrrB5-CM$LK-Yd)^Yt& z?`k`3$MBu=Z?viqE+7JC&W{95PQ0AFh=0+OGSGCLu3m*bsAuV8Sz@9g)1ia8LmIE& zVDzXd`qG7{!!qP{u_fZnFN`|oNp^i%`LZ`4TiCak9u#`HpwDS^CT9k-U87!D0@W>$ z<j+G97A;yq_6YzWWlBP?A>1J^BJITZP5nD%jyG&Vmrsn}GPOs)-a#b#ZN>a~jIw&8 zlCLrLr>)qg?2N~>yy9Vl)UU^@D!-4owvQ*M3ShmGF$>c?g^LnCGhz)y97EC9R9i)8 zf=jaLCT|Hw7q?UsqJhKER%wnPwoy;+k2fL)Bt4AVNLtxlay4sPK|a;f-?jYeP?^9f z%{R&LZs|4WEV$-szyS6uf|=uQBlMP@010Juoh)mYDjhO|gRmA<Bqqlwwey*4F#_5i zW~MfridQ;_Lvf~z&<11y$&f*=Up%Z{#Oq$wC25~@ig&9M%Jq_xQsY9i+`UADyw$C{ z$Q?t8cl$KyiJU&oMF{Q(Oa;QlmB!LxwvoCzl@wlQx-z+12&%H}47X#l+wS|6smnT) z&N%^8+@v$HPEbCyEAX9Xz_E_NJQsD-;iNA+-&Ht}<X`5Wj}%^{h+ptO#;LUd>z+$h z#R5PkS_lrLKJr`)@2xlyKwch{%~dW3_bJ|Tlvk0C3<;^>`Ih9;$}2gaupfH_%r)M) zNUKSQZQ64a-VVFH>a>|3@$kr=qP_+tEb)355$edF-tAKhN%F{rar*Q#p6H2EgZzYx z+03coS)o2ky)5x3Qz8n&w-6-^=?84%tT!-sr;EOQA12`i%rifHd8<3tbXtxA(ePKK zOLFI6VBfKiP8ULrw)Rq1XRnz)z=gh6oGL2ysiyY!%AK*33P*Kw_YXhvqDseSfJF+i z{a+oO7k-tfOC%@b9J?!LS#juedS)8tGBhj0!znGt_s8jdwDqap5tY%b&L^|^wF)_g z(&H-UC@!<QdCO+(UUrLN=zSVdw;{`|@+WZmgZOE~0vhdwa+hGzd5bj6_V`WV*oGjT zE#Eti6h4wH2)b}}?t2UL?H$orIMrUcPjwHufV*vK_DTkR7*L;vfO2PNg~v{FE}pp$ zvqVLW{bsKVJEVqk!$e?`#V~EMhl<=kR;L;746zcL`(D4Z^w(Lw2;>fe(L<_^ESOCe zKcN;-qnLVtUorSpM93Dqz9=U~28ODxo&;+OP(=oK3WD^`X4YH{lA9Rs>)=j?9UFi5 z-NL%pJf}B%b?MF1c-7wlUt6LGg>EA=euV{ChXg<JPC;Ut%Wk{6UAyFEfn7npB~4%; zXvkqbPei))hIz}-{=^sS9t}Af^$GJ@Z%U3Dp8x%YR%_t@1R3tLZ}Zq|Q$MyQa{jX< zzxiv|9$gi(B0}@-^JjNw+5f)a$pT-lbYQ?tX_}!h!En;VI}maEPVRx#$T-!I<vRCE zyo4W{U}}Sk!mULnA3r_>J6T9sI@6jTl!9zM$b0P(*`9ck5u}~sK%-wCnfN!~tFd<o zhnd+bP3Q_UwTN6|c7uN*-djwmZ|NDY7cKpVrY+&dKz_0_p~(mztqik%8Na3`_%23w zEUIc-FF31k_Y*eMp7DG4d@s!>OupSpINdqtUjpp3S4?B+s1_J9$jA*wqDW+hej3n3 zUBJ^CGS!yN%X&ur_>E^-&_*)x?KkPB+uLS;sxe6|p=AUJFF#Ei+UnPF#___7)1If! zSO@>&wB)D8mLqjG^#!{%k5TNuul=QNp><)i#qA~A)8gmY-#;*-r?<4IVBwSdbCJrX z26_*yYuhHq#CE6&mSB#u1Ol>%^pqGW#q!YJ8vfz|bRwy|$0}!iWGb;OD59Nu+|$;@ zbN9&H3a`9~yRWxspR(r@+sJ2eA4BWhKRl3<by<y^!qgj5lR3v<LQY=ruc{Qao}c@N zxZFXvBO<?~(g!b2VI=K7TM#qQ#Fa1kMIdI``Fz!@vV<Mpjz@HHh<X7cAunZUs~6-! zNjweHKIP6lY;Pk=R|Q0f3tVkRSuJaQ`#6M^gQaV^Rt;{>p!?UTNfrKBWbe^-n-s$F zlGh!R4qMdy+V6~eL-H=4jrC2zkNr=)%uK}0Y<@TeBY{WLFP)WI@JIfUsX4&e6)}u! zy$~~1w*6KqXIalTVFc_-TPfLiP*gMtZz|9-*fX+kqS?$%=E=Tk?;R}Zvzz{+U3Iay z780&0u~G+9)f4si;XA;3Z2^eMpwjFG11DwQfIw)oYN4dSP7rP%CP~3J0=-!`@wqU- z$PZG%rRJ87j-9$GIj2qyC9FrrK6v{*?{W9){Wo}9>)Y!D@fGS2JovmnV7lMG6z<@I z0n_XlB~gmg{m2-XmttiD#JOI>*BsP|0n+jLW!C{o@MKaXug7WPIFDB2dZ?JzUoYqV zCD^8u>uc1QWTn%e9K49k<D=vBk9hU%Q28!6M}21si}!9X5oJyrX|wV)W~RWaKd`o7 zMFZTCmPr0Az7Idj@i@N-Y!O^d%xYQl(K{*htU-5Nq`jp?abR?mW-)s9B@j38&Lt%s zBON1u-itF|7FRoKJkSjt2UJ<Biq-skfB)XIibYHs4|s1<^PN4UO$^fyPVWwUIbT*D zx*whRf^?0o(%$N^JoA^WA^T4+OeDV~qX&rCcDmdLp1n-zC>yV#FH~-o399+GSvd5c zRNFSC!GPtlx&~MER@5QBk&`tnls}M~Xt@-<EO?Eovvd6ZESq-8TA}|=H{CDT+on}< zV%rnK7jL+cVrR}^9ry>dK{xOCd09i`YP7vs_7JC3FuG&Faw|n<dJjqK_lNXSQ6qIZ zqp5~uR#@8kTUpZZE?Mxck}Bi+e&a5&<IG}rqjUuhj#*q5KUO<JjE%8$JemxlP2{;4 z4z5rc=eE70?0EGxdX&;q;z>9nQ0~ne+R8{i{QXj33c=~-*CU-)pKFjFdVuEr(goTz zHfkwZAGtv32?lcVQ(15$rKe0xXy6)p4E`xx$BzN`O;apnpha(YTF)oXzMjFw!(N-v zv7GFE8A9+XIMeGl_($p8GhThoOP$u$zyG&i18}zU$St*b2QwHglAnecIwQuwey48} z?B$F(;;1_EJdBGRVjq9`y1is=2Xmi9w8*c#D%Rh~^K*YPv$X7<WnV;B^IuY@kk9eg z$v4=T!#lWP3B^+*-ZBlyc$riEfH@Zu6;{jcwZm$X;2$*n$yq!}J2zZ+kQ+kiiO=sH zw3XZaB|P$45eR6ofIk1GFH{@2Ahlei5AG`)unt7>QMpRSmayw!2(KS?ytEszkouOn z7`-XgB<@sU<WPT$EBjZOOY)1s>};V&e}4^)Ra!eV|MFN`><OoslS*y1$*5vB@Rr!D zzX@3UfBW57IpL883*LCm%bA<)1|p837IeoCP!|QLmvoI&8poHq`@z6*d;)Ra^R<q( z8B+nTj-)SmQq-9@n!m1136Uq%L$Z0{*=uQjIlkqO?qqmd%Y%;``l-R2oSRot@~EUL zC8g3Z;m^DKW&&Xs{qEu3yW6N?F63@++P7$NecXzp`*T)Yd6Bl^h(_uR2suwAG>2ag zgqlAF$TurPX6{&GBMC9Wo(`md63js6au0ceMeXg(Qy$E=kQ<e<U+fNe?l*P!zGi~J zRd(pH@!j)oiZ^Z_yvQ@bPUq>dYV%HIh|CG*j(_piF$UZ{mUi3w48Y1JEUNUDR-Lwf zr|oI(>`4J{UbHbiN0iUW_y~Lu^PrFo#V?CWimp4-=|JC_rM4wN?$|u6qKS;2Y{AS7 zABp+epj$0ZeWakV-g|B7oPgopx9P~T&@6^yHL{#(JU}$Y(x!V|%3dW2>eP$Tgs5EL zla|+iOE6j=p5bp-nsJ5Z?Q_K*0bWxGJ)G&83Gd<<fO+wIVQ&<mKC#FJBr=H{iBz5r zen#!Zu2Q=yfrt^^BDLJQzdPfG^{CU4)(5BC32sG2uaX8_JY8z?2ckWzsj3a6<*c`y z!l@d{;uh(6IJGFansO`E{B`?S;7$<cY{4ARAJtx~UbWG!EXKwrLMFKjvEW0U8SP{F z#R~1@6p~XyajLZOgWp*{%ctj&A=XA1qHUD5IJZwA@f<Clq4He2_0B;X2mB$J1S)Eg z#escmjwQ_r*!DEzBUW~xYqVG~NPC?3PepO;^8VRp{T7|i(?D(t*Ze)a!vDkA+vw<w zl)mVbCyy++cy8B8jr#{QF7kyB)bnpha0VD$b7qR!F!56x2`UNF6ape=pbxK=McF$) z6Zu{nd%UhQQPBMc$?*T(gX-?k@TjZgllc`*+PaeZtt&VGeVg@Hq3SyStce{Ig@9U} zWB07DRo8xjn{Sxd!xw+f`<&93n{89lmGRzG=V0C#?aQ4Vzop_2o_n<9K#%7McHVQ1 zwAeR-$v(4MAYV#p*ASWYsCHesOGf^%`>h!3f{%W7`~Cu7{*bkQ%9`zC0(|J!BX{>% ze0c7<z3*+dMt2%NmKwuBuL0jQUP6v0{ere!N6xcvgMMZC6D<5_d!?;xA<>j^y!I~R z-J!x)zLu4HX_98j-pbCvIM0JeKDqaZJ~;?Iik*KiI5fTpL7;=G*$idLAf*Qkf^5Os z5s|yT!pvlA<V@K(JD2MwVLg@d7yFU6wb|38p4-}oPgbGnPLk&JA81-fXoii7V+EC+ zYaY0i8y@d<y5hGv{DX!XP)P9*cP^u;jsSq6@Lj#v8k8GdY%)i>Eka?3rAyRhxxT{4 z<Yx#bs&jW6X-1Li@>IUWVb55<F~}pQgX8VjNVyBB{&pwIToZU}&@ZpkS~mIHd5%_+ zTTD_?`qP60vOc<h@)A+c)c9Pu;I}9d?-m@qC3l;=yBp1U=TQH{Zr`HKYYtl6c^><| znlJ?xy^5Xc7O-~^?RhW|RoxQKUw$4fM}{^^vY`5US3}h!)Me)y8+Hly(DC1r>io1{ zlqP0wODhj4!#BAU*sebANB{h<APPCl{+L*6y0TSyT0;Cd4x#M)Smx%OLEcIf#sE8^ z)Lz$SzgsdIy1z0C>Z2;$=?>-^+Vep>-lOLp9wNW;M-k0cqw$|BL0PxY8aY~i9a#d= zmx;AVtdXr!4?=Exb-Czp<MesH7FZKy)sYR?@X^vTKeIZD9)F}wOvbH`7^qKuV_fTA z0fC-bY56JnamGb22B%7A6H7dtmmqU&)c>k&Q1VUQT8!$3lIy8jm`79pLfIlc;jntK zj2kt<Sql8Z(#S7}_TRKg1UFooIPH?5>6XwfxKbDDXMN>UZ;jpagbh6`KbUEUva0zL z-l9S+^%_T@!=9^(`n-$KXsa|<pi$CNnB2_+#%B#R(AiAFW!pIT40uH-eyc%454}WF zUk5)|9#4hPqdl!mJ#T#7yX_WU1C3j}81G#8XhtEzVC|H1>bJSM+wY#7zpjiT*7dzu zmT<}}_JG+<$4K9c`(saCh>tzEwBVK7odbP`?Cc`<&pkDJK6mP&%qLo{3rEpx<>>)u zcR?L>0ZWrP2b?=7%ZFcrx8prOFGCbx$I<Y$?O!GEX|E(oXxkkFGQX9tPYDe&e3a&Q z;q4yeY*BH@a&Nl07Y^S*qEhe2Nm<!#o4rt`o<{NPI~g;c02~uWTs67q49F{<R(9p* z<Kluo1`#8ZO4ALCq+8A8jadfjoNTYAu;4A8{`dzmR_8U8aNvgThoWROSgpMr6*btI zLCcwg`BVcZlV$6LwE&xpjf5i5v$lAky4ezy(Qy5>jN&DyeM_fHD-iGW!S<o89vNd< zAvEtx?Pr=-&|n6I=l<^=+i;Ft8P%X(Cew=eEW?lD29RaGdRX`YJ{V=ASux!G*%Hx` zL32b?>qu)*pPes#BHq2#$@{tcfkeZiclz_lBC2xn%sd(a*h%x7s*Gog4aj{}seJD| zZTSi;TS~<>NL9ExwGkRs*Sz%r_RdHA&cW-4N37xm$gasc!kS!+c<6O`XYt?mPK#PU z4VDQ7@}S9*PUKuCl7@>V1CBq^CTntkDKl&2_K3vPMjUh|?!MP9@@0+^v+Jal0q&RM z5R<+x<+Xj0+0qu=kf8h8nP7ncoYQ(+Oh8@Wc_w#HK#XTKxi+|#v{**1#~Cn!tNZ7o z-BklbBL?5SgJ~<FEMQO%@&uI!rPRd%wpUI;Kzl^m;9bc3%+JdfQFJmQIo@;!&<_>V zX;MH{KJ|Bp-atnn_K&Nhlku-eV9=(&hv`Ozg?|}bSP79Ti+6KV?e!GPfPlS7dHfl$ z=J_KWg$<J=D~tM^8m84~Iv+DYD{50Wwwb*y=1^WIAAmAN5}@PZoFJ_o8hRUIkuM3M zS^1RSE#7({ej-o900TC;){>ci!U2i{BC8)*edI_RFwI~@B9ti7qVDw8%FWNH!$}o` zrc1FpNVH*@b?LiwtbAP&0Dv?o)9v@>dFHOy*}4w`bzNkVnLC**1cPGbtYO7kSZN!8 zY-J1{nxauNnUSEDxB3+ZCO!4)&8Ta|0K=NLhq~aKdb12^qV^~}(X0rNIAuT*Dsvwq zb;eh#w{%Td8hsga*n6^rxlw99Y#x$V9Jc2Gx%C9>YxvT?U0lZ=AuCocIk=Y7lO?WM zn|g39X1?~poAp%!NpKB2Tr_85r)Uw=06U5bBwCRr4z8Y|N71R;@!1%)+Q{^2KE>xk zR&#FuAW6S{B3Zl2H9*JKr6BQWD$?)t#$v=GFV$Nocm8$LNBfXf)KZ|aX@TYPJAN_J zfP<d7hFZjJpt2J<3+){daUKGfh_qR&#>iy+cA)m_i57!FBNw*Ra>=l3YR9f_%gce8 zU1o-DqEEeN4hGQWN0<I^Ro&%_(Uu-SmsM%3WniP<GV&Qv`t`|(%&GoQl1@8sT|la3 zE0dn)=e0?2c~wW{=Ce7zJd-xdpuocJlH_Dh%Sm&3u@1@UKlx^*49&b{Gx9^z25}2s z2;Xlv0^^<NU=qx;`PWNCA4R*-i*W-7;Ws2N9O@^cS&c36*S<m*nH4a8*}9?yEFJgD zIJbgm0f-|AyMJmN?x>_Z@#^=eqvxD@LzX2{hdIzC!Kr%l4XXw$FkwD_f;<QDC-iWg zRYjzg*iVhO7&n8tSYW?V+#FB`X*{$n<vQNtcJ-zqyTp37E*4d{ZTdrco)Al{B^ed8 zTU-w6u}GH%P6@7UOHC+Xg#X(OUpWQgi`mG;&6Wr<wnzQ0Uo?3zjK3SxK#l?T`7#a) zM==rHK}S|eL@OB7dQ+UB<tA$#cFQ-_47`rDUF~qU)%MI2U|tr)8Bw|P3&$12di4@P zF<Ji`zTd5<By7&94x4}lW(B;`R7!98zr*q)s^>hMT8I>t)pCV#j^g!YVIV_?&A{B= zzZ%U@XJ>r+mQ?k+sB?V6B=pzr;+)*7gx3<zl0l=E$jRrwC4H|`96wY+BOdLX=q~QG zVnqPHP+gSx@|vbIGS$INw!f-au5JQva^=Y!Fc_kaWGJ>lN->YWF1LLAcI8!+C_&<w zC-a;xTnNZK{U(PIMTOv!oS|dxex<JNVLAO?hg(m(y9<9krp-v-g_Eh^B^WYb^TLL` zSphGblm`Mr=pHhMcdN3%grdo#43)EyTxZC5qttq>E*3D4N@*?*q<+arAVJr+)a_bn zS<a_iS0k?!8l;Ya!H#|0^l>;7uEEC&&6Ea;2A)MXbF);(yjE?4twYbzwHWN9?ZlA@ zvcbM5*cspHPpPI|msj3tI_12^^(JlWaT+P~hBvY_p^S89=m?TkRy*EvRek}%2>Jb3 zy0~lv!dZa^9AvrPbIws2zIOZ;6{nn)FZ$+&PjJ6AZ3&}4GS1H~qWgi3`f;YnDT{Jt z-Cv1GVUOO`UM#*bpbkaEuMqSM1?DRwwla1#wa8~wUqSeWlxa0s;`cmgOe%}0JSf3M zm8afA5f>S)lw%cRlaFM}$hE0!3zJXD;v>>L`#rGB^SgCOO^5B`CyL5@daff6|2pbq zUr?j@hqHD2+9P#Ktna^@0m}yBX9eH;6!Ghk)BlM$a&R`k_nmqHSwdVoQ?JLt1*JkM z@yZ?fjJ=Jdr8CW!4;zPZYI*D1xq;-xSg>b1$i6SO97oy!AUJlj_elGsGihuijeChm zle{i@6)#Czd+)TPEL#l3Uz~dWznP4>+N8ViJ~uJJTc<;L@TnKu$k%k1Dk)RITuHOp zd9W#F;uK5+BCmNW%S&&3hLP!DTaUkcSa1L7>QX`XgQcnDVTv3O?3=Z$H$OEBh$DD0 z7r{2@@}CDFdOW=He7jQrih}IxWdIWr0qGZi;IJw!0b9`WGh8wx-`pUH<}x>C`2&rx zgs8WhW}2Ak7o0vKxR%&s-TM;P_2S<sc`rJ~2Wrm8I(a$DiLmx`tLJTyIgZrHe$?@d z(XoiEKL7H5K6e;d$UL3-ZQ2Tn&-mEjFqZLRs64*8<PPVo&mxecT&<(im)`$78oA4# zQ8dYefCr?5bwOPhTrE<*S0Qf(ruV^vxF9vBS+ih)d#L9GaF(%jzNM~3i81yIq&8?) z12KYUu`x6kBe?QC-nHod&0ASeTGm#%JChN&za(tW+aDA?XM|>NTWZ_wI!f1^x=xVR zK=(ZDtQt!MxOB!lI_Qcw{2%!bcdw8A!b7O1yO;Zido8r9G;2D<9>iE2OJDP%@Gki& z1+xi&GDk*K8p78-Ab=#cc;rm08g#S8jxm+Nxr25Nz^lTx)+HsvurjK~{m%+%vcf3@ z$qO`D4XzNT=4ewy?xe{v4-XH{Vk~N3o8-~>r1^HBpdKeLVbvi8j<*2~!YUdG1>++n zQoaF}e3CC<9t8UoRttYYiD)qvex~d4d5CmPp}Tk4uM5idFVD{iu`Uaqj#o7|eEtk- z<SW+qD|So}BFIHqBMNl2=j8!Jj8t>Nk$G7%>pvG`cjyu1MfUpWck1H388*FxR|5WE zIPPr|on$+QD&#Mne%SStjP`m&H@|PFcDNs_KFb$BC!m3DBRR0*g{sU+PLC`3CSUKr zF=i~Sj=(|QMo$KX+Rijc!}PtnFIfatnbkH9)L%RKPm(%|7&VEQ>ztE+b&j_l-oVWr zO&A*bobO!>Lg1O33gjf5rc^x`v+>Up;|aR<Ci#r;!|lq00|MFjt;&;x@QJwI|A;GP zi~3DEik-|==X9)WZI6HK@!S_=z4_Jk;^d>(?aS1-TL9sekZ&O<g=6B^4;mX-IWXI9 z5w6)SLnv{VCgKzv9gKcPmJoyf@wL`&e?{KjIfFvi3~4B`0869IemgDIj`t$!4%5%B z7|!%shJ29ndyLiE`2S9<VvyfBJ!`7HcB;~NMm_3IujQ)D%hq)41ZS2NbMvzRJ7Pn# zvEwV09dAQi8W1qhj5~&A0@ZDOlPk(!7P}IxAcBo@Q`eNE4f%-~>)up!1(H7_iAPwP zMu>QpLK}J&U%-7#^=B5mau?7=0g1-09>&5IKFU+FtQ$5uSNC^X=Ma};4kUf%95jHD zRXb(djqc?#Lf|%pQ8?qbVLR_W=!<&$jRb+5;;@QCRX1_N+xTmcb1*ox9dT6bMry3N zWI8$O=s}2L3B-UD>AefVvQZh+T)t5~YlyFHZ~0`%sHIk+gR66=@#3>jee*KuW)f|0 ztw$qXwsI?X<oV8B*=Du>X*$@zUd({oqEh`{K7HpYJrfqNaggOU?cV2ss{Pbh()^Ck zofhX(+S}z$^IEOk-#R@vFbm!PDkbmn`}KZ@@GA+QEDe;CFovyJ1#P($=}A;CY?C=P z^5@vwpmj~iY|<|nz3x8NQODDhCFGoyzN4;S>~+2`?5%gtOyqtuqNOAxhe#9^DhI2H z=40Evz6j;!$>uRV>&w{xaC(Q;x<ZKHIL8%oq){VP68d!neqCRhleqEqjIRxE2Q<dT z)Hw(v^e~noX+D0mO?g%9Y4xtY+@?D#7pXOPaVkasq@a7Jv)vD`aM$AYC5po@OS(&T z;f6QGs;$XBEtvfexgAy_%aJtIkz<e|VnfS~)P=H^7)z#YuF|~=Tph-Z{Y>YDxopgv zsPaCI3Zf>=X_vvNLES?mb(~5*6c9Ix5S?F_cjZl}T%7bW`&c}r<}4Z5mql4hJ;g!H z+grA#27U;>?6~xOVmUiPjmNf)33k1Xt4aE`#V40hcTz?wxDb>sbnhLCr)QoXyytlJ zx#-M6``Zbn@?i@xp)_cQM66{!Q|QA#Zmwc|GQ!<tZsuZqk8mf9fJj4%5gkAL^p`9k zsq2F+&k6vkp(+F8(#viKQ0}cj6MDURRp;S79geNm@f0_Hg0{Bt;{hcFDm4+@derG( zMDbKCm<bU2`xb#Rl?*eRk2&&M%p5k4rYezV%<81h8RG^~BN2nZP-zkpZT-fg#9@tF z`*=Pwfx|N#Jqup5|4vZU2`BGlJsn!&Uef(IbX`vKg{xQnEdKaK3-ff7_rWGP08i1* z5>QKV^-IyFXhL-YQWW_>tl%%tXS4bZOJx_cWjkx#KO{>YPf8%zz<pAEFWq05k$Qh> z&-4JLgE+qW#^|WoJ#r-<3oaXheYYQRP^?6x)-wsnU(7$cRWUbXyU|23Cn^hN*Ki|5 zy1XTfW5Xw33obJ^qX7IHq4=nLn~HY<J9k!{3&YozUwo9={G#vlpfD`&!_##|o-F`z zY8F|$zTBjNMEZ<+ibZ%PS(QchC1^3m0p>(~;;f0cIeMcu+(lT{0fgTgG`jvDtits_ z9$>hYJvVRiVdKU%4;`;)UiQAXLML~@v@O7c@F|S1>V@HO2xBnnn5p#2Tnki%{EOoT zUW2JdZR3*_3d$mEt90rX(+#fY#h-+Jt!F6n;v~o--|4bw=I$4VVlF)MEvLz|5QnOi z+#hC&Uzod92pdpiZk*R9zD&?Sg?cRzdptmMHQx|KSq6D}>(Guy)~3#<9hbx%#oxHf zy;Aa`A@{0(T#dr=O5z^|<L_JM0pi^{UmR=g!Pg;Q0A824ESl3u&>~;tUnei9^jM#K zHT|HJ4V-b338r6v%nx7KO9{y!l}YHje(p%JU)j~uJ<V6o3D$Rq&}h#R%zRgGnl;|} zUkzI=6+J4#Hh^FVTkkef0A4}K1JApIba1~`7NPv`N`mB_PcjuOJPDq~o%u|nILvA= z?EwX@Gfy)hzzKqa<6PP%;&&e9nTLfKq$VQDJfxluybP{t8NHFK+{p`=2QS`dSWVF& ze0pT?FPssYQ1;(!1SJS?nr?{}Gq0j?b*v2-4KVa@^~!c@N*rOn@~nk>W}ajx1z#3@ z5et)=Q}q2x6NIMxG)8+<ZU3rmdA=(<1DlWU>Yf;-xJ1qmta3V~`ZH1Q{_H=HAFtqp zBtJF&fd;(Bfj^SOY9&lRRQ~ecKo?PM5K=I%H2kVw0Dn(Ypekc6#0d3d^vHPJG-c#n z2GdHmNyspk$32ibUgsGEn67xxuw+kp;*9|rvA)8@t!7F{jmvmh6awK7hbmLXCKMjc zHhFZ*?kh?@QWwn^FZoEBb{^%c;rDd>sqsO2oqRP>9Wl!g;n~gPPF!U7#L2g+9`FpA z#;Qkn$OxV*;c7QX2{=Z6hgYE>uQI^!^`)c_*}%w^T`1=C2tSX<8SNJY>HN_*(2;TQ zVUHs%tAW1A3t9;rEZ3VyA#m;>`D}~w5FwnZ{CGs`Tb}_uJ(P5Q+@22kGxS5=0HudJ z@GG~L!cNs|4eGJqoQEZgr|JpX%zdrJY`waykN$<<+*7k9&c{zehWi6>-t60*YjyF? zKc_|RpF&tCWJRG7U;^faPb`el90MLUgpTbdW7{fR;KH`-#Rk+1{hqPjq*^{`NVM%= zsrqYzBg~S!qjw&m>BqV(^MW=iZ8I=8aDJE^U(TRAd;e);u7h*!GGiYq=0`R;-Dz<Q zrxcb=12(k)4hqfl@Lamg6{`F|%571h?Py)P`jcpd+Q8Q~)d~$pZrBinI5DnayV|6` zE-VXbd>272N2_v&r1|8e!$PO}j^bvDXV~8T?OYoM_iPdSRNUdJ5)C!am-`os%=(BF zMvY`Lw~+n#!sB9}k?DSGqo*jC3}P8#e&ne1N}4J<K<5cJE+}PV$;tK)8Ck$j)z>~A zdTITJi|pWA2!^N^La(XGmtCw%V|W1CeMI!JqTY0!%!-@&&D69t5W>hLy6HK?*S-{m z=%zdyG;-wxNP8}GODqqmzx01-{2<(D_WE_whO>_Mx(;Z4TW3%Ye*BNoVW>22DEERm zmVc*m4caRa+$#Rsr93{sU4ca%Ld(F4IHUdqa`5@E*UIkv+=;4f&Du`_k}IC4&3$<J zjRElONI3NZSat{3s-noU4J;kx$Jt$k^guTb(*lDXJ;1&ZNQ@=SW36VGOUvqzrF$Kk zB9$TNk9r~bw*9L7x_}^?NAJri(Sh3URGU<m<Cb4b9_Qsh(>t?wEhTFIoK|0$WFY?r z3^a{4K*Rm++E}~<5gwQ40ylmZmRN5GD>wWJ^Ea$UOpdztq2Srx-)ZYQC;Ds~u8k#z zgq}5N-8LRfUvziDjoUnQpGKI|KIS^W8a5G?`T<hT?aCeKxP&`8s#wWTG=XE7xQ%Hp z7R_h15f(D{N)f$b{Aoj>*Rq&2Ki0<b&Y+!&o4Kosi^`Hh=C4{T<#7_D;x)sNCo*m% z*=M|$DF35dX(QdiGyu&LP4$Gd!k~f&+GR@s6G>Q*v}()FUS}Ui+4{JUb&tM03iY<+ z<vbfM_f&s;>)~%0d`Q-`K?7jhO=>P*+|2*i35h{QTGU&Rp(`kh|Ldd(TPac+8_@EG z4Fv?${gl0f126meZW3}Gxy1QcZ=&m)|K*P7%6N)o^20OTq@nKf{&@ZVa9yj+1Y~pX z6jwbTib+j4t8VU7{|!{322MN~rErDE52}ioVK6jcXoCRcGsu?HrDNG>?=GH`gZtNN z;d##d;&`py=qNvW%p%t&{_v1dVEKn^x%tw7F9n6AWk~d#X?qAu)kv$P4sb>Z3bOjW zmwu7#>!`7;Na9w_)*Lcw=0{z!KP|i31O&=ZNpIh^vHqbk!TyqN`;*yzf8vc&8uH>l zzjr<AGKHI)`#tx<>OV+eLK;;A2kHh$cv#DM`Bl9RSHo3b+AwG0a_rZZIC~3wE9PaU z<5M;=Pe)(9<hZox#YeU-PFEGDa8v{i??*-?&t{T|03r8PrH^nfK<wHNCO3(v`TzM~ z8T$smm_G7TL*c@38E@hjB|=CgVp6GpHuntUnxcU}Ul}6dM0=SO322t1`T516dEQ7Z z#JirImqn+A`dzVkSd>@)`=^>U`wjP*)y)j3Y7PR=STvg%$_43Rmx9iE3oM{@*y)jU z8Sy%4HIs>X!zNtzZHd2oP$oAkvp~`pT3>3H|JPLcNmWAnrNloAth<B%oYISoJ#lW9 zyvpD^0WQxCxDleliP6Se!9rRHrG$|dS(9|O?^ORPx+V<VA$q;3f0L2nW19?KdM@%C zaBA!lvQ)|5HmZki!D+oC^6}uyg@0$BXc0UF`qdx(+hr}`V4bzIfv{M;8spaSG*SG` zKub!4>1G)xt|WzA*N;cbA}2f!(kH`$>y5^8t^{Nbg2(&?&5Z&#!Ia<^50Bl(?<`_t zoi&1N2FS(1%H)<Nj$#dYDKa@<K;pc2dqYj!d|T512@6GgrCHT2!Z^t}kdeb(7zyJt zwGQ@0P;W|zYvALU$Dd$|YS#La($~D1qW(WYBAnNd9qCD_;CFw!ZFvC(9mROPGr}{Q z4%~S%N4Y#e4m$kKpBfLlSk+YdNqZ-VY0Q$&MJR+~N9AHCwa&A4^>*d)0-2lAz!7l) zAB%&yc$)`#%K3$=Jn{vHc`Xvx44XHp!!@PX%-Blfe8xM+lJ@g=8)%t~@RL=%f?F8{ z5BIG&z<EkQ_m5+1_{zlsF=2EcxrqXxNo(6rjmy+KR2`<R%~Vui&QA?CXEx7*T)VGJ zNHYcha(Wb~Kasjb!=#MQ8O=y{1U+|n@VQ4%O>mImUkL)+C2y+<-9ZLFC{5$NJ0VK( z;h1u3mbwEe{#s4#kps|>Ak6rB>k@B^(hYU2i{tCN``+)_ozv&*s<!Md&Mxr?+d?~3 zYd)BJ-*@y`W$D4kMkllyXWaSco`%hSo-y2Dx&o!#gr@8XzaR^VRh}Kt2|fte1(OK( zw$M?sK99>_*I%1d@e<Q~-Pb?zmcHQzIe55s=I7;F4{IN|@Vc@R40z`EpEfw4C3*;j z9E$-9`^-;`&jwWV7+V3Qj<5ryG?Y8T%6$PU84!E5Y7hhFoB?_yK~K3e1FMjX*Hac? zQPt{AJJ9^H*?qqKYf*M=adH)|{+eOlCG6nDsK)_bML|_W^l8hl_ZDaf(7lN{4>d0! zNS+O{nTqZOaC$9>%=B_Vk6a;JGte}muQ)kKD4lAknzO4zx%ujVF);15NJJ`Azpe75 zVIDoq^xg#&<gW6RC!P#>d$+Xq>q*(%M`P)vlnl>7p+jFvMt_mc*NNf(iElgMhF|Qy z+or3t4G^7u>lzvw>SGomhUlL7wH?mZw+kAU9?b!^-P`Ec_wSeL?^@DE{VPkYJqJ$J zXU9M!{tS8f?ZVo{0H25Nx~V&VjapxKzP-^jM{EmeN88n%Jd<vFD$IBo*k0LGEzh)@ z9XUC!$20}`6*aotDo#4f+b=r>f1(o@X8xrKMp^P8)gu%se4St(?iZvf-MS*Seva}W zC@XRQl8C)T^@lP!aJIXu1{S_i#UcO5_~%869moyHUa2&v4rVJn;U#4tsIy4eBEJb) zuzTJNu!Uw~R)K8q=~ptx*h+nrUQgDws9azc7qsJig$x&Ct=icL6Dcrl6!6w|UQ9mG zh2(+SZldUU%ayBs-u_ei|2~5LVjQ!MQ!gk{m@8RWkW7gUgA<%2o4L7rRC!d=GMjl$ zhOY}~(Rd?`-|}lYXm?%=_G@Do#G`J<ICa8@NBqlaDUB}!%lKtCJ%-IoVis#4HVAr6 zq<q#pgBuntL2>G0t<g2qrU(S#(v^5{<Apz~nW-w8phFFcC(rmC5v(~Mf#a5{)7dL& zx{3H|Vg+c2M7KcNtot2oN7qxs%Olfe-p<|1byC_k?Wt~NK>BH&)f7En+atpr-^$}7 zlk*u#y(iRH`I34ndWoo7{~h{YeRKI5@DsVRi_rd6X>5*w>(F-9Eo5YltPUbKt`3}J zp>7`#O_DzA10Mj{W-&j4vmm$99F%l43@_fnf7$=_%pLF~xLRxI6e6_@XfXO3Taq(4 zw|6MmEh#Iu(pg&NeJm-Adb~5pGWd8^lJt9s=qnCm5|m5W>iY*3x`_0M5-#X3wb#k} zza8MN!OUd}F@1E6xEN!@;Qu$wPgWsOmdB6XBgC_oV!d_XLsc|X9uPgLV-qA=<ZqOa zM9+P8!6}DY9MZaV*6NL`tNysYFwyJYEJN>m+%Fz#ZRiU$pTXg<UERf5N?NWARPhjh z+8X&fEa%0y#QvoPvSxlG2Y%`KboCZro-v!^VXosY7|@La(PX_owp#o7=%|g!!p4qc zr3Iamg3htAOOlSWrBJ_^Elg}ZHOBz4>0}Vp^EX11|F^%F4H}t}1*_3pjDfqVG=BEm zchv!k^Q@*1{o5)4Em>KHMpJYc`pz<A!ji*->2q6cjCC$0fAVuHE$jbP_EW=)Uy4^Q zDU<J1l>i^6PfKn9*17w^KZY>dkECCOS+h(j0cpc<=8ZFTo;pjo`{xdebjB}_07PH= zQ)3T0+Vv%Bsa-<Ki$V80G0**tT=KF+gHu!6zF$XTZ2s@uIuz;#h^-S;A&ILVYC^!{ zv=;nWK1huf^ixD^9i_S#{+nDxi;5l>)R#}BnqlEGLQ|rUl_SGRtzJcb(b+UyFPJ7J zKJ|S=m#D0ek$Ajw8!#7~i%RrAX*+fjCyn1d!L_|G7t<k8_9P;HoT?$CL@RX$*w_aA z2u(s*j_vj6gmMyiO$b}g#+=d_;dQC2+UV;$O5*iB+UZKSx$?&^27~24dPE;R>kPm^ zcFS&SztvmVzhQF~eyqQ#B_{#Vnc(&$%Q={qVwXP)SKKkKbjxC{z>363^i=<fg0776 z@WYkeqfE2Bp`yVA;VrrC-JTZ4y?s|KiwE_$?yuHB9%v;hAIw^gKuYIDM9k)(dqqN= zJh?+{&k~ACz3wS=lvgme<awqb&w^#IT!c9j8#e^bEo!p>j~@myfR9o-TB@_t2j$&f zrVrdv<uzuo>z{I0VV3Z7VX-f=T|En_(d3JTCdUV;wWH)Zpr<9{gvRx@B%9Xb`!JIr z#TnLHJEV)-;JlBXc3)6qYv*{qa(Nn~=|q`e-M%6Y1s%)t&i49soUS?i<(HMF(9vmK zMc%-PQKJX+fQL#b2)ZZ=OE<KLI?@bl1Qzy922!*Ar$&INN2`u!pn9#GiI?apk`Sw% zDRuE^@4$rVc9dS5!?`)<1P>M6YC@ulDNC{MFwU&mCdA*#vnVFc?Z>XxySrv(4Q;WC zxSHi&BXuyYn@->;Uw~j9I`jXtG4H>=1%WV*@7X>|!#V1={L!sHHFAd)mOnLsx6+De z+o-##1<;p(^NI=$S1)MGU_k*o76Za%Q6at&K{Q1}>P@mUsjEBe>G;tVoF&#KTRJ?! z&VsTW3wqLS@0g7>9q5=i>+^zf<&w3h*@5<AlQp%a?k-Vjeujofq#Z}O9GZB|k5!#P z+V99hg1o(L)N1^4J}oegh-VtUDrz6iN<H{nN1j=ydI5FlxX0;V-_bP7BPKtO-1}9V z8x(Y9$9*>qHJzWB<1xnq2~ub9Q71CCQfmY*CCwDscs$EOY1qlzs62{sr7FByFO(XM z7zH5<ho;BFD>v?}BXK371-qfc!iWc677NBkbH{>`D^j-oI8mW6$Sdo;|6vuK`UvFy zbZm8$a<RUYIvPLY=vjgr?M-LFMqa@pp7sFKX>bT4$D7MzRjE9@kWoREndap2a%49t z^O#;}OY~$P`GicJMGxIp6_!ppD^Z1dbp$ztD(tcFW3qWqpKBmV?8G}V&5cO!82KZ# zM!-IpC$b%NLj4CMiOQwy{i=e>una~j4XNc6T`W)>WxXfawzD?)t4-QUod49;IS#$a zBIX)cG)4@LOKap`YKyAYiP2-`CN90Z6FTa*2@@kfcTM#2Kas&LM5+Hky&zt33BXAD z02M+=8}rQMZqz6evdWZz+K%dq^jW4%v+h+<RHvL?zy!F=BOQ=$?k~Lf+9A6uy*<A; zCuM*CIOYEMq12A|Ra>tkcP9VesdRh}7rTO28ZUsRys8ZD$G0jd68`<<Hf00c?`9C` zj@(mhQLe*q3X4H^#48!yS<}C~>_Ls2ookk91wLk_VEJUlPmSfC!?J_c*VNeqI{VdM z0irLxzr{be{I=lA=$&6rYD~xemwFFo+XCg%DD{FVRKF>yrB)O#Diw|7plqpI)ZBXl zB?$cnTIz`Nu)2LlkK%b*a0&Qh!Bm-LWX@T6pWPVJJuMjBR<Jzp@~Tvaw!d=&)j*;J zIoDaT(6KQG70H>Z0yuLi@%+V9C!RpRhOkg=tK1>`8Y|l0VcKV7a?~29!-zW8*<af* zHtISyNV~o5i<D?1cz@=4*o~R|w(lWu8azbauB|i!Y&#CU3P(HPc9;6}NwtK7B4E0< z703cbVD2M|qiXSK#(Bg#(d^M;rxud{@{$sQo?j}CBks<USZ+O9UWipZ142+=8WwO2 zJ(<_|lp%~PGw8lSO5B!_fkoCGZ4ELxRQna<LXWA3)V$<OB=;i9!`zPm%BEClT+>o6 zYx=3NW8Y7W3YZd#UUAQjf9J1&3^e{r!c6Y9kt%6&5D}*$ONu!{Tn%I;94BQ%(RNJK zq+WzQ%~c-lWa6I(l<$vuLIsP2(aaQa+cTw&U1n99ubR+mUA0YcsH)ROp%@OCRM9;; ziG1&GiK`yW^-VkyHfKJlfPNri%ro>11(pO#X<3w6^nx2XX0I<3dA>rYhwuSul)Ro{ zzHYI?`66wtvwT62hllp?usLn_X!9pS@Jwnu{PNS`c;yPgbUM7J_aby3QAklQ<fN-V zQ1!_d!n|v*aq5xlGOVT64K|29^+gZE1&T2vEIdqtsP>Q7yZhX>+4-!)G6;w3!~BIi z0z<#8!pnm@oN3oC)9$w;|4`Md*1`GQhP183U|U8hB!ZtB78~YFHi;eYDCs!ma%aV% zq0NkU4M+Wrkhmc7?2lAOsfj1if~l7SvxNCd;tR~KeeiO<yx$OR(7a)iX{S9|JM+Qi zkf;9BsrRn<t&31~Rq|`Xkle%?5w^;)9?^xT;ge?ODNYOy_@M-HZ&MAdD>y6sNjt)S zt>|PLNQg08!3Ue&7)GvnkQ|Wye5ij}8L)e^Fvruf3-J|R`@F;8&A5iYaCnrO@*G*t zNksEB4S@h%^Pqg}HDJ_pm#VUqTjv$MsF74r<rYBCb*n>%@K?SirItxVcc_}c1i%g3 zMLv9B88Mdg^b{k#zT2Jj^r~j!wF_C%CA|R~pG3Lxo!ApC+<`c?&R_6l7r~u@9&U*z zoACXh2m5*gb&FsgA0R{skXax`<<to6#!?2d>47@b1`bZWDgaqL*<$!@%JD#ZdiYTY z;FMHp*Od+;&qdbd>Wmk>c$!a|RfXzK>n!RQo5aiv&7)BMy11*Yp?Um5z*e$na^vUz zf+<k4JX>`yh&UILY*X<VIRjF1cVNc{bV3iK7D<<*BwQn=6BhqOxx3BYEw^8X=;ow= zVnmn~Wz&LlHi%FsbsHIGWWLFT8A<uKU4$<_)RB8|27d+509c#iSk6nAEUAglNeCfZ z2WO_UWsr`~k%}&2<l`qyZ|vTaZ|=WW@RC9rBIBM8mj4<5tS5%l0)RwVLW<9dZGXPp zOF)0&qkbTfk9fS7xf-*NJvHyDOd*jhTu^h)Q2C(v>h%TNtH1EuIWy<-iXlM4V8*o* zV7t3E9HZ0BJpz(81FF4)h@g_1=~n~^0XbakK||^)S>vmueJP!VFuw90Oe%{Rw~k-` z9rf@Uau13q%?QaFOtk%Du=@#t;p$a7ZW@>K_wBz2GeqTfOLA|Q-hXuAx81JCW-Xe( z^7?MAq|O&*-L>+!-`~H!?J4}^{&@D-<`^j>F5>N&IsZ>vt1Gwf6wgA`no0d-x!C`r z>r11WI@h+b4ycGxQBWwvI-xS8RffuxI-(-R5d?(PQiTvP#{v=|WLpqW5vg*73J0Ph zL}Z8v0fmqVVW@HtBoH(R8K?t8U`HYBl4SS0Iq&<e@B8t-KgjA@c4c^;d*AbQUD*LW zdT*6y&`|)G@P=cj2kOd4{HFrTE6eC^W!*KhqOy{R*shNFhr@BVC%z0&<pUJwp7-5d z?@s!x>vHGxh_KaT_2uD2^aRlI{99&T`RG$-1hp#l*9nI`k+z2fl0s4EWXYXqL)f1B zTd%@HM)voVZpdjniby_^Xb8<dtM^cw$rGYPew~AcTj*EN8_CNBQA$ygjNC{`6PZ+T zmV^WWEYDZoOj^KP2D?V9eyXJ9<Z~5<*h7&%;@ZcF$N4=~FE-TG)O1IVB`4$exVb?i z8TD}4H^?acqkcJA%L0>5)EgNdkRw1fkhK8|(;=Ri=pa@JQZJg`RQ9@&d>0Kd<=njl zs|FWKYAUaxtDxsh9=F@su>WViBznX>s|LDBhvI!qE)m+sG54^LUeE@%pK3PP9g)Ta z@AN_Cjf_iD8*2%JZ&Fuh`c(@cT8h$2<**MM)xKVsO}khM95ZD-_1FKg^L#Og;871` z#6&sq^d0`w;IbQ_21L(gtLt>J_`6@&z49m4t(SyrXdE-aQYCpY&>e@^YeLe|@ncv* zDeHT5?4~l<h3@N31@ZE0PMqaK)i03c(lauhPqzc#%C2u|gwqH-7>yYOc<=l0-T|gZ zLtZ<zhbcccFksGi1K}0D85XV+V1tVK{SvV(vj0z)DcO5z-h-3D!@b&R-_l$1_R~dy zCx<N#_f?tueL4B!>$9__AulnW!yIp!=IO1%AUXT7+nKTwD>)*<(v+Mx(s7mPBs`8W z@oggO;C9{0Vca%nq{ZLW;+C%}{wm9d9;sTjK{_xEN;(dTo;&IK@7|Hnt<(`nr{|uB zOIpqHht8SeH}aFPQ4(sR+LKzQKW0b)-5TJfaka6pNNto+TJEry=vO|)9AZz$CT!rw z(iB1c@drfDG}cM|lt)9_>B~!oEV3(t4op8Fx76r&K$boiqRRd4XgoMVNTEgei9?9- zMDWa@m|puHQu&*@GoNB*uSzc74<|yv+>l(*KWq_e>33~fbm`2~9_l{HhdXMgk3F_E zi3z()m6r$F=WKa*C&bCl!0-J6mE$~UwKi-JI>Reerl+Y_^S?IS)9+Tc@nA(5|7Hkx z*ANmt?9MN)e8BRkLI*O*NXkU|CV@Zw{CZIDb@1!1E{Wg3VOZbDf6^DYJLaOR%p>Dd z)(rCJYa;gj$KYOP5iqT&du;fn!?l7qViUzQJaBVFPICWr`1E?77@2oc_DL4l@4Hbl zQ9{q{YfDS;>fB9R)Em2{b)n#MB~meo<WK1>jB(R!g9wr(gaedGEhJfq793P^jK=3V zQZ;uVd(bnKq9LV66H=z-V3Ny*rWa=@<9Vh&43){Cjp($e@G9fc33?za?b*2^zMzty zHD{|4GB4*s!Nc(B<dm`p@(@~FdG<y6!=aq~yco}{d)x@~qlN7tRj;mWj-p+pMJ<mt zukrhgczX&o4=UXDUK8t{!JM-NhS6ymZ&Lw3Q1FhKN#*R-xF&kJV7#WBld_fq$2=8b zdANvxobC&@YRY+^_RhJwj5p*m;HU`}-YKOvpMUsfy9!w%rk`IPCw)*u&OsI-icSt@ z$G9r|hUxwTD7wt_9Tw?lmoSqKR%(#cj9%6FWshK>uko}WF%*{tVWB(g6a0f~tN6>D zi|&L~jFlfayw6#%Vc#JBn5<C2CaG<!2-`#$73+xMK!^FaQ5ws+MDK67UIyqlU-EPI zmK;fAAks_*rq3>~<rpJG8tD;mF=4|UWf{^~hSuzp-hIt?%A`o6%MeO%ibI6lPvVqj z8a-QIT%zj)`}c=OOG`$>flf)NZENw`Tq%H78WgQRpb7mvc&_3(3&R6b6NR%PvZqNY zAFrv`cTVK?Q=0Jg-RCCM4p8@y^gGK#d`;d(^nRxNWexLv*w1Z%ulwmYmJQfGxPtM& zjj~oChD^MHithN^=o)dhNRdp1|5cGgT_Lo?;=Qppo&QeF>{j-7(h1*9ljA0=bs)7z zG-Vp^dyF24{Y}0D-@-@rR6_I>U{`dXTUnibq<iw8ospA~uD+5O>mO(9^XbzPxK<~A z|1TZ*!iqr-Xydt+l_!<6UyzqGV#<(ZeTM`|d!?jkik05c(qW*usCmoV2#dfUdYr#2 zd>1Tss-AsVWS(Im8}sN`A%wT`hA&v@*9-LR91}0o2$!rW|0S~FrL2qS3W2{?b708y z&kIxjH-ooZcSlq{aQ)8PW504k`Z~YEhE;F^)pC>}SEqx^`d}#bTFXYHM1Ifsulf}% z&caDxa4^OgO5!qFh*zCvoz0dW0f|TVvg`5Kem}(8eFy(_Zew*$!i}6GpLIv`wPUm+ zF9)Ox9G{c<FM$n>-2Me8MD_Ap4W&pZVUe)%QB8^(YJ!Xv^vBEuzDf1#)zWdVb;Ce= zSGoAs8JDHQHGbxZi=V&cwV<+h+N=xJT?f#<rmOnh{?n;Q!@ou7PAWjY7XAvGWnTtG zqj`^jmchmq3sMGCl>S$Nr2(J=dh=|ruX~)^Da{uKoVq-MOZv;RJ*s1;NDqbsO}qVI zvsUU_4P(0a71^Kf{a-5({kIVq&~eDWvX;-%5o{s#g@4|$y#%7gX%$b+Py<k1O~&>* zBWeP+!&tw3Gj$GOUap*Pcz_*a+7sqy5H5<|u5DC=6Mx2E5`X3?&WNN5a5*XDr3AfI zCkNeoSd%FZQ%$hJ;4atU=EmCfooFcQ0NIA9{mpCfTdhbv3sF+BeH?trcPcTV??#sK z)f<;Kkl%<g@J(0tA&M$wVy_ngfpCMx{%zE@(8yo~Ca`9*|2z`G>!XP6N{367;5WkL z^XZk$gcy^Cd+p_c`aO~=UF4bInF<34P)*wEHac6>xc);Hd@{&;i`e6#yhYv`8GBMg zZb9VNZPsWUD<5I0O_se)<JQB0uj<MpEaIvwN+MhVz_6jN+u8FVPEYI8^%VzzkWp=Z zz=xZ6{=E5nCUppEzj^IQgDE&tGB8qKg;^lw28Vy6L}8fSN?gGr6|(HnkBuGk>2ApN zE{Qm2Pt7S+ZpNx%oYZx$&nJc6hhYBuB~?}@%FE{*3K@{U3&^P08LNNK?v|tBBK?Xr zt=bgqsn?t#`9k)0`n4iWc|{p!q1ph$y+#fp#y5Uf??<<8yAsTu+D$#wIhpS0^OsYC z-y6!spQf}H7M8&VJlCP@&*wzQTF|||@h8!b`W0dLg35=?_<){hD;<*2SE4=Ed5%;S zx#NqEfU^^%zsBc+O4qbguglTxPPP75^v)&212(<UA1_q(On4qW+!ZtboIUAWBm~TJ zZhsrO@UZn6dP`^p%w3I9bP0Hy+?q^#<F$(Q4d$!!0MVZ)-4DZoElGm_|J3j66&x;O zS!-HTSur4KG>shmRXH-5C8byl1E&z@Av5XF(?szPujds%d=Qsayg2v%Fm=f8hq=fG zIePINWSH<f_A%xHKIT;k04X`zHjI3anbmQy+i|pvwey}Bl@G>VQNHH4#nbMm`gPsc z5K<?s<2=0#jt*u4ubt8lpR%Zp{1^JSQGoM|#yB!}8vHGdwHIEwNCTPAxI{JfI1V(( zD$yn31}1&5My%f2RQzYS4OwHHC~z3$fP6D_xFjAlr9UX-4yNy(;4D4aH$aots#CJV zO4p}kBp_w9ABlre)B%!yRliEKJP?1$K?8g@^fl!egtwI$b6{zXnlkZ+b2_ra0cM{Y zrR=ZSl4@D&RVVk2V}5-=@)N{Z!1R+Jk=n1V=_cI;H9=MFvn)@mgF)069PDy7ZUTOA zSCV21CqfO2=pXY&#&J^>{E9U!NN*DVfwxhs7bocsG3&&E-pxp#n`8%CRG^^F^QEgs z2+3@73@HEtQbd?kI>0P{g*%X%WDo>`R2NFheG6zmd6K=mqWkA9h$)^-@bQ;SEFpZZ z=yo>U=`RxY!~~7BQ;v0yJH~*>SGVJtdQmrN{7gJZeT}qb-%^cz9#idub&&jh8}ey3 z0e~{kP;>NFzk-}26ighgMaFK(mDhO2w&M0kC;#b0`;N~I&UuSRlo{#eW6_bv9X4Ez zG2N^hZh843!E4l)=Fps1+O?xAsOg2aqp1UPvc(tnD|rAL?eqdcIoSLW&7yg&?mi2e zFN2!cMw?|&NZDLdrev2<lzG2sJ^gn)6YxI|j2sXU)|a*BE^G7HpGh7j>Ve}1F}-6D ziB3}>3u=H0w^I9fCJjPso{K6CExbW%3ttcej3!*Qls6HV4d>fP1)Cz3Pw%1zSn|@k zNM-6*B}nP;G8eBl1}!Ws#-XX~+^iW<XX8W6q!1~hc!pHF<iXyIumt#j_Zo6N$V$4k z&k=#J5|t)J=^PN&N{w&J)w__g*;~5Y1X>;+6?tiU(a?*?Fvoh{NL*c`ETtsjJo!uC z*SB^J*&o!ruqwUz!fK1hxyxJJ+<tpG{Cvxk-;Q0{KF5FB1mfFsA{xn4zt%+{4LevD zb*I^XL}9a4VJSE{+~%v?7NCQtrbSayraQ!h<t!2whA7CFtE<hY+zgsZ&4pA|J)xl% zxiZT4G3N^<G5h%KR!6#&{L0sp!3<4yBcvqRK-nGy#v9ErgK3l~QSS<gl(&Sneb9uL z&R#4n6WHCSMwn}5yenf)k;_!$0X=tL+n?qf$@cAvJQrj4XEmL^a{aTcjc;ZfNFoKo z$&ps5C~rvGbsCE+{76cOW?sE+4c?w3eO6BQ?+q-fD5G0)8+h}m5pmT3h4b+oD<~dz zt?amnAQwG){bP@U)~fxjKgQU#JR3v4*Rt*CY=!Z2`LE`+e+q4@u}x^alGEseEbHJF z-6mi85lEHF%o;jJGHK`g<*h|XTZwmpgFb6PLR=Q(ACc0r_n~Iw$DQy7=<!H!mm<BH z8NP6>in?gTjh6~!y->!dX^W&)*2s=-$BA_ME~!&TVu*`C5)--h_~V=boo{!Q?vGQo z6~Y;k5})4-@)v~OUXhAj^4Ha}rJ`AbhGOJ&hRj<Vp4-Gwd_qLw<X_3=UJGBS-c0mJ z*P)H9L#Sh&qPkbIk6!N?<I**+iXm;_l{UYv`E1(#6~!6(w{a73CODXT`}Fe5T=!tH zXi$@E52qx;#m)bU+G`t=<je1!=<|7Tw?e9}QU|Ie#~;`9L>63|wsJTq?K{N35tI0N z!0Mm3oQ>{5{GWG|Ui;w=xbGb7@Pnb+S0&sb_Bxm$541J~M&7s=OXH46{fLVvgMVs~ z1Xd=7t0#Se90FU~j#bqNZ!pXFR_;M>4m(CFZ<D2aM9IM47>}Fw7;4Brj`t<umQz~6 zQ^2pbyDE9V3Ide8*%;7(Y<?|V6Tw;H%(#Nc=5dvI`&HwIJ4tM@GQqBKnx#&#uQ~HU zcTDt5GFm$7Tcb^$%$TD5*ASfzGx~;BOD*W(0isJiQjE>SG3$M}4byC2<jy^CCw0;; zU~DQ;=KsnYDvJ$uGRrk(?C73!u<)<klE69A6=R}y`Mj-k>|+Y04}>JP;tS(-RuNS6 z5E-oSmLa2x3Jn^MUbYj{^?<~QU@@F?cDg#VkeSqolK^-yv2ilZecw+4ORwS*EU?47 zXWTxyee4dG+8^QfA02V?yy#gt@wnj{((q6A>fSPuq7azRVw1}z%R&f?=;X(u8_ou* z__tx#o8f^-c|D+w<(1`m%67()u4Shs<A64!fY9{W=-~Yy27=l*cQn-+%C2{AFOAp~ z^*IP>cpyRpSMMdxXA{1ib4ffnC9ZfnyP=_b)~b=DKgOvNhTe({>Ftu40vc|W==D9j zhsu|76`}=LZZmPdGxhD%bNNCKV);<MTPAX(Q{Xd5N#QKzk6#J=nC3(m7Y;25eaLd_ z(!1VcUYO4tpIn>3iwOKzj<YbYpuBdL;r8p@%V*)I5j1{refdeGVUOqxWzk8xTPbeU zMg9pfrOPO(McoXjNa~}S5Nr0fA^aO@w0ivqx`~5-$YWUZv5(2yBO9=5Khhyaa!mOX z67d7t5JMl?JAD@NUMlE>%i8dTVVW9+zj~)`ZA89_#{U}Y8??MlVVe$8IIY1Ffr&%b zulKPUl|_>$L1mMwYX9jI&8;-nGWCWt$#i7E`l*lUo`$<9URdZ-8JSR)Zu}RdK>tZ8 zssEKy;{VAg5lAV|8}15X2QP{gFFW359L|PvEBjElAgF*zJS@y?a;qDlVs`rsC2U8G z;e#fq+k)?u3rIy+?5@|*reyJ#x*?Z1$D4&i7o<1e3~7Jct+(i%{^LK29m_#l5%A?x z3L>_yBU!kGNk5J?qnra(nwH$bs}nx;?hOc=gx#cT>_CPhhA?l(xpOV|9$*n4Pik)5 zQ+ir7$4XUWLGGsRK|Rq9_5%YaXz~`u|I*1Y9tqR0)Rd28XlCOJ3Ce6~j6d36lO&u) zuO1*q?tqCy!Z}?Kmdza)`IfdJ^pttY#Z%ljNI-DJ(!R3(=EC&6s!9)+&ilx{NMtjt zD|G0W9@S<-D&hYLsfuj^T*L`9{|+q1bfVPcv%;0koyVSNPV-V&&gj@wQ?{1<O}8Ml zDA?-`a5r8XX|~vBlMy<ng!}eHgLC`1to%V$4PFv%#QO}@cT!!9Its9vi7gm1?%f@e z=LgGk=y(I+S1nHL8jmrlD|6pb(=FKG?77F(^5w<+z5{T1NAeSPXx%s7^(lE<SeqAe z(Pk&kyRF{}{-BwRp8Kfg=~%cdi_Djgrba2qSIqN0P(reRIGFJjR@v-E7PtF6uT2&A zZ<g=r0=SfK+{6hi_W+`C$jtp!%Xk7B0@XEsy)NlzrP<l5i66;Sbq_p7zxBp+EJqq( zimJ%>TKnIHg4@T@Fx|F@9O-{m3Y8_pv7&`6lIMqlM-j1KPKFFy^hVXSN5#f^)R?_g zuY2NNVXId;kuj$*+{E_bNSqg^g!Mf}Quc5D8!<X28J#3A35K=tgZCou4Ndu<-yUCj z@&ql`H85|&@^s~kc!&JF$nyzNUz?gfSoTU&fN*)EF*-8g?M^KPD00s@Yc^?qH*oOz z`mHkqjRiCbQqgghB9_mmd0Dv0wTJ42_$$ks65ltKy3@UHo9=J*NRC}XcwqOQ)5k&l zdq8nx8U7|mn^ZjPw&B@*b{o0MhF3s$+pT_;TSBv}&PB7Edvh*uiy~Zp5nnv@@J+~0 z)1KSv($^<t@0ypRK3`Xq5Q<A)dU>!MQwKAik6)M1b&R>g+Q!f%DZQI0$(2}&$%^g| zou{y0m8PMMzl?l4^dvZ7!#&5WoH{Dv{nJ$S)eGo+%i(u>PyawbLvv{5XR=hPfUu|e z0UPo!=Dp9M75?<O&AvR~&n)jVz$jur>hs;YOloJC-tq;cl-_y^JFSakA}{QC*Qm$g zBD~Ae;%aG?VaM1@NmJn0#3%9=WLZN2y;|DUx|ml8fCSi<qoz_sK52U=>k$+0o+Imp z68rMiQ!`Sj4fmmiZEezGv+*@+t%8#TQ~K&mpawpOe!Q;6otW<1Q=8+g^Yq$j>Xn~f zJJ;n#hVJ$K>E-u~Uf&)Eh3JWyA%ARLj_Y9^q~&vFgQ-U%=j3lH@r73Cl=crp`OjDa zDruA(RS}8rm^&K>5OYlmpb-K<cm%mBTBMIbb0XaWCQRf{F#kR1tMoy4KwJ(CXSbh^ zMIHuylI_VWYM=X|xcHNT_{v!SHLrXmr&n(-)+&-tKu#S<AYfc&OY&VY=rhM^!gJP9 z=Lu~h4u{Kj#U9<CbMCec<WByYA?;6AyXw11<ylJ+&v)at%EI)?u{-f$#k88(Kz0GQ zKS+Lp`sSSvEj~1!wwXe|7#1_T)f3w5npWrfrY-9pzr4EcT>0IM@g&@&7!rt1ww%hZ z(J!}CwW`B(8;GgMa+vuUSe`1zl5pHsQ<Q`b24b-)>S88EoIgMNho3cbHK|Q;7Ya@z z%C_J$ttTEc``0}aRMb~gyGSJ=>|?BLaCFd=Qj`Os^T*54#4A84;EsNL4#*UzHljIm zRR1HIdW~{<V!>Fb@6nSP;vDNgYpZLVlka7PJGCwUGWOxvWsivVHAZZRfernjEr0yu z=ytdh2(TW$Dw7f*VX$h=--u{-r@w;jvoIAPGLQvGSolQ8Qo6M3U6or)9oP1%et(vw z@NTwc@_lCN$)NZvEoE#+a#9ZHFm_<y108Xy2CxFwIwHGOIN$KQ2US@Qh2v<z>GR59 zyN3KNw<HZ(;tKtDs&N8qD|)8Bq>q_DhDVYbWmBnU$`5%eWMy>1fp+e&?DU5k+Tu#{ zvQrP<(y4z~{BKTv@*<q~Df;vnjIHO~!=SA?EiHRQ!&ew;aK~^Pi;C_TtF{rc=_h}p zu_%nU{A76TT>ihib`}h6#d4eS*Jzf@F8nJ?`^!kJmR}-pc!)$~OF9hadR5S}ftLN8 zm)LE1Eiqgw!{>m!LrAKMl15%8w8yfcd~dS06>qfd^s=D?B}16FW1g+J-JNRoN4|Z6 z@m?&Zus;G{Q&U|b`3T&TL6k1DL$4>)<dZ!j#zP~X<{R+$&#|Ns0K~3{!WZs)h-C!! zOd^IIecC?_4<}6G)%3%H9>(fUmaT@<;$@CjUp4KSkaU=5DtU{-W!|Yyrq0%9ClXBd zOQO@=tRlTyWCJAa0PoApzyLO48cGoKa7P-s=A(L>rdJ!1q2#m>E{m$Xn>&C}%`}C_ z!=}U>Q|Qev7?E4(=_;ZXGgpaxD7(u{JrpZI=8stN+w<?J99%vOQhFyAM5pgLF;O29 z?7tJp;nNwHWsq|s*AP=cjDxEI*7quc4d#8KbJAdPsIDuf%)(E?H+YRxdYtl(G}7bM z?uRRCui53g_ia-?tA2W8+uf{fOHUv$Mn&Ua)t3{2dDjMgAw^=2Dyx8}h!9LA<n0*O zEsc5R5zFAdC@s(Ym~bhAcWU_s--l`EA-5>3_`UI-F|6!>--EN9dq67mVA)k*?~7m| zdmEb0q81I?kg-fI2wQQRtSMqIo32%p^y27@dEu7VA^>2)H?PR>eqEZ}d^<y1bdTmz zR!qx54*1t5*GzDsV%-8~NC1=#^x8H%mgqe6G{Zk*81w1*9@zG~BO=pDd9siswfC?# zFI09c9agK>Yc6D}41Pn=l~F|@jZV7$#bZ{q!bFuxon3BL(3NjsThC$mZ=+xa<{T9Q z3h7{22MpQgoYi5+rDHJiUB-&(L`rRYCQaJ+)2#_Ru-?Hu(yrHK>jgPs)m>EyTV#C) zyQ;7N2Pd5g)4}1Jl!1+dRMlGif12Oh8O*_3+#o+jpb4kQg?TIGSLzf0bbV+MbFf9Y zAgvi>TG*BM176VX_c60oq_jz%HQjA7^4k|*d)bXVWuH1O(p+-lj>Ed1`%i+tgr2cA z|1@hlVP?ex^I%Kj#-6DV8F}!7NaY)LT(h|Dk^W~aJN6Mh?!&#<l8EWs@AAKs&hDoU z`$PN>Ir;r;R<GH<wLhM|F*jiq{O$+6{Eo#)1BI-}cgs%Y=lO5%{&{BA@C;&Y<72P_ z0~)99(^#}H=FRZt2lWT|&QWvewe!Hy$2Di}jRf%HVWET(n*VZ4wTcXLOggN{f@pU# z^wb<QxreX?r_ho?i$=;4=6HyPk|;<h*6&Ao;&%UIUHZcbjzR}#F!1R#`H)+AhSxpE z%!g|zv7w&r|3tFgV$}Wr?EE+03SYAYyzVhLzX$R~(A<;1%^h@AlOoYpB>|?Iuoo*N z>Z7I3YJ!T8B3uhD1k^EG0tx98q&YM%8+3BIeK~H|;}oCZHXbC*H?cO`@0M~eZRFG0 z=sJ`zQ3xQ8*?%2|k7%CkkJ<xI?e3v!BxetBKKTjP8cMwuhlhMqErH!Jko(;Q{+?TA zY899w4KL_%;;Hkjyn95I<5}?(wWqDG=rg+~lx|ObPp6K8C!7mrmUvFT3tkJ51}{BD z=gC^5@k>+kuCbSErc#7X*jKGhwD;j5-)k&>ouHW04l_*-OAlzcND5iG=85L`@X)|F zPZK}4be!Nz?)I#$>KYr533kWk{cmmPO86O;F@_6+5Tp|CU?GEnJzVrvMhW{5OZ=wt zE#K-zv;!$vv!0{g%4MbnN~*H&l-lzn<0&XFc{HZ%|CT>-5&tQF3WkOlhA;kKppiV& zPqre56S4IcjetaIl2?t@E17J2T#~;U3pH&Oljh3<u}ojk!_G*I)a;P|&`lE1Peo5( zs42swjH@#KWJ`8Ykd?#i{asbmt;YmCF`h>cz9w^XGPir4NAQ_Uc8r8=>QHCspfCAC zJ_k9?NVDu{Fd?Ozlb3p297KrBg&(GNVc8xWf-_|yEyp!wLbSXuxHQK*m1Naa<67I! zPcL<q{Y!hn?+b9bj6|J2ulGU3YLTLxGjTIRCWrAVDUJM>o2Z0?d3Gpjnj?Q<yvZ1p zrYjK2k_eHpVD5cZ0BW2rK)ABBdXvU{2Tc)Qk>K4JN4c^t(fesv#ubMuhvU(0BW=v+ zzT7tI=$`_&%i8Kngkvy5;J)=%uXQy2c|$!}kqw?pxD9{pCd^e)uKBUofi#_GX!8XZ z(y5Cx8q#rpbXM_}VmHWDzKruKB=6vh(1Z*4OFO&B9hyM&?e6o=;x=6rwZlShIfw?S zYMyssYd<!S7VAUM4I;pi*=*L-Gy(Ckn|Z2=zMVTf#WvP-TuI?6iA#Ko>T=sm65{uV zPuk6Q-WW;BZ=uI$^Y@-xeUW@mW(0B~h~gd@4WVG0JFb%>NwPf@!~}mMTIpqi`8UGj z<Pi&f=tlWs*ac|~L2WOApt;-#;~Ybae#>%OpLK6*`$J&pJU=kvR%Yn=a1T;93k$P% z%fH)gG5y6Kiw1wapKZeOgXLQ$yGKO8w=m6`hy;%x*>0?ZngedOS0d*Mri80B+1DpU z%b5O6Zjne6#W}0^K7HDzAPh#cMR`#%r(+g1d2h?wRB(OvBne*tkznos*mN-#r5!b7 zpJj#We$<CxZxwPPiw43KycXhOp)*qytoiusByW-4M|0z<LLCy;MT(jKbCq<WF8Da# z!T7}ET7JD`(*4AOTl8%V`!S@~QPgA$EWH6UXWR-)$Qh%tijJg#IUqqrST=-Ixi*q) zB4lBy>N)BHjP#tm1nAl`R$^TL+MK=i(Paejw0T}ke8R(F3m56jMw7jN-kSdWN``2) zK%4{Pam65^j}AjJGw@=KSf(ahdtN%~VF&8k?C()>qYq)_y8zD*#fU;}r<xrht!=%c z--O>Zt<Ok1TXuF);D(Y|(}}oay(A<u!r3m~s*c*fAOD9)F)T`yuMoN-4XbfmNDqNU zgmA@@dkMGII{~{`y`9)4=}?aoCw0Ii^CYJ^f1}$k-`Cem^q#cX6?kcFv_-aG+)u#~ zzx1VhwP-)Qo2^HnL$B1zQGd$x-c~hC+)}ffIr=<mU#s3S7*5{Z0McgUS@>JxLQw?c zS|GPYsq+j1;@X-^?A6TA@%>>cA71kUuXROyC1(lqe!P6^NmP3$N)hDVK`4Cxtb0QI zD8g^;(d_*hJ{1zA{VP%DPJGc_jHqGkLcP_0A)1;1lyD`QlT2PwB~N0{1(<UBo}RN+ zR4bq}oRC`-YKl0@XO8Qy4IWJur>pwsNe=f!Dk9f-?jK9oM3NtsI0yNxCVVgkqHGI5 zw=NvgqykkodqYNF{9v|tr65dY(tf5-`LU7mHI~#sG-X+wc6l?(a#hF|1w<?>LUIde ze&@H-W(S{?N7Rgeos-2cj)-f1ue}#FO@XU}93&Bh6n$f0z}`E`hds|$^>K~+ASlXZ zh?YiEl0sZjN{ifVB*f!&WEIsSq%mDZGWAYCtXqB^G+p*VW>U5`pdxSkSAz$;)qHCk z817J>;XlLd>}#w5unovzO-BPYJ;b;dx6&U24J?z6Ma({+=dIM)RNSMR@Alcfxny5t zY*;K~oh3+>5A|Ba?RoYf_|1r$^CW&73A;!hp3+-MW~{h{gxQHkgJaRw=lQEOx3e3O z>oyiZ??5`#<uH3_z!*EPbl73hD+pHk?ukul^XXEbu0<*<YUp>m5`6rGl8FRItA0QH z)vJFHw3PqEMh*m(A`xR;=C7S=xFj$w!`lYnM3NbU$PMJ{=#He?j;7!(+axXdOHhH( zSi?;xjeE>}<Q=s<(D({(%)^%Upt@RGlYPPMv|2Z`M|{+!Hl6oZ@BinTk^kqL1#-+I z7ko408VB{wNM_q%Q3^;OIsn73+dBjVGj`N)^F%#n`<#UKs)(2W)Lh`v%yP#^?Hpp8 z0zJ!%5qf0Q;!MVc7>C1UPIdIUH{`bROJqqXZu%G8VkQ2DJhMx13liQ#<m-ryK8UM_ zc55!=augNyD!vMnsOGqp7pO?9%F|JLGiSv_JyDg<3oCg;#qPiC2zgx~2^Cgz_V@N} ze|oFY_tb!#lHY;d(1htNLO~`To}?Ab?B;A}+)`zsyPs*iuQ+p-yaTOYETa^oHp&i6 zOADfkeT#q<#Ld1s&NVgKWEJ8V*z4U{mFp9{b+1HK^Z0RfqI6(m_Ou92{>)5Y-s}i` z_l@9LkE)db@D93&&>3Ip1JS8bmGj|Kpz{%SBv+fD5sCjGinr2TtCys29&E`;uSvbD znO8K;PV)TRe=#su%$2uA;)s_$%Dv`ctl})gtXgnuDToaWQs&X8Ta!J4mE>hZv9-2W zpf*GZ5w4bd>Xm9_clW*O?&x&J?w+$_6aD@N0KlYeoR-B0J^U%(cDE7!5)M=b`)05c z?XQAK?ph^zl&6`}W@zMU0-CA)4V`MXwrz@Iw3NmAX@n(;G9Qqt5MAY1fHy@_E$fl@ z`dGG=)R9jzhRCx`WpakzZybV-@na2z1s&xyn-x+P`Mn7J2Sabp#TQZ(3~Vz|ZGxI8 zMC$E8rV8U~o#*g08JbHlc;jA~%$U8stRYs(AYhMXuu@y|Tc&+DOR%glU2smHI;zb& zFN;H`)43<!7C&<(Iv{YeTmUyYux4_#7kX&TDR@ROdnZD|83xcynugSp@%3BTBW9R# zEL$^M%L2e~+w+m~;odFVBb=$`SZ`C#g-qo*;cdNCRI;7Hqx;4ETuVr19WMTalV{2p zmN_t?R^lcEoj<cuW$`P}m{J$9A~cT48rUSDT#0f2!HH_a+~%=DNnQMo9%9pd_hRXI zXoYkledmGhvN>%JY1xNL<B|(&WDeJ#+gPBjxQP;cb1ZgRW6~gBVx}Df`SxE}->6L8 zgBkPmdo_67AaNG+zTGP6zABPAGxUgn?isqX`yPRw5b<FXt9Vg$jd|~3@p+;Sv0*uA ze5Y=(SAn2>HFs`ddEp@8E9TB-w{(8uzq4GX23DgurzE)H=WMf+@76!?xA?{TT;)5; zmd1zcbNm;naRO(Na20Di)}b+OkT3LFj_nN5Of?}_Y*tGI4vLO>*qO#)pJ2`6`bV#- zwQd(UwE64*yxGU9+#6b5RkN=g8T*hZT~L%^c!!&OisL2Yi}kjswC;?I{Y)uO$)D57 z&o#caD!=V`7t5jWM1)Jab5&)~erfN8FED;>w_kjy>d1>#eyeS=p)Q*B-+#vFM>uu{ z(g2T6B;(Dv%6qf{<c`~@zz2d7#rfMXq(pxT^j2`I5Jb+5RULd*O2aqy=)d7Ie_^bU z4J=T5d_$wn$+?{4#bJa4+W8-j&h%WY)aj``lFq{tYV<24_^t1BK@dCNYr+5n=0EWo zKC*h|rq64*+8+#e)q({o6JBC1(ij=5^x|r4q@5=*`aUe}{9tfYvrC9s&)D5t`<#$G z%F)hs2|j!A*C|r@iEEN089w=+6YP#$lVFGL60}KRm1g)G>Ppc$D1+DYV8_cx&#HUP z2WdWsCxE2nn2*HS<V2?-OHZ=C#kepz@$-WAq4Z+e-CowtBIf>%6B-NcFK>sA(6ehk z55=9y{!u%fAQ~xSLmAjmgHY!R?O0G4CpzPQAd7`gOfxEk;HDWuev^#(%%V;jzWOm_ zg-wif&QgyA#Yma|0mt4zUnx&XTKcKBj^_5S3pp*n*R-83d2=G-+mmm_x;wBPaf7Em z4JN9EH?Cq)3=h~_v5o_VDz+2ro30oFkuD8%6+`fJo>5Eovcz0>ca9TxbU9HMG0(Lf z&K;F%WfyWIwIw4Xb%xM@Zp-l#(n$wzKcS$WW4KnL-#|V~fILVP#*smkZ}{6NJs<Km zG2<$mfZbFvSvz56N@fzg|3!@F{>ZSsppbWq?&-a^*U8uWpmf5T;q$Ei^<=-<p~q6h zDQ%=5$c={&5&Gqz)}*&^#}}p%^vk(gXOz@Lk@Z4$$VjDHlf$W?Hr|_o+vv|}LY@tF zEIf&M4^o?8h-K{G8(m`8zhl(|)sp#>=c$>F4wiKASz=J|kw$mZBi2FUz^uA^5u%Y5 zR22`BioF1Eqz-~aS-jR~ZkNyuvSb@K0d`U6k9UYH1s9y$4?EN(;mh^wFfquK0bhmu zD(T}b6I0?3=vWf>Ku1hM)a(f07`mw}v+>;7lrwhq!7x#stvUXc6@uLUsJF1iO<+A~ zt_w&Za>;yhBO{s2r$W_m30WkZ#oF78n_z&dpCh1@F$ue#cIvGjzS{x4xOEp?KX>r3 z){~$FERe>O49Um07Y$`iUr6T;$s)Pa_j+dt?iw1&Ge+|@+-)6OmQ6I%Ll!zOkmZlP zP<0?_98(5A?T4J$8(vnx?({5M3{y&J!R(IEM?<a|krzYV`%Zeu&hSn>{8U@&5-7|| z2t7B8gVOQEv)~PDS=Jzr#JJk7I|nGcdlrfM?O~<;EH_ZZhV)Xd<ffnfNxlLY1ofwt zY3)1KcUMIQz6?;?#`~0*22n1q?M5biDL&G(rOsB=Q7*n%3h$Z6ej}=&CLy66mpM;b zn?3y%px{^o4Mc{G{s+{$c`RMCk&xm_T&#S}+IkneZ_=>gs<MASVigiX*Szi9z%8!6 z!wqoZ+GI`NP6udFE6WOV1%moypfh14B6IYnJpDebL}N>>LR*nUmYFh*QcbrUtGEbT z1!94p^n%}o*C!*~gcZdn13pcgXM`bTbbqa-Z?bxnI6&0fyg<EGubwF>Z|nwB(iNm7 z5%L8>3z}%9eu2iK)h*`Hz$c-J#yew7Xx5=<cW+`a)@56s3|Rw9Z2l*!C48UU=&sY< zk*B(H3^5Lf_kpT9Bi#3YPV0xnP|y$IeSn`}Ft804V$yXO1)t-eQgN<X8mD_ip}B`O z$^6W>5$tazUPRxb0GiEOjsNs6>6Fl3SS46G5?A=Z<#0r1U!kG7xZAeA_8D*)#Qkk_ zl7Kmf%_MI~A`z4>l5;4{qOX`qN^RL7=PI%c-^2{sr*Xb3ne;shp)37dbUbeXlcU@A zZs6?+Eaj!lcc+i<9uuFEG8doVJ?>GV@nIec;%fk)|7XfhY~bjvXWA-M!!zV(kt|dj zYxpPZGmod|ug7L75_T#=GIiTAPBYa6VT|j))2wX<i9w4D+>Z4<C?waYMuNn@<j+U( zn~Iq3=<&t0Lqm@v+_ZJ^#aNc_FTTV^JEi<jI!=s)kAN!n!F4v+4R(<g@VC*69)l^E z{3vyf(7q3UORXfRiME$myX0R@X1vXW<D66GwQ*;dmb0K$6DCGhAYLxm_+iw%5IOrf z&!XulliVEWe<j+-^Njl}Re}p)iLa8|BT_EY1bz}Vsf`dAQ!I@K{gVpaO!@pB`R5K< z_Fyi?M$*V;9^a@&kgp?bR(4rz2g>uH=fvf3LoZl+oWhH8+75IJd`>Ice39VFt|P5G zQiHqs>pPv0bc@}(c{f9+lc_g|D5JLl*q$m|^-IUK8!#V@2qXlsP>t($G_1)qjEdT( zodx(g=&a}{)_NLtTQghyEi-9v7`>k059FBFqCKH4CY~<6kfZ%I>f(lUm8q+bbTr1) zrC00q9|`pt8TEyDB%&B1^bKl%GhBj%N|BepQsX}=QoYAw2glRpHXvuY^(dAax=4=x z&fMCVQzIU~>b0#WhGI0lDQdZ!#kKN}sC8DFd7T6FitZE+$K|#BR&6O8sugk!>-|5& ztr(}zgjNk5Ul6Y#EMYmJwpTnN3t7ty4?Q+%!c>K=sbzEZLK7^KhM*<DAm&W4jL14H z>f>zh;*VP}*`{Z-FJ(hcbgLIJ^~VA}!SL`Q(58L`rGSos*>Mf)Lbb3jC~i}A;0JkQ zB-|8$_KA-n?pE3A%}nrxvmu5iYVx=((7Jy-mBI>Eb)<xRk0s_5aaBC?dsx7r{AyMX zp-FuX>GOLc)EvCSr3+VGd-d}3HYfQK@B7q~G)9?^Y0*tXE`FMiUnZjT)X@gb^cQ>q z0c`F-JtY}mS6yi+@vs=Any_C@1>O$`2OBhZjwdD}W~>D<08JOj>fhhmSJ*medQL1( zYH}^=5Pu%NkISr&cy~E79$!K><vYRrHgOzj2+>DEIkKGD@)2J+OSg|1hb#v*Ud<dC zA&I?aBxE_7m9~MG;Heu5Sp_pa&+1I2VO|5*rS@P(QJ?rU_eAsD!m54C_ZLioWH{u} ziEUt;yKzUP;V7HHZWF=&ERAx7SY_Ktk-2Wixc9N_eb{T>@PMlbL7@dKCMS{2yd1IE zCOIL-r|-{N^aJ0kMDfRj>A5w%IW9Fy^-q(|rkf*!4x%5*?jv94I{~Sj@M}!eOeE+N zlx0y3`QM`lMJ1SjGrm;gawoQcnI`sH-$~+jP1c*7>|&@Ar+j;&p=+sniMI@|EO#hB zb!*e%;Jv(0NH%V|1LA9til{da(=ul$r@#?45f`(KK2fbaU<J6P`obk9Ly2b$a^8*% z8jC6OgmsTjdbqrJ$mF&F`|U)e@viFlJjStOJ(EG6Ucpx4{59RtcL|`72TGI<wjigf z1aV=j!QN3d>aG13mZtO4Z!+X>KzWG*WRr3{QMjf@5BIu&0Qt`){o27Y|EB!!>neot zoTZ@QytC<-A+5w`of|u^@PsS~i1$h*5v~@%LAV%E)KgLaj8rcVG~<TKAb|Z{WEJ@` z@WImM=vAPzODboIp5yC+-G<$-I#E|(VaIB3iGw;PH@+^5F)>O0DhRnqZM(H;*Du^I z5gKjWK)oghyg`MCZ*jvTex{rbZIFNMOqhd>0KRY1YP%tMP;Np)7O@s6t^JRnICpkC z?U!lFl?hL8pPs6zEYG6{?l?RUJpmAt(@?o+zR;TGrja+xtub$9s+hQ7MCgts5B4Jm z(3&KcwKC5ONjMs!AzpD#`+anx-|oQUYX7YG+}x`xyubQBlO^%^N9($4v+!~9Tmik9 zwGYNbxvCA7KsZ=}>6DyHdfKD6Ls!q+<kE|QA5`Y|EFqHH8go82I_ukDs?Uo1C;Lyn zdTdX3Zt9wMvwn*J(Z-<zHHwZ))Y$@}if-Jc_f{elNSRGR@XB<{zR0n45`Mi&L!4vw zqc<Mf%#(o!w6WUCzA9;D2jiRT0<(Qqb~TR=8H*A0C_zSQMV^pH=jwv+F8Lyd2|Lae zWG+4*leBu7rZq{o4hJsjohX@Y;J6fW`}*I!h@V^4Ro3w~{om((*X_-``r{TTApb;) z5v1WfqTpE;6+DcHLK=1<5hVdJR(&NykyY|*_!^1R$VJDGx0DLWM4#2~_Eox#IXkV( z(*O0XO!h-X17rF5Jy*!mwa`rfQYum_NZQ(}ThR<9DcPCywSJ#wd~|%!0D^#o*TPpZ zjHUFbp@B%@?V|PF+RpZb=*P8zeaNaTpF7n-&!152&x^3%BLdGA6hyeCWR%jTY7g=r z4D{1#i?WLYDWMOCYSXWO4wTcTlheD;pL{#;_Fc0>eNS1_;SPq!FFR+56)q9R0he<O zp8sdKoLH5uemzJ9G4)JLH%OG3C|j?4nPOtqX+CgkwNsC3Vy}Bxs}PFO0Q17DcBn?} z+FnO-huqrBMdxjxWwm}1K28=a+Zo2Jq`1BZ;c}p-y$9ZJ*Hid}+w3>O*|3=%z&e7N zpo0o>>v$ScDS`l-%9$-VTFx|Se9ZJw@R~AKU=}`_d6l%VhrUten7n!C{_G|j2aP#5 z_PldTA@b1W&V!u=nEyU>DzyrM{r@Cq9D33k01V`|aIB+|ub7Eb3h+_9OT;BCi$U_0 zwD2`3lnCQia%}%#BW{^_I8WtMA9bAnns3)6iPpr%MxH*)VEk0g&stEZ{~5llT)zb4 zqa=J`gD#$iSa{4;5oh(Y_8N+DPejka*fDL#Z*;V97O)*z!JvELWdoC5%iDr%%L{zS zewKvSyyQek#<-DoCo80SyRz0Ij0=L6=|z{~Nuw{|M0D^O)ht_}R9nXSL1?JLomoMW zDuU&Z@p0fn$!-}~4MPQz70vQr+&w*dvTA%$NW`}$eI{zx!$|E7t2(=Sa^PzQ{-T_T z9P8ruyB_Q=LrM|M9ccB^bm4eQzJno46efQgro!t{yPa4joQE0fxF$Otw+^8)5tU!b zLzw&7ia{RHtl6(7kxp2+&a54YEuv|qUBiJ%yQb6od+G}dtB4=Rbq;!n&AMIqYqGJI z38G&L!P#p)>j=o)I5})>%wL4I->vP5*{62gQ&xPka^E_0=$G6NuVokVn5n@%j-}C* z#Z2Ttk23%#B~=M7)6;St1+J}62-haUZtASqo#tM1G;_xr@@6aL*V!xS@}>6bxxJ~; zB%5K}9@5~{y{MFbe$FkQ2Ls6Pw1<Uj+<|N~NEU(oEU2k?_O!P0S>h7H`!D}nf~RD{ z9@GNePa?f#uZn_gIyT*!R4$?e$-IU|dZMXsO_BL5;C(PWgv}%;Z_VbZA6&)0&ouot zDg0tqNb-I727!oGUr_%5GXnXzlb$zkP+wnSFxT7a%-KCr5Ib|dNQS)o*owg^OkMVN zyC!WPwx<@_6OGTiy@=a&5zL^$wutJ1m0wzlH*iUf364|E)xzcGke_6Sw0<4unV(F) z;uu+V=Mwpb=zr`#Xl)^}zX|af@~=pq3?!?k)Xq9*yqmL1uv;_IIIs*mP1EoiL%zlC zBp{&PYh%-!+blFkcf$jARv!?{410>6D>zkLbYEsuF!z%rChePx_utV<is?5aI3Ine z?|SvC?4Zm1gx$&Ei`1qhXk!^SZ1Y<9mbJPX%RCQ?NLwGWd@_;N{6!j;(6p=kBUA1N z(|PBgWx@)ZJ1gD=T9*ynG*mcWul<l_jTujq4$rJ@B69Qsz-zhMaYHt8mUEf>gge@% z>1!5MP65tAK}qp)j+pLkOh0-A<=$XHoni$&G~3I6KGwKiEgfgSj#8GH_@coAnM>;F z70D$Lh+EM;qy(XL%)23@e&*5OX%fNHRQI+&D445XepJi5qdxb|v%TfGBbGBJ^-J7+ zI~2I=>u#Ck#%?yJUmRqfxnA|l+O^-3Ye5f+1OZg$uU}4pcH`^>g)f_!IPk6Ej$kP& z63dtQbyAe^@+)jpV4lrb%1mwOl0Qd2r@Mz#bqbE&AJ$6UazF{;gzv?u2MI!83#JnW zZ*_GI<HNF+7MV(`)vBy~+}-#7{Is^N_UX-2HPzKM$sqw_wzs=Bp1NwY@8_6ZerKR$ zuc7FdWovV>bRA&p-DcQsWl^fpxP<^XuSf5To4lNmMMGUQ%k=}#=#P+y+7+(~!V2r# zWp7-Hoc`H;P96E)XC>oY`B6&slL~X~KiEmV@1Y>@RuY;zE{V|noHt2I+ZNKi5h6jj zp6ROKC-bi9EmSM-*YvX7FBa2tKGZ~>Dh=H2BtO-ieXx79P#`H^MXDP5Gv0d7fD|A2 zAR~6i8Ew3O?LU8+`#sMh`UP<0ACQJ9J#^gX{DRgrntJsq)E`@k*Ev?_Vj!Dbz;qd> z<~FdGJwcJnwh{-yQ64CTr0&ryLPK`OmeKrj4rbB{`YG|wO_{#y`DKDk!iSIe+ACNd zY-WDKz{;;$s|a|MQ2hB32W64<StQK1#*j&o&HmfS?+7P__>udu4?74gw7+M%TW4@N zhd@>okt@c<R9S~ol*$*)r{Ukal_#qx^G9CtVwK3(Sl}mGzI~^9eDGN635h-5`F!EX zlGcS0=A=a(42ElV=fE5Vf)CR5j>wN0e|d4<)Znk$wD5fQG$@AuHj0igvis7DFH8lS zGr{8Fmp2$PyjHWJWwJ!DwTvkx5&DN9_huwHZCRb$OkKh{s(t8TcV9ky<5u&%@XB|x zc9)8>eRE$_JWf1QQ&m?NA0HsvQS#R>;0#5g_ls-)HZuGRL7vME8#T^G?2Sm%d7_zU z$eIYLre>{RJCNZ<y<s0W*aH9zlqPJTZcS{(OjNp>Mmu>TaV2|R1l!un9rYE~k*>3B zT~j=wHVB0aKnOrJ6rKJR{Vgx3ar%7`uP`KB>mL~TK4rqy#s5O~79jWwM&ih5D5B3G zBIpsaZUy-?$KbAK-6c${-~8auN14ON%=-G?uy_1(GjI3oPe%B$Dj*`sg>!fS-#u6F z4rftmD5@HNOHtO0zLOJ$Ys2$bbV0=NgU%wpO@PsrA`@;}RX;fpJ(H>It0OJMsz;h? zrs5p_os(6wRW>BG41X#qRgWPg2Dxg@6a5Y7N$QD92e&z^myuT>4W1s~g()vOCpdfT zi-i~~BOLGMj|9)<oeKWx<1PuGn4IIsK}s69xmTn6><mYJpVZc$__0=03Df-yO&`7( zZ1SH{{Y8M!hUz^3HX6Q<wj(aEH~$9^I0MAFrh)k8C~y>^d!n>>C(BGlPIL2Dd){Zc zbd$pfa}HEuz5@R!5XN-Skj0=4|1WLM^?^l_GL{)EN0!tL$&x2r%?A%uKY3c8^?k?Z zFWC+sLBahnoX}r?8{Li|E0X2fjd%-ry0I2$aNu*Bfog~ZOgar6*hcQCyGE=PY6zEN z<VhCDlUKPab&xg{JziDgwY37qtxD>G2rx3@G1QO0m{S<ctd&kR$ac5CyTIKi;cuMW zdqy%K`B<I9_)rJ$%OYOAtMR{j4@Jm&Gr2g2o2Z0*%Hpy!c{z*KU4@uMc&*Gh{M7d= zT~pwmJ1BrUqw9Ye-yh_Y9&}JLneNxpV)$5H)y=zm&Aph(!CVUPL$~3FK+t(|R0C!o zV5f3GOq^!n52)xKIp*dH3|B`fvOZr!hHw)bI8-`tPwXmO60&3%ndc0Rps^_xHFc&L zY0)^aUL#NU*rs|$@>s95;+as0i2~Q+nW0EQ5~%Hd=;B*%n2z+qKkm*rtU?r}95hA< z73Unx@Yo|#w@$y#aQi+sUdmc`P3(ges-TIsONh}=aj}jglF^tysz@ep-v9}_gnq$1 zXR{-h+a6!?PMBvF@+z`y&C?U>ZzDG;n6n5!TBai)&vXa=HX2#YV)NE!=eq;L^z>ou z6f`3Q{eTq_SQqI74T%%f`79@qW?o*{AY(b^I(UH#@H%F^xYz7KU`j;+lW_>6-3_G2 z#<3i<H;dXzPgp%vG4JQT`mO-kMMVQdm}8ijf~}hx{erGl-ME8_{jh$RYu(NIJf-0L zYTM04si}^FP0C!-Tso5fy~g#rKERM&b~1dygK(h-+FpJ8LEtA@R!PZ7PEXYilLQh) z8e>J&eB_Pg;sjVe$OP{(Ge3O0$#9oEIoFV}ads%aHVrEPKRN=mgX>bSAb-8;oh0bK zK^hpi5nDn=_jf>n-*6~KG>_T(oV^^gE@YNauyGXmAd}<EOoPOnr2-T9X~#6iDSE3% zb1G7sb8H?89nkl^DT3|xmZ#!P&#ae@M(24`dhE-{EyJQ(2DZLc{|KVN+@oM7Jk2;P ztjb5*aWI!3X1%5&4I1iFbFC&CbUKm5_mZq(1?{t({W|b{{b<&XF!Ng>+ZpIB<imW& ze1b#yH;ErqD?d2i?AW~d4|xq0-9^B@{{lwlH-25TgdE1O#5<nAB(F{>pBKW~hb9P< z2CaBk@|S7+6RUjsiTgBL2S#A(h?YrHc{I2D`Uk(CT`wtjh_SNjaFI=bbjlp$PaU;q zO1}!4LYo*GlHm@pl2Q!FK`%n5@pK*}E=HGUHyGUzK=;(m_o|^8^i#dDc*4uP-N53# z+h~Q}irodtTU=SHn3Dkw^0_E*4NCBhEIX8OjUm{oWL&ZF!0H~c$Qn|b+3ro;;kfqR zq*1mFGv9OB%aIc`q68aR!cTN<*WByVx9d~xIkYF&Od*+6#Z72}?dLUh=r($7{oANJ z;|TcRnF{R!+?c|Wtd7*3g0|T2LKplGbJ5&rat7N40Kb%Z(CH>O8N5}#uQ(c?)`8Vs zpawAI3%)-=L0urQS=q}s>^HSKEFI;Qe|+u8I7=F<(supS&9fJdz`Mf&PDASevI_ol zpFRvFj1DO{Z?)76k%cqlop1Htn(LN-8(lvqv}RHog^PL}Cxc1i4XuGysQg+vzd!bP zceG~!w<3*F!<Uqn&B?Y9Cj5H|`7aUDV2ixpBtm`3Ix+y+<UlxSr)+1xgOB@@_&M8? zfVW#HI`AT*dHC#!J%|`5T*Fd&r9O|426z3c#lQDn5FgGGYp?Y>lI~Y*U0rwU<-nkF zeVJg2#`xe|we?+}*uG&|fN~GWL&O^%ixhC5M&vWF%NBcytl&<Qk~~g9|AQ6XrC-lw z&Bdb58|&9j*Ie~lgH7$!Y^YF<FUH!Ag^%Au^HU4ON`$y9gqcgdAwT-_<JW~5yDED4 z4m*9GKwP$}#fL<SN7T;`dHQ8(_{*A69dPniYIE<JW?(l+stm(g-g7c`%jO$k+H|K% zI`m7Xa1)9G!R%8T<k#36s<D7#mIa8^**39lUgMkfWc&?)M$J?AYjjxoh_d4fsKFrH zoZ8KUf9eaA2#CDE5w{p986yHW@CydkZa)t~t1@uI*q{E>uq4<3mic)m3;P%H##`$K z6!r{Rs(wPRGeQ!gXOs#4zrPjwpJ5YJq)^veXo-dj_D1}rXdd$OQ?G5i(W6JR8^}up zj6ud#*S`@XQ-9oiPj-|Ws1u>mj8Fx~{KdmhmOr>;-5klzcKJWNy=zocR~I&lA|fJ0 zKtVx>cWMz*l}igqR1~y`@lr)W$g30~L`*3l5kj_tauG4FTA@NADk5@?%1sf7+^bZP zy96P(Rt1dNTLrdcXM3jaJH9i{_nq^Naeka}e$-J%OW0X^tu^O-<};tiG~6<g=~Q-w zHOe{_rOg?@%iuvG#sAQN2+h#Aiq12q<1e44moSs4Xnbz<BaHF57C?@dp*!(-Q~*-7 zw}r~iCAfvsrbsDVfctEDG(4+eX*aPf0aZ5t8$XS(qTDTFCCB4h3;v!y?iai(plp-> z;45U)e@wL;7>VGzMyHTQ3=Wz+rA%p63tX~Q1E=d2#+3WYI_GYdL*MEQ<eq|5cTdI3 z$^?GAUAi;ok;1yga?alPPk(+aFYsAaZb9E)i1H0{_zhw6@r%w1Kf=t7eDXy{WeZtn zA?>sjGRv2cQ>H3BY>{qP4=NN}-dsSP{!rf~N~SOU$2Fv0o6CoitZ^037w){^es`); zXbf#E5Xam?O`{sGEmidK7=^qayC%vEn=Cx$pu@_ltH~9?MR;>dwoPt6H(NG3X;2s* zIp$v%_e+I1SxOv7N;J2%@Q(lSL55u5C8fqAeSh-wt{Yu}pwvq2eli4s@EvM$IQJIW zMK04{tX#^oS-K=f=ThElaU23d2<R_UXT`F+^!W|;gdr?{j_@>diH9|JJ(BL~ke-hh zk1HsN#38hhG2p%el#G6|v3&@MR<c8-U_qD%;(+CyVMSdZ9$jtZ`D6d_2_w7Rk1n_S zSNA}lBuFfh;_C~^y*b!N@-JP5kgNpHrA^c_aWyO?qmbW8t)jG1&LFx-Fq@IyFc;dV z)@=Dd0|Fun3)qLp6p!cGqZbQeZ*U(Ba)X*5d#o{^UX}MSI5wnUcsP7V>>W?!!~iFh zv*50}P_qcu_O&b0E|P@GEgCr&k$btqCgx1=%u5!)bQ^;(!h-kVX{BM%z6#V<r^#96 zJbOe&F|>az@OT&>$}WBt5T5*@O>*o}a`)d`-_p;jhnh)7`XuCm6=m9+^a&2P={|B) z`q`Z`=mKJIGH<dBq0NQsA5K<d6m&V{oH_7s)H)8DE-Ost&pHg+z;ev4sR;Ag-{mA& z&<P9I2!x3JktKLxQ`~`K(zfP>J0rFjFFcIJo{M4o*w60t);L|fl0W8}Tvl|*C-#P9 zjClsZmUy?Qr@g%a`y#fh=OZ6;!S|b20Qr(W9H^(h;!vh$9SwHi_$Ge06T#iBudyZe zeZ8g0=RwfZNQ6*C&K<hN^=yxmHq&jmC;Qy>QN`p_YnRq6#8ge}DI@5D$JyH6UqKZ* z@b)#O+IZaGqePL;qE$X|RQ)iK@eOlx?9W#adnvYr*-D-EW}Zf{mF|cqlo%)Pk#ywg zZ`AuW+N*4)_PF4p6S4M%zDD;yRmZ~-C`J<7fl;N+?QGOY;kYgKm^1v=FOLRu;Hj&z z!n11k6-tAN0Awy(Vv%0hMbO=+qF%j?tI8|6HT-t6;^njOD_f?VYfN?`Gx^R)<NHiL z6&Hacm5heGqZblh_{Lk@#PMAyMZk;2(hr~(=XpnN43_?4n_5MiJqR?Gl|j~m{k1+h z^)5Se4n7no{g~OV+3RNSTmCMhE{mK8k@7Oc0X>kwyC0=-N5*`yAo1C3SWh1WU(F$` zJ8$tJm#wBXaR!-6zjs&kfr@<5vaj$o;;yR{%_4U8@>l)dV9OquJis^YcHePBimzze zdk~NE&-BUlPzK(AB+l{aUELE>3+JN3lAP|)Y!+bvv?Q*9*B1a==MV8Dm#X_kXTqgh z+=<7CCTp+HyjW*)3G#~J|4TOUo6c+<9plIU+S|3s=w-^~^9bXxiYotc0d07<$7#wx zKHGGVrF7Jfio1$7!>7lP1Ejt765ovMSO*U3l%^8<)k6w#8gO0aYUJhDZs8R{U7ti| zqrgs5y{ofUj813{y1SK**2HC&(I;bSg8~b3vjd&#ds;t_qe_y(>y4q>;9Hs%S?`Q) zfIThpObx?MOiTPqBj`WH(9Lk+7WD6_b*)8X5^72nH95{Gv+B;lfDMP6Z@*%@{yO}7 z-K7|-`vV_^a6c`F?;auMAR20e4KkL22QpJW%^(4KKPbW-{F*8Fs+4!FsI3yj#5(=( zVWn7-+KQkX9uK&lYfr4BC&&{vc>1C#Y0!J4TXL_DX7>%G?E4XwUikF=r{Qmhyn3Ov z9Mo54?_1xH6IvSXOnqiScw1oJ<R#FqCw5Tgf6@7vTZ&|TtDa?rFkhCCh6$+tBs7&1 z0|;;UGkc6Q!jNX^JhF4r#;J!MMo;-=NH~8Cgej8H@g{OHluVA)+2Lu{7lOWfF|pCM z&0|q-ol|*Eg!2x2r#q^c9BaG%(2TcZzNWxoOp@2d)n;C=g%ayg<CRgn_-Mz66QDtR zsH*pN4|4YX+JavDGQR0ZK)}e8<ICgk*sC3`C;y5yN4baJulWJ)aR?|^edw~g)XDwO zZ4+uknS&f+icG=}U8hXI{gb!?Y5bFfdob}nFA@{I8&V1e<)jYb*W*r2zziRc%B70~ zdE0>SS7$H(eB9oBcXl429KyMQVXlk&uSFJ*-uL&fKi79VpXHux*Ye<uOqBLTC(9q5 zCExc8?9qo(Gzgi4G+LLD6x`DXw~xVwl=QYmPBhcepoP8e-t;pBH6J(J@+iXYr~V1r z!p0l;Mft?3s+5|F9!IvXP4G&en3&iowq@fR<g)Eo_1U!`h5fxCP5SAg05B5#(#X=U z+0Z-sw?NI5<uYq%%_97pH}B2>aj+I=-W+eRMBlVLGV(W+&ciP*^xfCYw?oUT5IMLq zQ{1;U-WeJVTG<_A=VtFeWImmHYxhXgor+mZR4JN({h71}`3ElO3U94|Ug<}Qgwz-M z$S3wa`UCOQpD++`<n!Ls`Z_urb=p4^Ko9$?UeYxN?m#1!1j{-2N=_?;+=@5w7D7Ms z=^*gVNmTJ|@tYliSVHGlw&A|Vu5E>)w>C>2R1cH^n=z-1-QwC-BhO_8>`d}a9@^FO zY3e~;!;bQI*K<%w4@pVEQ;`#8+olLJ(PM1TDs|i6nZn8g51o2XHXKd3p*or)pJ(Fp z%cT=x!I|9PQ#0*4EH96vv4jm9YLK75!b!*WqwqKfgze`^pKo-*8!lXc+k;4)R^twl zW)05_E;XMncPlV)#@&?88S)NVB4X^IN=_<+&oFfl4n-8JyWH&+DxZi6ak68?dUaFY zG6Y(Zi((uD>Dx1X&3k^T6ZhCVh_jIYd)9go3YtCA&-<jL0i5TM0GX7$M0PLy&*cxA z-q5?O8u^-WJ|R})56hd;lHIsHmM()%y?r5X4?1^zJ-(r)sq3xKWT?%IaKS9j@j@K3 zZh+oJsXuPj(x3f2=T!cvBsr}ltY`Ll<<4Hm98Kq|;;iV#8E28tQ}E?x!KWYLw?ns1 zpaX~zq+}uJYh*zHPV*;+P%wIMDe;v9lk(W9-&W@b=i}vVw&I$uiQl_d_B&>o4hH>h z|2iyFF|zo|2U~I(+y{wJyu3)qKEiGgLlw+q_Vg{7A(vL9ypyA$lRx6?WSrAf9e1N! z1gJ6wpi?_icAx;4fBJpD&sCj~a7oqROr_sIRebK0r0rhZhmmlL%TLydGE(!DaFP3g zV+H_8v!B7cv8A&q84b?31~ZgXGl&gJVQTDnDYqP4o?8MLk3AOjI<;@2O|+oTud!AO zS;cI#6ZL&>s!Y7&6IRyv&h1!>oD(D8T&Fyi4hE1$h4AEi$Uo^z+!t#$1MeSbXeK!D zeI|Odg=bgb1sG*7202@FLi9K09?R34vkPvWG-JoPC#hVWR81Y)+*8#b7PbDhyMwB} z3arT>1C1~FmvEN&I4zO?gEkL(&diyk$b7y9Z&UZ*&_FfYdjT;}+0Jci4t!Ciw=d=< zSFr7&U3@<K0L^+hqkCp4TKA$u9lcxsqalD$sYoM~f^WCs0Bog5Tj+#H^pb+x!nSvr z{2HY~crF9vV)BXDxB$%)DpZkmo0XIW+@znfT5a61katJ7_WTRePp94V-J<b*A<Je* zP<*t=@WpEjrf9RMtVfUXkcK<gXM!3t<xjxI=)`;tX)p@A!x597^Z17B5gJZB;jh5= zc|AZs;NX;SUhO_ZK{XA#(625W9a&f3UG|U1%JfYiLIj($KU=DOtLthjd{QdRUj*=| zu%~cGm|X9>&X%mM154|Wf!XOLqqWGrF5YHL)DoARRBGh>!K%4n{G<W<#DK)>`ZCSx zf@3xFou2*Cv!!C|VJNkB!cw$-(Wwf3XP{ZbACy)ElTU=!9%f;d;zpl9x@QkzCm)_E zkv0S6Oe|kiDPW*|Z<^y(4kIpT$on(5+Z><Sf8xp$jqD4$B_qBbha!Xs=JLPavEW@B z0RiKq_6kUx3{ER2k7;wsg)B?krs2cmG^HKgbfw4IevcKtqSlSKOK2^=0S9U>Gc4?% zXZ<3f4LWVVbywiEr*PHH#+3?=(oE6$t`D{?Q$p;+vm!)fugx-)L>_UJVA|7I7DhL! z^U>@WryGCr^{1JHuNwnx`YObLw;H3Ji(%@G)}%WL&rQ(g;Q}VAolG&xc%D%fUK<Va zF|e<w03Lt#=BNPuIToPV3ovU2)`uiN(?LpHi23ahr>!2z#R9+~nec^*aX}$3M0=yo zMTQuziFqVQ^@G`SH39CIDU$L+4?!tgXjCaq&oe|rv<=sarA$_yHKtA@tU`)++}>8i z4Q-Z?1^E(g)6@1AdJH+tL^o0J)k08SRW8CmYc}z|#heOT?Q(6TUIPRe#I$8eDL-Sp zlUwaC<|~F0zLgwwst<qqa@AQ^r0#7^S@06)o{sg+CXA0F2rrPw{o^XWJEaj;wv+eu z+o1u#j03UwuOYdyNQ`=O&Pf3z#4H%VyK-xdCFcT2sBohi<5P;~H45SEv4QNv<56rj z%>TcVct(`5jzwSe$#8wWne*#>Z`ZnQN{&b@s}SoWva3PtAGCtCR^;4*J6yV4`VPXg z*owQi`LT`L$CyA$dypwsU9|S31W6`Mp2*6m{#aGtRBE*S>S>w@?$hLNeowD6(Q#vN zi|g~O*#g;U3>Zqx6`xk)9-*W=UG_UNF{L?%ycG@xopz7U`fjeC1rkEP7u(3dSNB7` zIZKMto7ID%zW6qU7`JUtC(Xucuk|~eSPQg$E_5+n`d#MZQ?25KRigRcA*bl+&)TjP zd14N4+U||=SD`7Xj)C5r29EPKB*>%kN2!V<%wZS8R0g_Xz_x|!EF?fFB}>GEIgsG| zHlw7R{|wrKA-}TYnhyV`d!h>U_1k?&RT;>hyi?m%_B!C<*?+TK4p8+tr6;2QJnVSV z!rZSW^NN4z-hjZvTb|f`(b-Hzx6l(oN0OsA41`?{Vd~xP&uA4|H92miDg<f7engq; zsSuna==_Yx0+xS-sFgYw_iVNFOLQvf_yGycp4!&^bC*+Kc%$k>)emaN*2*dX1D=xb zQvO%7veWe5vHz&Ue{<a!u(ge-UsLBSP>N@?2o}5z(ns~rxYLVqQCrY=m`6cGJ|5p3 zWcA|RKfO)~?1R1PmFFcLjGlMbL1W_7t^Hd)`%?9>eF(Yy2wavnNEglx1$oc`)hejH zPSioCq6j(_)?~kUavce=Wx88zP(r{UtM<7#SO^@vP4Hu<xwb6`1h`=JMZN_-DCg+o zXWVIuqE7+9*|rp?I2Dz4lh2JfO${5mfGSh(JhZQpwW@2vr{3J&?vQibZ*j<e)!(&2 zw=%^~5EZzCK@X@gKa*ZfZD%8rP~{(D1ClPf5X49qQuxN<!X<u>f%4Zoop51Vn^7ig z>P+^qM?D1PRqrkz<~DEYcDj9K6LwrOl2Oc(IFyAX<i8{nkj6{G4_t9|tTx9xnLfb^ zt0E!@f2hq$unpu>gaT#u#ERZKy4sau*=3|6i?AHzXxO;!Puer&ckP&9vJ53NK$?uV z2IN4>e3W`E=90{mh}rpf@chSdeyyD;{CVi42s4Ah8;jcK88%mAi|`z;ZhLr*3rGEb zj>O|W`gf7<VY%M(OyEybE<m=jtaQ4ptig3_H{Xo=nmh4qg*+v#ZVjR=qbmgGc^m&# z<Xm(+JX2^`X9om3v#~sbE|G&WQ=$pz4(^JsKZg7JhwljrH)2JSq2er={Y-15V+&-V z+wcuvbcz5c*U`>;fgDrg>mN=GFSG}pMgWrhVksr84!!b+W&GaB>{Wj~J2mIL^k8kT zhv7liq~!4P0~@y}rl+4ixAMyN$$7-x|C*r^DJkzZYnH&2<-K>z`z{we%yCMK^nOFV zEW>aY>XO2<sWz_#S$B|PUdL>-`40CJg*6;~>))0U5oul|7c6`mYy5Fli^vg1&5Og$ zr=;?GOci8rvlhVag7TvgQp$J3Jb37W=_(yQ4=RH*)8Gu_H>nRx6~%w@6|+6-Ck32P zo#G>tQpRhw(ljDaVSkilm~f}sEkTMqA^A-!NvqF@u|H-#vnlEpm&1rM&%MbiSw86R z9h*=bDAjiS7mDPc;tz_^{ZiakM3^aC`Nk+sm89{-J!Nk)c=2*gCk(MP`MFXMfjbNw ziVX+H?DOH?*>)#PY|OJ&XuX@KSCM5#WX_GTyyZ3BrFGkooA~*jO*!Dd<Uh?4e~WvQ z#^1w}ufgHd7o$OEG+f*$20Uq#ywT?upxkekUbLD2BDth|%zQd1FGgt2eI)0n$Qkok zTi%bEfed7e1nBmN*0Se$Z%oFnW=73}+H^uqm%UYg-X&Ibe9<`xr?K>u#$!Ncq=SfC z7h+k2nSk8$Mx&1mUgLL2r*F9%F^|2N73I6IZav79FPXpD>Y4jT*VcL8wG{lFd1y(o z_mZ&E_wO6`Y(BPhCL1#ZRkdGT2ot+z*U1|BGsfg6)Dfx8N&)%9t-Y(dJgflz@W`-# zfAy48MsK!v8n{l24xn#RqucWgESnF%a0+B>>nq#oH+|s7so+Sxzl4Y2wI2STJj^=( zhlg31uC_rOOvS(Tdk61V&!CTpkV{{FoV)`1{~Rzh>xmv_w~km<AYJYpnc8Huc6CXN zeMWgr#C6_QaLtTK2;F-A^NPE@RT;(|dwZhU<Eqsiezo^%`zwDJ@i%I#5!ncE*{G_I zSo?nMP5$@1Z?K>&%p3IFu4bh>A?F%?==HEU#BG>tee(fi-vensR%Q0=#kNj<Xbnv$ z`|bSr?wX29cF#tn+b7Beg<Uy23*dzC23T4<?RD|Qe)a@e**k+6<w5S5bP#NNq_fC1 zxRv60I*KSPF2o2^$fcE-^Cfv_8e%rVvBWK4Ojh-Q^`?byCaw@L2Husb=edl6VnxW= zsRPxD4<lnQeK@o(IvOAZSN0vuRAz<WIVXexPUNZXKcE_UXbLZ;@DT5N3<lVib(1Su z4fcI2>?7rB9UobQS&PT_y%7@`&W*1zkM#Vo?94pJ1jn)II_J#kz2VNn%By4?g7ToD z4n@^2s>Td10*ZDc4-)8&bdSeNI*APeVllLby*U^p8nclqD7n6`C6PWPL_JtDIRp6B zdNSH+q@pm<!<_Keo^4n`*rzjIL<c%sA@gxhh4)$a%_r!UPNMliKVBNM%sM||hv$t4 z^*Pb+b4A|R{y);c@0zH$bc?58vqTe-uxgttzUUbLineS2V6@Y<G*qh-1oDak!ld*v zBG<zw*Y9`?>R5{Tvv>WXKZ)p^w7EJB<>)L34~bj&%~aIq4iBNd6Qa+-lp3sjpG2&{ zn1VWv=rC8>K47^pamcAK>7Bc8QKR+itMWM$hLL5A8?14?>Kmn%amlwiA$PmIgwv7W zwWmk+!m32jpp!CLhs%`=iJ^kgM7QEbwIkB`<;p(yK#L}6YFyT7_jM)X1=}02d3jdd zs+D(>{QFnZ!}~e*XFYe<(PABuuuRs<q)EsnlR#JUiuitLn|c<Yw6U<O$yL}ubI0pG z!Zrf)QLHXhs^^YM6sy-d^`rfTEE{1=@!O9_KW=|eYg%yi!?x&4j`zGeG*bvbZge3U zL`j4v+$y%lU1Xu>A!>+hzI(wp58AAV;518TyGPz&;^LjSSTq#CiW&?07A?3cZU&Z{ zsEu5TN8Iwc!-{JVJH0*^35|QMa6j7QTN0Kn&RAPYcoQ#?X1J&tuTnpqgb61!R0GHM zW4b7tE}Ia4Ms|qajYIOhN=DU&Q6yes!Cb&CPcSctXNhHhn8MF}@ATDzu#$P^EAo`I z9k)7Cd?;|#Kly@(X&7Qazjw30ZiBK@7cbeKJw5XJ^=rd%hAEfP=CAj@u#77VfFWfc z!)H-H4Y<InEux~&>1c?z)>_=kAr_PU2tp{{;sl~RPk}=8X_bhq_HMT%p*g3$ES9BG z0dJ9Ak^-<AiYy+d!l^Ok#w#ezWwGskC+VjArM{;W8+{ZZ(({flS7dfHkGN#D$5B(< zqY7SvDJnZko?-V>m!rx#z;MvXcMWGn-Q`K99zF>BAGT)WQjZ@9#1DJZs*ROR3Ba|I z=ZWbJq)mv7Vesu*F>dwvBq$@@YbdZyabHn*$Rk&-@!j%?Q3TqhBL{?fs(%JvMwHpl zbH4jU$6bsD*AQ35_%|Hw72)6;&~oFPpCD6znt?}Z7Q*j(grGj^mm8F~(==W@Gn}Cq zW<su5sX@k>bHb4YM9dwYXAf`19qGnu-X8hziv3mRpEtK=o<yIq3dYZ^de|**sLO8I z%!_!hBA;t3#IifWyWWJ^z#ep0hUO^0+rt!JS59ob-&;l3$N5v+u$pvYTk)vtPRxIV zy&j}A-kc)>OGo|J)Lkttoya}b&ggM+8;{t6bof4cRNLdUD1!f4^x*_-><1L$M7*L< zdx>)znNDonL>)Pi0%+?>_!6e>o@HUvvvW0Z+~xM3hx;70Hf@2ya$&p3m}N2&9{bB+ zz>Z>WN(+5%biMADp3AH*QB`i#KHy+Fy!44LlKLOoys-r-{-R?eW>L=|75a?=WFh$i z{!ZF*nQ~zxM~~%PJCMp>%C7mfBfl~Dj&B6JV0Er}7C26gPhYG#bt*LYde{i{ejn%x z@2;!7+f_;vIb2U<pwXiDA4$Voh%yU|ub5gMAL`}lRyu5?Y2eqU{*K5}gdo$s7~-;% zW(V>yN7P3jL?+93X@UP0K|SkAYZng2Dr%C+b-Zr|L=%Cl+gtfdEU$_NCo)XO-_@3< zmxaZtlCLNjM&l=U9(j=EjhPDy!b5k}_rx4jF~NU({E-N~kR`i3!(y%u`T8xe*T~&S z4K`b^T^IRU-+|YeE!=K2>^_eI6W$Bk+bzAD1m=#@@6%9WD}Rv~YY<}lS|qD3ELZz? zIg~Y6-~ljn)JGWP2TB7*occw#jP$n-#Mm8N+N+{X++*EA+-v>bNntY|s4lu)O0N>M zq`i*V^6Y5RPyO`pl7#gvqn0Dfdr#0j5Ce(-=HL}a9A4MfR#%^Y_2<V|o~D)Qpp^YT zVS(_P|4%Hi5(Id}i9*!3-kUUdq@<-Fm6_L2qzP^*25l~}tjEc*?|6CXS{_A)=njs) zpz3m)<@`t2?I)gc^Plxbzrkbf;NOQgtiWPZomQk9*=?)Fpx{VLzE*-*GXK|G04{;7 zZ>?upBZC9voB~ux8Zg1R%2(VAmu$8)xh@iAv;Y$1gx5u4mLuxJz2tIkQv3A0-xCZK zF>fwZCg``Hv>+|Io!Brkkxcm995p$_Wd!tqT=q+!+uVCj_3TMw@o74~PC)*e2AP%b z7aegVQ~J;U)V5x*=gPnTbrj)JRCbzu{7&Sax@kCA$xzX-;3lemgXP=OWe+gh^oj4^ z9j)uNS$w;yD>`76>6j}izO&YJb<T~j{wzs#{`d0_NqjeaOMRUJTM#|fPMGF2<*gm4 za|0+SSzL{@JhR%~ySk#M?v$+a9Nn-|ZOFYTSw1sdvP#gxylA6~VD(k!+7BMZXh{UU z_#5h|WwicG@p&dr^ClMhw8LlaqYT9=62QWu16+{y12QXs>HEo0wESA^FAXM-(7ZDz zr)LQl=2ymVTpyqcRs5FC8?mox=wEB$*H!0M%F0{3%3{}pcu}(iu6hOy&j=<B5+KHY z*2E+78bq5Xp0*8xL(>o1Gty5SVNDHbg$zQJxOr0im-G4mryu(N@Mn<!zkbpG=Jz1~ zU55WR-)M+}Zv<o2$k>EXRupPC`L0{_{zRGTO|QCPF`=K?u`8Li_Xk?JvIk+=d+xa` z=~`TvyLK&&<NwWIM1(`=8berg;XQzl3@1Gx=}F`oe^j_2@6xgMCgCD?dV%jkaq8Mn zjd$LT*rNrlEm`X%0r=NpoqJsbQ+`OD+0A;le4sLLqdH<{;E+MYKNr2AupiTiNrMi! zZ**}$*k%pFM4KMO9COUPB-9-AJ`NzJgh@o)vM&A_o{Q_%%8}KmS3Nt@`q^2b#Q)dB z<w!;`gB2Mm*B&XWVy}C>3HxgXDR=bKMGF<7Nu(o4YDZ!z^4Aq?1x<(8kJ>__h}*W9 zZ~5o=M5h6FX!BjnHYFy0TOIIC%_SEv75N7~c8G{5pS6#cJ8SQWjQBB~4=}F@qzPgp zHZBvls?X%v*Y`(v&gF&SOn74CENR<Y_ru1kqSR=>(V^jVyG*7H*K7gl7j3|<n*IpK z;iIn;91=WzdP{HAUo>#|r1=wkV(CyVwZiFu>z-I}pr<v@whFZX%KE}_a&|RlT*Z++ zi)|%s@i%g+IXh@U%HD^9=GvMxVog5J0IL?r1@p&Tw@8B6NRUl8h`bEDc=g_X*i*{$ z75NXf77ru0&n=ii?1d-@<4*$|aPi!w9*S)iSEs0$*c-WiniDN4$Qp~NX%x&LJ6#Je z5T3yb%0gg^(bGW*r0@VPEQk4|5ZU5SofMqY$p03GHmT^;IY*7kyCvwGzDiZ-9*myi zhzMuD!Z~oxC4CD?27*R{6o$)WPXla_3VsiBZlyp%R}7r-uqN~dDj6w4qq<dSPkWFp z4!w@JdE|WX`xdS4wdbuW9`z|>yGR<UPC50L6=`pe-VBK6W`A>J&L(=lsW^{`f4`5M zBUE(aYZ;BAL?1L=(Vo(<h(O}CIm8a7oit?<0TiS1Chn<Z7!n7OnS;EoT?Ft+f}8oP zF=29*%0c=#$a(;%-S!sVm~q}m*7U&Hs!qc{^BnGBqSN4J0ebf^+cx<L_x`FYVaB6s zg7{rj^a|Ii#WFU{5$JyK#;ADymTZl0Zs<knLG*n+qA=829qKu;<#$@N%IkSNJLlBO zj9$}HvFah3-jZE-fp129hn=j(d`j^W`J2Q#?~*5+xkHVs3(L8joxSQ2tLV>5)83aC zgvptiE5A)|`g&nGQSn3}ITy+cZv%}UBL7o+1DRgbApRTE{zA40(JeFz|2cwsiO)+R zQ4^$5t(<aWTf~*sxk|<Y-U(bN%kb#XP?`|4hGk(kly1>lkA<Vz@3TVhgpcT=$N96- z=Q$Cvins50p30hv0iL&gP4hpPgHTSJ4c@s6Kli|S`~{!DHhire62r+sA(d}5zK6?E zz|31Be;F7rRDCWjn=r{|Bfc9i<{B4R=XTfE)i_5oeKy{>X1%<2`>~g8-`&#19mJ5M ztpVbBq%^3#fb0_9EqMKVR;)o`pcGdt$()3kqZO`_!y}{p01{m+;T(zG_+ZD0wb%PV zzUx?MqMfE?bEK_WO<1WRaZnH;@Hhv^7fcf*oGD=|In6Z0is#+Sr-ma2pg3+P^Hra( z63uFqBMZV^XFpWHykRF3KO>{eWLL1EP(b^y6uz}|NV&DG@TJ57M$}x}ikxpm6td`V zToN=f{2|w?s@M6aG)%ImdxSZBIB0mg*v8|V$M+8p|M;<Uc{4|mdi`9G-!YTTFsU|Y zyK?J(WRLb3oU-fU8blR==}%7JLX9KwBd)^C6e&$$vpNG4w%QHrE12hF#5O^!1}BuB zO6c7Tn=VY55SmHH{BNf(k%Yo5V#MCI=l`s&nqmFh{l}}TO~2Ql-qgiZHp8p92H#ph zzRCkkD)9#SDlTF`MaP6q68$E+1mP&N9wR9Y=6Eb-)+(j&?7_u@I-9fu>gtGbPedim zD7pp-oo`0huc1}dhmWn9*cf`KuA!m!!O+U@0*I2Y!nln)!DJjsL@L#fA?fj$588ij z;jaBj;Of1*{HhX3Bb_nn*{TFL|K1}nCdyW{*qC9v9u<XUw_QVuS(X{9jA!+IMbSHm z@k3?hibTay%H*Fn#b@ZayKr(?ThuU*cS!piJrooq788+Zp<I~8U(9A&5Ib1t8)l!o zg=l<aLj}gKlH0om?rR}!>Ut(RGHiEsFZ;KDSd+>Ti;amq<+$OmBi}(Amx&I+EvB`W zs_4*Qn+jF{m&NI~8Ds~7{zQAp@P;+|jo*wc5eH`kx4UKmpBS7v<J6n6>YQM9ae0!G z3K}<H$e!d7lkn|mR52dTSioifSc7@wghbsQ%c`AMd#>SX2`lbw<^YP!5Mp{RSu@t? zEkHUMDo@f#t-bBBo*ZDJ+ksQ;qfBX+n)@n6=lxp?A?1T{P=`k~$<n<8r4Um8g}rGO z7>`*tZ@QC}{c2<hTK!v0#?Io7;_xkRcIR##aIl+9K26V(;x6gr91F~jG%zdT{fa-- zY~~$BU0P@vbc@OeH~Bw;WWMoXHZq+B>8vrav1`JOW^%hIRs=d(po?(qo^m5IF#S*V z_Go)M&lk}-OALr@r%FF(Q)HnkSdH0%{6SZ>N)KbUP%CIEd(HYWM3?8S(E79>l)rHm zi1Y!)<6g*Z_WRoU=jlpo7s&iz8(4C;Kuc$HubNgF=A<<{h*Z~L8#6LQsXwFYvv2xV zqt7pq7putCNEO`pJ(E-TkY*?EIH*ccB+bg|+;VP;f|}axOKYSqaE}0vEz8_;BT>6r zaElw?#J3oT9yasp{#3uh_uk#An#xMQFjdRI-dDp<Hnv=+_Qw)tcBIi<ZLv6%I+Qed zN}CUXHe4#Pn@qSOFMOxQY#YT(K8Yw~XPml)vw%C^Ml~VLxh0-h77$p%O?lgKH`zwr z<`IKh%;ri2qO?CMQ63)PIQlvxhGE%3R}&Q53AUWp1s-lzq){{eH38YJz2RZQ@0C{A zwn>vc=HlrgC{UvCeVADz(^^!A?7xYB%bAA*M1Qy+NCY%69H)>}Tzxn_&?Rq0-*H%^ z!`&<I)>XUmZxpg!k5~Iyo-8E7kVa^T$!<|=Y)}8E41Rx&tzqzvVSXv31KGpWZJ0@T zqM{aND&GM=CkL?61@FKD8XIO~7jq)7;}wVc)-LV!+u4Tzvgg@fwmfNHS5=9X;dnsC zB@j^5*g>%y&mW;yLA}5c0%gOmnR8KU!nYt+ps|k`dc>}~FZdA952Mzv^Of`^<0Id9 zJLT?cvFuECGIH71f7#lt**~E=D<uBcNq=tq!{w)+{qpl}%2EgcvZr)>&2L$gd&b0- zoOg&_NWqiX<JnhV3zZ_lbgs4_X=nPWsF2&Q$mjmP4L4%-9%crx*GvSiw<@}{G;4Ro z4od~?({^n-Aa+J%EAxKVW6q#YlgFw7_Yk@I4Bfbrp~E8Wu$AWKP(vg4yLUKDusb%c z|EBafe<&d`%BlXW4e~O=(r^cd_Bz>TNLPR{vUXVLok8ASUEuczFjAb0Wa6{T1w`<T zx^?UzGdxl$Fbn%|cuS2rHkNIX9OJu>aZ2^-Y~F%LRp$!M{ds8j`gP5gI-la~2p8D4 zjoT3pBOQj2j$j)gtXI8JnZW_3Uv%Gm2fyMUsH@Z$5bJKVCKNsI>6m><uF`KYw{*_9 zJ(P&tJ;aj4{9)eTkF{IcK0Wvq5omlsMR!Ywegv0<PR$0wwx13aNqUeQ9)peh4}fOQ zc2v+Jw&n^IHiDwJ2l7D9<oxl_p0c__#bYHm$DLi<QbO&k%WfxRzuh$D++BJ1ul<|o zH^E7oLs*VuQ{>!bpwn<=@`d&{4|jy1q9ZpjVe^Pd4c;kZNUVOs3R6m93|5>xG8;vj zsfJ_Hc`@Q-$gX(_9))tXDc80sZpQ%pA0>v?KFuR*x?`-FyD*p3O5vg*ZtP?Fr3MOw zX<K;ZH)HwsXuSwvS(fN!%_ekJXKHkqJF6U=&Vcd;(s{#HNYj;Y=Po>DKPo=wBs$|^ z&;=Gb*7P0hj(V$2eRU!7jV=d!k5u=l>vr52I_y&f&4t@B3ue5g&jKYlL`H7}Kv}oW zHoi2RzYuo<u|DQpo>LL`e7v1#ez*-4kCg}7Hd#8oe-w55CCfji?OtqQ*ksRG)aZ0j zPKf=xE65Y}kx*T44z&<C+}uKyE$Y&YR8GOIFf@4r%Pc7m3o^mA0DF5k3eCp(i!V3P zOh(~zugQ&=jkRrs*E@<2j>Y{P*P4-B5gB#6;YJ~I(Ejh;IVEntd%_L-W|@MDuTLW9 zZi^6&XO1k;7Ko2pvzz*-+Y0rQCj|3xq(a%A?4%9Ybbd1IQv%~KI+BA@S|zb5Rq5Iv zBHmYRG;VI?+urZygk(1jw7B-pUtJJM=KQo<A1i<Y#{w4mn1>tE=qP5FbV{Zs3-M8C z;03KjLz7~@=*;D<LS3N0eFtx6b~(9;ZAL8xDMUaSv2B3AocJC!(q2UxGH(8L7mwPE z_Fs!#T~ZwGy}y^(Tp$V4^~7kC0o#yEDTKu%a<0850?#4mR%n87SE_`m)CYkAl%c3e zYcPTj5;k+FUlURoBA$i2?0cBvbLNxCAbfg%`Hhmp&mv<IipTvE4~M%<WvzF-Qfe@3 z{%NE<0dWB(I%I&DFKuUnv8*l#^yoLz)A?q21lBQ`AjVGQ%0bWbkL=b$z>4q<3U_jG zR_onySMgcs7fLPOdf0ZKa9?p#ik~FgxAjMty!0x%ITjaEG3K023dm#P*@*1+7oD0n zj|JyJ3R5iONTkYlq#X`fHe>$V$vhffrbI-R+yf*#qO0>ZSJ0HVYGA^e64Vvrw&R4O z)@N6*{mP5+K51JcW+~PvzUcg7Wss+6daBsD*tW7dd&}$@r;d34sv|S{>lZgrwD<@B zTk0Cv_IK!?ei7dFBbNvy@Pa@hnPgChg@g%EYdSgb)~dA04NLe>_Y#|O6bR>h^e`h3 zQ@79;wufa}&H|{1cYHD{Z&gp-;$!{QwHJbnyKSpdcD;Br;QoxW@BD~w5V_H*YF3w{ zxM%De9Z;71`j75sAJ_^hblC_3f{zEpX;M9Um~>FAWI)QN17g+*vhO_%@b*TbRk^!& z0kL7gV=)-FU?SvVCB+P9x8&)fjEuT{x47fp7~?`rnVnyk6N54)(NA|Y0`K(u{qAOW z@YQ1-3fqt8;1TyBjk}Sza{9kS#1GU;8Uz!I0V0py(+0>NpM<|R>JwBY9bfJO4EcY< z(HvK7S9>ga>zu7l@^IoNZSGcmZ5^jix_G!?wyi$tYQTkefjx^i&Jl6dt^UKmxz4^? z49>D>;n~9O>&OCb$DywBiLb=d<;?$)MeXTiywfF(4{Gl;xbUCQ(H-gwh-rfkw|h#i z7Qp^rM7ZL?w&~HgHkY&bi!idnL7?z@!O5Ab^|}{X$f>F9Q~fw{@YH>uaO~Ort-uUK zvP_l&U1PTpcez5&2~?`_6;tO(SCDTLv`Xjjym2`;#sO))h79y}VE9Kl<D9#h%r5uW zkIl%Sq%)ncl~XL(4Qu+;?%u`2>{u69hm2dJlO2VlC(Zr3hhDK=#NFqRc?fzeOI4)V zMI_W{@ALH_&k-xB=Se+$Pa)_BzB!HLC}vmO!E0J1#ub<@C_UyX;=_3R%LAG0EK4T| zm+kworn|iSb@ly6E_)?&GUfTy77)|H*SvxD5jLd|UnJLaKY|R>M1-O<HE3xqW-JPk zb)NsKm3y{w(DtCbs=LlfPMgP;oJ^5xDLd{Z4)}I|8aLc}@VM&8$6LsQhZ_$6ID?c; z5WgUe@nV^TdXf2c=<t!ZwwxX!y2zaCzDglB<Jx(m%vO#r!SCfAxsLNY$ocUwGuITS zZdS$0rOiTWxC>p9&GzLe><wGu0&8A!N{{wA4)Z@49jc75`0NJR_17+o7d=91fHdw$ zf(3>(stp*D-ezNlcVSr|s@AXq*zevg6my;@+Ao&TQiE1?R}TN$qoKCeHk5=07e5ZJ z>UB&w(d3rE*x9>Q^-r?w@qqb9G7V`QWY%=5Kt~&2!xL9PXz?I?a29$+PHhsU#JxG^ zz5(rMg>V7~#4P|OD90l>Es~m48exar8I2uTTKC{V^2o?vV^N*P!jOms=D&dX{BJXF zCqWXt8y<Sc38!aZ8wVgk;XC>*?ui7yT`4WD)k$zo2s~B{IM$m6JlJ=j*O5aB2iBwg zk@B(X14fZig<%5?tI>ZT<6hwG=lcHAJoTLb<PbnNfUe-nPF~gg49RJuEguE|T=}NF zqrD(o#Thh&%rOepZ>O5%S3h`B7bqf@;jB{P+n#9k1%0pr$k|=d?Y>9OG-1<E^t)T+ z8%inMMBfboH81Km7qSL|7WDxjvBFbZ4^=DGnC(wbjYftHC;zDKtF+$7=&vkE<NrYT zG@O_2>3%P7K<?-HKqhE9d@%5q-*;Qp?D&XC)-T0JA0!?;q~uHjGL;xf{EQ8#Of)n2 ztx_8gTd8DBY|^lf{}+80n2U|?yMHgmx3bs$5Mbxov+G4cL#)X!IuGtAzuDXH;F|pn zZSS(CrE~RkzM2(pW<a*Q`HN2eS#6>i{hR+ntVQNAnIE`h6Cyh;M7<fNNF!90$3Plj z9-f2wY{hdiky4Q1s9r?;=rPMeg;q3m;RgnOdO_IUz%4Z`mij5YL*;d~zJx@WTBSmM z?8;2o^1<HD_r+ROWoX~T7oCywDAW}SY8u~!4}LhJRs?(#IZ8@0RmWpJ&l3T_QF|+P z@;h!;Bh$DJ#{A~8I~H$0KiO};&$lW*eaD=OuNQI;pNraL^F?PHlFZ4V%Wep3LR%<{ zxcTittF?C#)(&Euk~%+}v>mYR0l|^2U29kMRdsxxu-@WhPxZnUUc6Eusp@M<mNI(Q zmK{H?7}P7P`>rijEEc0|1f&(rCZE>9I@j_HaZs3*tWXGB7-wO#c_n?N)tlBW!`#~P zR^sP^V=S7XHeT2Oq>P_l1hi)76}co&JloLfVS3NAh-DhE@wFhKcr^A&q*1T$@fn&x zSnWax6$gMo*g4cZ`8R$>^Dq8WCY5jS_9SAZs`L$3AhAjGFyDaoAghOHX9n*Fxyr1u zUoG=x$lIHw)K-w+^?TuH?K&1upTD^vyw>5~LUk5WekgpmcRoLaaQ<)d$0-p}aSy3d zA7IF(VNAmi%s8`w-bY$-bCmRScO+QOsPc*_0+y(B0dFqM<;F1Q=d4>B7uZmRC^D>k zEqs^M#Cm-rS?mEDB<VK@t2da{XQk)?k;F|lc~0X+97HM2%+oG>3Z!ftut&n$(;948 z{wJ@L@mx{AjF|ZPy;Os-+Cu{|WuAeUp7L4oSn_MR{j!RRq$kNnA{=@^<XjicDc)M} zpaG7n&t$ze-=d|#0KsNoQ}~)`I7%wmLWZgCu>95y2ecQ5S*lYxpAv!@SMyB*N2fD# zTg!UqVLpqE9_wr3c^lE7YpC?Ghdv&A^DuMx?Z`|tCArSxR^Zs>fM-R+tUU9s<CSiP z8D0TCY`a68*H!n<`#iBPRoRcoZ-qr%zfihkVBj}m4tAmfQGU@m1J_JltYXd5oE94= zd>{I;wf0^0UHh`i+Rilh75U??VDobCAN%X^Lr)}&=Kq$w{+?*AUEdv@n_@bQPD+=M zEBJrGt+I<g<f4=|*#{ZvDmYEz)BGQRdhhfo{Tru6pM>)dj%wCcQSJrKuF3Q9{_WY! zze>|xz5WS6PEc^k7*Z3fdRQ{%H(h-Lc1TYJb1ZhiKjYmKY)FxC-{(}k+pAZ9X7)GO zCif2qv8*!<!;T~_-TF)GlJGk^^kE0+<2y5P$9<ESwwxCQn@<1hR0U?CY;WPDN#_u! zP`7sa{1}A7+ld``s2obCn&YOLrpa3K65s^|v|1(?7KMeBISq|RcuCq6ZoXUSOFL&| zF1#%64hN0Aqhc9Y1TGb|Ah=xzF%K7_hDu5z-<n|KPK9IS?5t+-BHIDDsm?X5LiJOt zW)E;^J%>?d5)q<!X4U5Eb#I<sgjrZzb$S^q>sL>^uvq`Nz;I?y8KNgd_nz`r)oAt* zCfERY#&0An@eU<uej4-GGNw5U#r+I+iNlP>k!1sQem8I-X;QyLisx^7ba#Nd{cca; zgRn~Od*!{$?_(c6xX3c6uhspSk`=f@&l2H*KnM)x+Cr|D7zVJCo;X<L)*hGA@<^0& zAa7|O&sXU)WEHVqdI$q~7VlziXB*`P>)&4YStAxTm~qV=e>}W%H*m@iq<AGii}CgP zF!lVJxUGY<ik*bIG5?Vkjy6gXaJXOJ7r*dnlA`;lLu(LOH+3O@s2x%C{Vl9AKXbbo zy|NR3=qwDOrOIEt(UucV8dsyZ9V;_#<h0%8x%Rzp=v1(d##dsNL-?6X>gA0(WY=2} z3dwZxu(p!^7HEDf=rqv93&o#HMXFG8o<#eA9xk}RTtW{Q4vH01ZNeqQempRrx1s{? zym1glnrD|Nk-*r^*q@l_EYBha_-i>We%Z-2tb%ZVn;ZW~-`>P3M;+dkijE{2p1t-4 zaO+KT?Y{OLB1HHau!6*LChvh%mU<z-PipO05C1`dq*p$O7U+T-g6K4{DxYPur2hm# zzcK0x!w~l6tZav?M+#Y%5xKW^aKcyRJl%i{`$k5stf0SOq5x&#Yb;dHP^gBFXcle` z*6b!&m~#f^GWl<uhVE+*T7;)y7EsF1=h-Q!DN+lM)i|TkgVy5!h3_-=Pd%CFXxy!= zIIl9!JlBg?O8gH6NnX*euG|OE5|lNugjFt<Le8zir1DtRW{o3{jk6FLl2%J;u6O&& zxUj&wr&=vt&d}Vg<j+D|ZEVr@G<T{9*MIEcis{v)-RP?eEMrL~q4z#zd7z}w_cn8M zx*;VhKky(o<@3i1xD>x4;hnMJK;DAk&{Yvfr{<Tbo(TowFvIaj@m`noTKcQ^@t;X} zF0m$C0_9bi2-)VAws3j;qmU>^WV8p^Hy-qA3t~Qu)!QJl3(R?N$zByxJe<elyi@6+ z88oPe@E@>!ZE+Fj!$!UZDcN|Mc@CSC7Uz{x$P5~_(ZOOmGwQqxca4~nU0}T;d(`Fh z0A?pVt~6(jP_ft#KYT3}AX)lvt&l~U>6r$TvA|zLfT)lTzwRQ4;JJ2SNs9Jlay4;E zdyDXtv543Zw6AIhX#(<=!Z`u${Zfjf*dUz$4cdOLE<{0_KURcUv`kAcOsGP)zo_hG zgUQG=HD!g3d9OQDiM&03#RT~^i?V%&J~B#J$%KmuFU;YZtTUy-1fyRhpttulY-9m? z;!H6vY^i-Tc<At$U)t?3|5XK(Mg0EknA~CQQ;Fp1zwTE(to?1}e4pi-DdM{U)jjA~ z#((A6D?A9u*j{^yyY$I<YVAc2U5Fj}_1gRPb!&`EFpXjg(!$gr)4sZOtnz?Szk$3( zXyNIYb=O(^CFoD%1w&(&HrMXtGm6+H_FC=p{5!)w<Q1dD#v_!;Raup5_2RNltknxd zL>3UDN&xpaIx*BtH*c`Qr{u9|wCD#;9drt@4MkFUW@W=~YyAAGoh7Z+R~3P7JH<ku z2jJ{$^}W-avXSn962l?xYgSn(z{?}=#X5-W@6MM}8C?b09aN#W8&-mi(5>~rc+250 z9v3e1Fym@=)O45C1zcx0{Aj{dQQmDR4H}KR@PX^o61fqSQhd$ntIA8uUuQO{eS0&d zyHD#9XTpH)0f;{ZUnwMR){q8ekgS<$mccy28B~vd*6)ot>v-XeMaX}PRnZ3ow(dn3 zQ4?0z7ZMAzCa>Y5o8&XGg*$?dEBotj^0tCKH-M(P7^5YSy5|L0!6nyiI5oQ<N^Wnv z-*R<Da7kFrr9hVJ_3DT7tLj^phlPxY$7e8`ziL#H#v;I2j!1{H6n^hAV3c<zzPN-% z)iusVz*8FJrfyU+soea%7~^7$y{#{{qC-|jm)EokQZj3)=NyM^58l1+H}G@6Qv!33 zCh$qp*6Kv7`0EsfA$0T10wy*1!>`@Ed37tm6s{{x!Cb<gj!ki7L8mrWK{*#ahZeJ@ zf6n_Dp$VSUEa9i$9L;=O)%rgF&d6qj6Tw+>d4uW98V#Gb9DQ9VQ$s>G&wW|ht##ub z3-MDjk<l17^jD4fXwh4~RZn!sr^J{Uq32GNg&e%S+x(}wvLhvgf^QDTX&g`uXb~{= zUh$4=v*|zE$IDmpm-UF{M@H#}yfs)(6Vv1b@l8*0xUv!YYn*nmB_4|G!sa>uHDDPz z;J&VyRkNX`I@XS5-Plk?=Fw$!Of*n1A8CY*aZEUv(<Yrs&VLJsmvrR6G%Ow$8W!Lc z*5kx?s6}EOr4UprsTZB7)UXB$cI=EoY=T2**oA|Yn=uY^g*Mpg^jppG3FTaoAI5H! zTT%46pLCJw&7$Wb+zxrnNJFY9UstmO*F~eeQd*b`JU|B&yxfU}*E{1mg%0=1)=s)! za6KNIwX+&KJmMX^);uwEYv!o@Z0)I>6ch?alM+~mz4W)I`HzM8aU@JYp8t`2h#Uo! zb23Te1!GeUc1YtM*x{yqpXg@%XO>9p1%igv$OUI6tk&J&K2pRsLo?cKe|@jV!ZH8J z!uUI3q%L-u`=&V#Soi56y}ZpbuSWy!KRCT<F_D*0%(;(siTgD1zobJ+zV&rIB6_e$ za?$OPVM)5(ogNvr)$OcL&Y>WKF57CfwEE+>PcM|LHGMMgwEji!Eu~1Q_#p1MVsf6g zoNrBz2mu{tA!*VX<Z@yyh=0;f2W`LuPL=a^q2&){R;qn%4Pa$;x~iVK6P-*`WYBl^ z#oYN<kNdhiug0>^<yIVgc`9@FlV5sXonk3mYNEf>3z&hBF98)q?E%5R%x{)fdAEtv zsrOS<7AQw1zUZ+QWPUrFH?pBw@8Dte)>4$g^Sq8f{n#oQNRP%R=!V?5&fWd8ZYS&I zETi(u>R%5UJ_<<pNxPo)rx{`@Mw^+{?b^j^l@Hu(8Z7jS&b4o0HcO0ukC2Arm>YQi zSd{RL@XcT>XM<8(D9~<l1g>qWM36>q@W+#Wf{AB!O5A~>JKW==&OtV)cLvWy@r>D) z-B!<;JQMFBE_fSfeCbFMTjBPM!$}cequ?1#a_*%sI$L!oXF-`Btl5fu+PhnSQUs|n zPW~lcjQmW;R|?)0pl-^kv&cWFORK$Q4`c;S>N&4oHAdZeYtA(Dpy3}C^pt4TzXB?} zS!K=g&IO~MhkAVKvV(4&i2j^gP#wjrT!`8dreAa>$GadT-TI<)SNd2yw89&8E<k&U z1Ko9=oN|1{UU5~Q*BL19XL7|sc!JU`(Dp!;owh_?gE7ucuo1z$aKFsj6NL%aeG3nd z)$Y1_=SfuIkai06`_tN0kOI$Y(Te#7{2r>6hb6aW(g*)oJRU^FQxrc?{ZQK`=@RZ# z<8+&_l-W@pCK0Fb0^||;d%K)+X+L#4Sy_#<R44wrbc@{fw{y)UMudeXxN8$t*$E%} zGh~y;No3gLE1q(LY_7dUy{~Q)&H?P3qS^q9BW<CZ1N|aONl6J>h^m_fNz8?HR!ZTb zaI^6dhpy-n!;D?N8-8RP8G2S^D);rr<P2Hau_Mi19UkI8g=01%yvtO@V203^i_rM+ z*5ee+N+E2bFXHK;%w|LDD7oNV4AMyr8qM2)I@c>Rm<M~@J#XSn28@xL{_sg{Iy&VV zIPuT@!SI=Ge=7d)JrT_LlqC9_P8!eo8Vg8A_$6@jgaM=DekaVW?~}(~AeWC2^ls9Q zw-u{t&!D$vs}mJy=XuV=Ftv$X0`($E<!wP{KbU@7hrTXCM96sD>-*UW1$TQsY3W|Q zA@vV>qhC-y7T(n+At$VNO?UX-*IpEduc0{^K4M#K{~OoGb`9FRe*bggtoU@Hp=ZQQ z$NS$_4w$yi+LV>KHRDi<#tNQlRf8_?NA2%)Sq}AH;bYnbe@BoX6{2EUO0xS8%81#9 zFP#>2O|-I0#*qvsb%amAv&dsvml|XKuDBJu!*R2;;}wz^NGB=K=S%#f6$xHlqH*Il zS=1hm&Fm~XH2QT`Hq3fG`Mz<rS6%hJ-g1QH8zg1a<-OXmu<FzFfkLBO&#!b8j@*if zu$^AbZxvQiQs@!PK_3}S6#xL<I85bCfun-ItpNE1ct7`|qDg%Syl8LkupYT`D*pD4 zRT)z+MP=`g)QC@So*dtw_&&hS;P&OGuXIPJKMF1cS@!W*V8bL#s&q{D<+KWo-6Q3> zHg0K|BO@RHfIcrO7#-c%%)7f~Aa-|Yg-eI2c~sLtKI}Ys2MiV&h%^bjRrnb|PVx{3 zEo7ek80`!JJ+G)kSDK~z9?Q6eo3p2UbFW`kdSNza<inm`{goa6F4>rwUl_!Qh~ljL z)h_Wz5ws`>D}a?}skBw^#j;Xyg?DQ%XEFk>k-_(!5mSaJC@lhWVi#(A-ou(@c`_wd z*13S|-zN2a04|jUMp1_vkiLnI@5?I7)*gQK*&r(FyW~Z&HPiK7|Mivss(imtfm9Z@ z(~Zc#=oR92j>Nmo^*~KI?jxf#0!@LsxQ=m?%UT<Pncuq(X0H5uN_*zc7x{g(=wYXc zbd8Z^e4tZ|kyqQ3@_)`A9v*6L_z7kRI!%zqaVfetT{s_MXL*=ubC4s?%$f!)+Y0H) zl^hc;6yN9FH{Zhf0fT>cRX&SqspY8j<VJ2SuC2*`^!L~1Jv<_(8xDpK++!eYxysxy z_0!P5D4(RF4^FUSCSeBxef<Q*2>xY1uu`RqhxmVi3R{Fsnjv0T=Yw65cc$|f6CPM7 z(!w+z=Nn*|g;CDe9J57dqZ1Ac`Sz)fRoC9@i}u~Ds<JYC6iP7+sIPC4_CnXik2F4p z0~n^aR$DGYex^xEWSl0NAuedkGlIevz0yvvc%P-D8HZ&D4xFyc@Bv-mE|2AJnI;XZ zcw5Bcn*PeL|AVeKk8A4ex`uH;RK%zVC<t*zi$GNd3rOmSN-?4p6ogb!86u`uFcCtI zf((L~DoQF8q9UL&M1+7sAk5SOkueg43{(YT<U}DHNpgI*_wx<!^M23whZ=vHaB|Ld z?Q8F~*IsKsZ7y|Sare68U_-Lmv8<ZhFj<sW<0v#jPI5lD=>LHX!ECY3AO&0Fp-hv( zw4>x>u<DDtQALRz7bazR)DdQisSl)pkvh3J<;b&W0V=D@Qi<V^vS`Hnt$&DLfaJ1Z z_r2BC#i^vjRqYPIIPfK-6G?nS&v3(idYT^qX8183Vn{ax?)zU|5jdFMEVSGES~5o% za&Boba^VoGD{RrluK0aqSC2h-DwH1faAwKdv?e)nNi))$ufB<`M@MV9iv)(~d(9v> zRuGChy=bf?HFyQd@>1lqDGY-~n8))-WJV9RSz<Y!s#mw<Y%2^o2;>#(BVA(z<013( zk|5+B=IV_`-u%$0M^Mc8&jcfm>?cWOqu8b(IZ$DJ<@7pA?+f)QC@{aYa2LBTWz3Zf z247)PRn1^~By6xXmSx}hZd_};1r<WdE45$3Fl%zDxXK5?dFMmj{22PLD$|=X*ythV zJ2UfphiV+VnAFG^bo8qb&f4!c;Q?{%XVPzEtrMvXYFuQ;Dm~;VOg>e7e5$HaLv0pI z)&^mr8c37A$O-H+!Xm9JmSugZtYTYn*uY{8DST1LxaMHETKAq;SeH+GQrfQEBIt6< zPi|kyJx;?9Apb)q09=CLQOw#`3C^+D8swudYN&n=^6id7BtcARmKcs?6t!H?Zh7#A znvodqwoOJeWhB-H+PB{kmAxsl)o#i0!%FKj#2Bs7K6l6&%#-(W3PxbCtT{KDE>mt) z`sv||5os&sO^%wY<0`-DfldzB0+cjbWX>0B)`dz8nFi7|z)q`VUjlV&&|V#<ScJK+ zu0+`NQSXYbJ`KMs&c7U?pvL%o>M)E}@GFx$TRqR(AaLkg0MVC=?jv8Im;fk6Yau<h zZ|RemFgL?EMB;e(wmQ;mCVh}MGmJgEWpi<njB5rQz{#ueawCFINw&zl)6v4PVu3hd zyWR5gs>8~o&?Wp~%`|NiA_cJW;9g>13J9+pB)|F4v;lvKEOs-K*~dy2Gv})+<jyIq z)$ns@;9DF5HDruDz-GV5ZfA*HXR)VD*j;Q6^X(q5$0nY3jc?xid8YiLZO6z+-<U_m z?4R}jo`UE?k-A?<(n^=$|4gj^jTypbHo-j5bDE}_H9B6IFy2X`Cz!9Y+O_nx8&OV3 zk<1zAC0Cx#QN=Jf$}Pfo0wWOG?poKv6s1L7#`6Izg~I-&J$zh32%St7T`s#o7;+at z!iAleQ&NPFMM@i#TlYu|Q>b#J79n0i2yScjb>c&ZTIFPOt*eTdFt&zA%QdpRc`qX` zKaAu2$JBJR^riISe98w1VgF;!4pp`E3W5ftfLclZ$7LE%RtK1%)XeL{jKOLuI1kG_ zf{F5(tCDxS%^zeFhN0vjH3nGk!w~-*tfi?ky*fv+0HZe0fq^H3$;Q0T&mv#=2kh-@ z%4_h?+>1&LzFuT>;<I`+=d~milI&(Ar@Z-~{wuN3ouHb*lV$y3>n!CohST@554PLq zck((~{Mwm6A@{<1V&4yJ3BNzgsW@l$=f@}fCmD<9_f1UTM(1D~MYE9J9wO>Xx`+@3 zhczyMcF|C-n*N$)4;dXfrDhEfgN1VoZ?omrwY+2rwBmCaF>e03p&$fsq)Y29S1h*J z+p*#a)zYfvVAtrq@~h&7Pu2EI;6MQk&)ZeFm;p+46(2h_rb9%+X}2APUeXvs3pB>w zK@Pe&)Cw`aj?+;*OD;4CAral?-({pU`a~V(gH?PP+kipw>f&`oSAkJ%MY`)6#E&FR z;Gz)}<$PEqb9GPMY&45l5HO~?8)8u(26tW@oW+{QP^hCiAOpA$lA*#xD}5Ex2;+&8 z1rfB%#D%P7&k7hwW8hpgy=d;`LJT?f#j7iVU>rJmE(^J>n4~1zMGEyVAS4O;2h_?! zbr+2Hk(HJzHgwoZktSXe$;h1)fRW$~)&`&rwQ!44aw(xMAXKBzu<<SJ&PCJH9%dQ% zJrcQ*7-^wV5Btcg?)A{q-!o1I5#^)?F1C79sZx<!vgd>QB0`3dN;as4r!|Z5Te4~s zz9fzU(!h6H?uS*Z7uVSp$&I2LDp{#Hp|8ub!{6sT2=o_=Yp;8*yI@C{fIhwc12{PD zLm@_dvrYXD`XI#b!gN#IXHuOg)1VPTl+&+3QW_3M^466qQmUH=G(wu4UP!$*0$@Nx z60e2Sm~}W9A$$4RibmrRaXG)>MO0A9>yKq`h=;V#F)GwzP^{GbYzv{@We|ppJ2jRH ziQ#f+g-GT(APF_bShvhR$qZkp`8CKdv2I|c%A*Ce2<W?2gf~}sxo;JLCOu`H5!agN zA<M7jB~8<r*Gy-4g1IhG>X5j=X{v?4?~oIwYu94uWtQh`4)=`oYz}T<&2JwI-|I@f zH^uHZ&%U)ii(l(!{P=jyl=vwAneVLf&^+cYlyDJW9#SsqiE?H`4~AzBqJ8(yslv`x z1}8`O+x}56@lNh||GCF?$d-0D`I9TPsvzRYVN;7+gN8Qe7EVD8w1~+8DW#Dn4#sCa z)0|{Z#dh4Q<1xLk0@S$0eF3^R<!tnfOw$6np^_ANm8<T%UatNeAlNz6{Ny1qJ$3O7 zCmHT+&i!btIWug2_0ByB-a_Ip^c4pvd$OVzB2!pv`aLZBZ+!MY(#-M6HqGvlEePrN zi4J{VabwEL4bLoW5bnfIkQr*t$Bj&Nu?V$o3f$@~0;*=Wb~k3IS|Ov=lczH0s#+TQ zeJL1MwwYdQ=9ielJRdDfDI$2HYPLMf^_Q;k*7OXM@mIO@pi}B7CTnXeDRV<JslyL= z6+EdBC#=>bAz`$u?k>9Pq!4l(aRxJ76(keIG7&?$FrKw8P4+4dfcnM^LNdS2n7bf! zhNixTSSl2mc?dg;KACPlY4g*myH)W;#mD2tE37B(P+odymu!aRH-g=SlsA!fkqdzv z5|>L>Gq8A7>7X6A0DDiFuRT^S8YjhrY?DD^r0Hv(!`N##lR=U`qvEhCp`_3YG4y*S zR;S$^stmtZx+g=~bGuypo59y>P%0a-`iNt}LcTq|Vzh~iA1}~N696zU{e|vvA8rH| zR3F`0#{D}q-dNl(iSnLX=+*F@&x!Lr@78Go7*utNmDRO?o|$gpXVB4Im8Q6P`iZBs z1)FOwd~S)fd#CmKuT%s1KXQ#7s}M>hL2E{-H%|mk&p`;1EfnS))zY+|(Z|CX>^fI3 z`Y+q7uv&@fSlfkp!{vcCx86~a1QBVWBX#pu%KWxGvOJnF#Ug79)A|7;L2Nq%ou+*t zlqCqm*^?V3V!*lQPspL--QcJzK<bc8?xPP%Uz9qSsPxwCVjjh826=#}8IJQVr6V&O zEQFWct-1yCc?x`!y93HjwUc&$Y=f(<>yfGshkgQC0vx1Nhb}?&<Hr@*a#`{IJ}KpY z#2kFbpbr3CxH_a}EmC)c>8Waw<|`2sphQ|t!P>xxy<D;|f^4ES;j2v3AVI>;&<jGc z7jn0CySWZ>Ef^n~qR6Rb$o#M~flUv@jJN}@1v~DJ?B6<x#E%L#J!U+bru`}mhww&H zj-yodBTOT;U&wkUv>Z>}14nHNf0rX~3e!#{WDAxR4qL5&HpiAb$cUJbthKp45@q)Q z{71}hW_5+@i?XKNEczs^s7yX|V2JqLg3p+uR$v!lx!LQIi^EALgsIsJ+gXe74x&Ep zh$)&#^8r|Hfe}e)U;=bcY?REx`kKTyMM-Y{cZdBVIFjC|U(++(uZ4rUK~X`#;a+jy z&Fi7kZ;0M8F+jS)>!26B4WIktciQXpJWPpJsVTZ!tl8QPV6Cl;mJL87q-DSnQy!HU zovlp=egD;^a_$^PP($E6bOO$XB&#VZ{g%tQ-GUW`BYvMM3Kjc8{Q|S*-a48;G}-^E zBodL0eyC}~2>`msX6N=7Nbok&qZ3GL@{ylK<vj>Y$cDIDRHf{=>GjgI0_H?qE;-Q` z)LU(`%Y9UnJm7Qqc<Iw$e|%yvNpGjLAxfw-^#{<2S{k;3&|r`Kz-|!6y1Xiq+lyCt z!T=p>u6CU&FaLyHij15bxT!?-JHHa+6bwbh$E!=Fgqi)I2I806$TCA*f|CSiXw<ty z*D5ZoRGCa9xvkP(K^nv0`2LkWxs>?G8n+;dSHS4lbui8yNX8*=kPc8V+x#m+X$c7$ zA5AK@%p-wi&WzG!x^0t&xY1+KSDStMWM-*uHmdVl<ShE-S2z^q+=Vdh9JoX^BaPU= zTQHQp@ijjc26;N~MSBsN%&n7dF})EP0Z_8-H#o8>(|%tz1=kw`LL*X0(V9Dc(_3>K zg8*|v2*NOV2X#)OFRM7nQGFAnOwXH=LPP_BBQudCl-}Szx|*vR1fb4=w?O+Fb`fkK z;cnFs`WRPxM!wW%P*Rz@!JewI6FgmedJc%+_J@iM;=!mUV1M6U&w_*9d}_G!p7c+z zEJT%w+Ch8x!tcqL?K|bT@?T1Yb&|uI;!mGW2Xz*@&%@h<M^`+$dOrKt$B#9%`jo!5 z$9@G#wl$*j^t1mqXUDw!JbLp2=}j0X*Hix&PO_)Iqc5&Rq+$xTuB}Ok{lKk5uGGxZ z<&5b}X>3X9$vaZMNI;dPuFx3oR82s5o>6TUAKAan^YTE;Ue$3w&Lx3IP_cjLR@)7- zrH{LmMGoUNPNucQ&UntF-=~~Hkknx95>McO<?DpPkA*{|$wD^+a6P0v<A7PN`ZDAS z;SMA*2b7Jp3x%>Byk@pIxFLIHN#{7=oC#5s#wy7?hJQobnN<UqWx}LNUOm?m`(Z7g zA$t<tZvmZ#u*$rf_xgXR?J=eAo^yft?!kB69xLl_daK`oAc)c4X|s`88Y|yQ)=PwM ztzF!fkH6*`&}tS#3y*ifeYqb7k-d~bY=*+0q<XKAN#eD?a#Y{+j#61auv?&f@s`aT z>ofBB_(fOHXg(u1@6FqFW~Dn8R~(k(P$Pr-kvU0vlQk8@j|{XX+xDV~jkF8yPAXnJ zt%iu{|JKDvMlBf)&6P(=-o9a^$yT}>^+Zq>_XRTO+q!wyDq@Pjh8Ln%*Uau2rxB>; zg_FKlGPj6oNzFg}?~>$zi##s+Z<6F(w3CRQpz9XHkQ$;j#=C@CokFu#mx2ShQRl=5 z@5G5%_vQEtBnnVPV1l^=w>0NqC()y_{4od?qZ9RhF_*qDChCbMWnL`Lz88pDZgcMa z7pmo==KbI2Y!ie<6vbrg#N|)g{f=3`P1F8DorIyWI42P3u*FwF{z<|hw|mf1H>vp& zpo4CO^o2*yc;XY?HGFx%TlWX|-6s6(E+QH)@sMbv<J;DilU_yaW`IG%oq|_;&RqAg zP`QjBl2gI2h~K{wkA>n6u>_j|t4#tBMT5>){)I8_r9kH!AyI9Oa0D@UdsTV2R31%% z#78?TI>uu*e1MT1?NNESf1Qi%ySINHIUFJ`i*ipZl<>O;Zs`x?ap*r2+mRvws)+C5 zh%<mv{n>j~vrv7CwG1pV3gpBuonMq%vPc7kR9!^PcgWNN=HQ?!4Mn#Na;c1TdD1C& z&?YNb=6oTdfM7hrH90kUx3d^V6BiCq&<l)|`T>V_fmOS0`7Qb4gsRZnRKMhq*8#m@ zoQxf%q1>1IbPMwk>GO|ltdWo`p?pNXpSav=4s+Ht3j~X+mGJ2we?lwNCTPBL8#*et zuLoI>hU^)b=RJj$k&2RF-T-N30!|D$cP3;K@UMU$o-H?Z$>W}(5iThOmiiB=XBb!4 z%h~|$Q~ut=$AFDC|NQswAsqSxbVtxe1w~tan=mppSu2H5&wzjOMUKH<CnRiPhRTV( zB)Q|K>KIgWAfcfNpQTDjfidBj9C3e>@i4wLt$w1jl}<{CTzvfc^G?6=^F=#!Wor8B zfuPf+IQuoH#aq7x8MapL(ClZm3Q@A8x1|wTg1a#aAjZvN&1TxRuZwtL;R(<;y)u2^ zbuZ>G`_RxfYgjQY3}4zcSn6fp;BML;-oD?EezPed6*}O(dNI3qAQ0tLW<c+)!V~Sq zkjl;3%LleynC?&AB}94;b^{pT(xv64Dv=rq#InNW_OlpdpRNe9)v(p-3d$_3Q`SA6 zDE<&Hh?HgRIU6a{kq&nhtrSIA9y`CX!!ckbc@<w(1s+11H%%LzZS(aBb_*8B5v0z_ zeG%yBB{o!zCL<<phohy{h&TFf6b8c3OdpkVoMUvg9i9KQ3Y8=s>#~*gMC1i#S=#a! zE%T4gr8q56ZEywCLfCG#MAVCntzkDiVFu_lIXn3jYweMc374LWGGVg2VHFl*eI@Ae zD&83MRl<DJ>WUOTPIty<jU@%x`_=zc94^nY{qVV#6c)l6I-q|WnGC#JS{4(uTQ&Du zu3~b=gM4a+>C^l}3iH(!IRkxRQ9DcSg`H~;9R6;dcGz=s_MQAN)3XiqpqNkxu%hnV z;kMw}!lVy7FOb|2X)*FS8TcO_y1x)5oL~>FTO#r0d|cyhBhqe^kpZo2#2vfGDA34j zsI)4Aj$;6#vV4~f%yEl`jDuGu!~+kCDI)}v`1ET>^Xkq<J_rbWW_i!_jF)Xg;hujH zZW|j4KoF4ngjixdF&^rDs?#O+z2uVltw;gGq8_Lf=dMY!_N%>)sXp<E2E%>^%|;;a z;AS85Q#6+_JZTqqci74Am-2M+xwO3ACZoTXP9w4<oNIi2V?^Oad5LJ}6%2mRT1?1C zlc8Y(M8@ek*qMAVz3nVy`0jk7O5h9@Ik!~O8_5=!%`XB$RYqU!B)iFe5!UHj_1eMz zU3r&)a);r+uk%j!(c4t>SDE*VQ$o+fy4t%?&wB?Gsvdnf_+vBi7DzmPVJBKyVQHE} z+U;DVULE*jPxD3T=xoMW&$K2hch!lvTb;X#BZ#wlTTRB_bM9<$a5jJcu+=2t?=*4K zy7wcWI|hCy2o-22KGz$htCm5hQoWi9r9;mJ%##9fv@s9{MMo!>2t(|(>s>_O^lS^% z1r3sN+Q$Q$pYbNY7G>}m6-mZPCI!)uh0Kj2J~_$CTJ?OG9ylwp&Nu3KCGCoUs3JC0 zzV!VwrmQZSlmLlOfM5Nm(O;_s<_n`v=S8~iqUn(u{t*683h0!BLghwC#(hVb_&-FG z-EVu>=CHah&U*H4tM;7UT6~_K-uw#&(<XPo9YF}%-{Fsy5J%uSS~4MyPESTprl8yd ziGi;LyH%Xpnx&6ar*EogjXtIs%&6c5H-A^4Dv?r{C**`gKilT~`yrG2-%!sog6mnU zdK~K`sx1zx{;psx)5Bwjc(pWf4n8*!eg+7LB1_yt!MV}{)U~<Bz|>I6k<dUGf~7R{ zjS?A@U80-2h3FTIO%}#B$540Pun0KW(f2CAeQfjJk9Bt!4_+noB7n%lhml!56bped zY9O0vVp}r&fhRZG=YeKPn#hS9U=?QP6jyRT>h!gq_bF{bp$`=s!%I7sR(=kuuDNb= z@YacR$KMG5`r`!7<2zGL5Z?GWd?w1I!SAaX)9NJC+~7ItN1`X=Re|ck#!5*NJ_j@2 z(kbwLs0EeDqGpc!Ejib;w+-ZivyEXGsK#RC%c0N=_bQY3)ifE~jKTFpJD=U7M)6{c zcuYs_8v<YebBg}~%Zi_|qH(-M_W*PRUK@<Pg9pWN@S19JWKdH2A=TZ0xeX(t`OU=A zodwd(!D<BbJ>c*<AVLh4V=st_J-A-y*pP#8ZU}DN5fL4|+4biRCh2I9X&fQpw@00s z4Musw#7_>bP9MB7nk8>SVDJHtU~!fn)~`aq2qyqxBk0d$1MN2KCpeRB3>vDP2Z>EO z`-aNy53vr&^w1jDF4hW$eVsU~^`6Wv2tliwt>*L;3Y?21vv|z)IgCv28orrlVOZtf z%wSzk<ayQ<+$&NyfYe0@-}(OE;wqBw+A35}fMCi-wTsUhq%xN|AzY0$_F_*>N|X8Y zaz7Y=U$5CUl1=D5J=k4j4kYuYsFP1s>?7@wM5Er93kHvrbdP}tXH~~wC0K!0X%4S0 z<7l{8CQ4HW>-fT8S6;kizF?V@a8+1JZYInONo%^h?pm0Yp~^YlZh1)$)3y)`_IVBJ ze~&avQ@ncI5F0sZPMDK<I{Pdw=)q7_YF2l(Ky?ARbQ6h?pj3(Wt_N<&1-G;OCeq|f zn2b<NB2amSlB9{>^u~@3EQH*$S_8Z0i<H+z?Cn#B>TZ#ak}j5mQ!aoL!o+MtY!>QT zJJ{&{!_&@nG&|d|OoPe|BVaHDdCfsN60_%<o^SW*^Z}zu_GLHUVoASW$YkWr6v;NQ zgK+A(K$LBy2oaRZ)8_lDArVB>kdpS0bhm9LBp@@g4VKEuKqdvdLzjaVO{3LXm17%Q z8$e28!eVP6maV>texdZ_ifG&eJ#K_N%u_B(aI-%u^V&1kO1~{e81#k=tIIQPIhIA8 zPCI(C#DB+RJM8yOy4#TJ^|Os3m!7Bn9(_x(pc812Iz?rDVMQ`-pA;3`=tXMAe?skU z#@Lk<hfs{hNy#-%s-mtQ@$nv~K$ww9WLih&z76y7JDplgHTC0j;_p_zt|cW<u%FEE z1L2??@&?hNUqJqiRzIR8@y36Gz1I7Ek`E1ssbV-_ghrSzKKl$>+&So>w2`$<?duIV zqICkHGYNxR?{50!sq>_DV$F&$;cP*qv`F;WwoA-Sj9?o-dpP0}c<a2IIyC?A2%}2d zsrwtm%}ChBsFuN&dYiUHVd>ryDPrl~2T`u!0K>JJxKR6(O0AKBa*U@{{*?Aeo>?EG zPu`O3wo>fGy>A$uc?&u)I$zI<Hx*_+h1X2?5tKsCh3+XP2(Z{j;L5K<FhlpUW6jAe zqu`AfSsW)7E*#15VM{kV*Q3@cOp3~U;-FKSDN0Oq=wp{Lz#OWBX545#P!#^du*=Eh z6;Ga2o0*9Z7Jbtj4A$=W0Cye4mF~Py$HFxl7a^a;7fAPK+YAbOC)8g-XE#*XyT47{ ziC_m+Y4sJ7(x>RB2cR*`=<^d+yiRP8U>-$2qfIV^Mt_Um^uapZ8hu=8$G;nP(a!s1 zsi4GJd^$guLSd~LK~OqK{8e~$@r^byG=q6}*V&&gY`&AnoF}r2=#x?1{xocLmyNFp zjJC<jjx20l=(UbuQ5JIOz8?PfeI5c6s@80+DMzgjcOhGWJEF+P3YGogDplLXbw&K} z1GU0=0>?8_vRRmmN!{mvRR)O>F<VE@JN6UFPu7b^I6iix`Q*({Dibz+xVjUy@3X<@ zy1+;I;SGot(_7=LrNG1)aUPS5IyhY(j3!^grwg#eyO>A7Q!2Y`4Z}4+6!BtcUAW+A zPRqg#)X9ZjZj(zY{tTVW^bdP28CL&Hta8MEr*Q75rkf&tlm6H&NCZ8Qy5&d+<qCy> z&%%P`qDJBjj4j_=ZI%#0n5R8}Q4uL+il4&w&m-<PL*)cB%E-n;A>InJ_ANPw0!Iu@ zVu4X~g)Z{(dF(|ucmO%~S=L4ns>dyvi?CoB@MLXh@ecNvvln*@LMD0j!o<m##LJOn zB-wkUr)Q&h1YeDIfLx}PnJSMh`>RN7Nou73u1NP=o&US|MDBP^?&hpYu^s|VOgalT z{!L5w<w)<A$KUisw;1L?Q2NhtejI6D%Ji88k=6gJI=rR(`vYjrhm5|F)S8n&e$9c& zsL_unR0gtjfiqEVY#FlIcuy5ZjYexmccwMa2c~o}*RES|cndW=KT}9xup9hx?>{ZC zygJ!%FiyGk_n%1(Q-x(bY{LPZoUeWg6`@zff2;;);}GNI=41@g1KlEeJP8FKOhk*> zjCrUx=`IL^z(i{zPH;d<dNUy<$FpWJXwCT2xJGx}p1ob5vJw_D(hilCc)60(4#VW! zxNrd@4{zCBW}KIjbA)Q26Y_Y(ys77Aes4cDN+7Bw9tFRvDY!pJ`0G?tWWqvM3fz)! zdL?Gsg)7LYPnPBw-o{#hz2VNKIoe33OGd<;WbH;CBa;8uu{fWy5|#5s<~A8J-*UwP zQ*|0;=)g!?NCiFJB}S_z#N_uL*@C9#Yb{{PWBT^TmF0pswGn#OEA))f%jF;B`AYrh z(<fqgq_5wcHQH2_;)*=+yuY9?V2fkfaOGP0_0`9h5?=tiLC1fcfMpvDngl+W^EbVF zNLf5$=!i=t=stTByk0U*c$Ay+i1pT-)X?gxc6@}z5HErp&ZtkjGN;|Kb%DjMBCRz# zA)APgh|BMX^y^UCa0UZA6C{1#8OfV7GU;=Rhq2%g_&KeiUEva}0yx}g{r9vEY3FKv zVGWYL6V2KNE^MaYbArGO=<YYY{xoFtt?$HF3dqmwuulO;Kf-11<48OUzH9btt)r9@ zTVtxih4xq7Ew%&~4kPmceC?0g+FGpjDp8#);bC%oqRDS(+DSJXIZR6VHJTX@n6yJ7 zJ$ESMl9SZ}yrG@++Yo12ntljpU(B?b9QFf!BIkaZ{>G_)b2+@D`liT#oDIIa8_dSX z5NY!_y{}dz={-dK1B?+((y_0Uy2$^>+Y$Zm){fS^=QOaV)7~|}XIlfylCOe=?-e;& zO1U9HkF<T=4pkx<Z>7qE@r<Eq+C$$hK>trx?cD!?Ry*UrXtkWeWBQAc8;b8j@v$XH z-TeVL>|*0tBHT$=RI>o@CZTuu+?4f)KyXj67U;DT65}$RXLHnPZqVHwJjSulZOsD& z?$(m3_J|iVX%W%!T}RE*-@kw5e>~&RE`zTLq6seuHkSdGc#705L_XfYUlS)|@s^tk zB8US0v$l+v6u4t<7jBCWR0dyhHxI>^cJkb4GO_{FuuHH)j?8C74McMGwU^P1CE9%| zqvCk_{q1>OVSj{F#`(3Zcs6J1nI+d?kQCu?gY=Ie8USfxk08(QKo^~)`2lqDK*BhX z^-}UdI95c%E>6e|bSqOJKEb35)Z1;P-=}Q5d_Ts)a@eZll1?JnD%w>z{Qoj<xgt0^ z2#E~yj}GR6``UBZweYj>SG-7$B-OaK3!L+)5X8>?bzM>?vn6<uXE9FqAq`eUYSw}0 zons*u{=yzd8=Vgh?TFL=zqpcarVs!pqTnQrHy1Xvz9ssL%bBVtPV*%AtIDFLF`u0$ zok{|aY*AhBne}Nf?$}!|qoT6?FAG(5vwut_(LkdQ(uLC=mq^m<!bSK}1aA+t)!q45 zcN?sAQ|c<XMuHXe+YI+XndFkdT3U4ai$}M>)ACayDcQ~cY_URIo_~ATeeWIjTpBh1 zP;XMvsqGWa&I|OAy=C8V;7pR%3T}b*Ee()0dP^|)VLrShq>bEGp(0TfrEtNo;C>rm z_PC*;a~*6=Vkp<*6Kje#81o2<mTD*qM_A^WEv&gBmgNYiqO!jyF-Epautj!WIsU>c zN($xtyX?|=nsuTcy4p8IZC#2pgmME(5^?OhJ^VB*byD<A&%KTJ)lF|D_jArYqIw@0 z7d#Kc=iBHw7Wm9e%_dMW2b#y9aZQ1)&|Qe%Rb%|wSWZ-5@@OWZj|q*du@->VrZ+)Z z5&Rds>74sY=$TQT<1N7GLfO+z^@6?zLr%M-W);%0(~STxJ9)^LVK`LMY7PGU|0-nT z*CVx`A=Vg?q!$OEi4g1RsfufHsc5ch17}uqmff2-*q(_dQ>#VX+Cai<+$z+!Jfk)H zaz3^v25FoyR`~jj*@LAoC*$RvqC1aEOG+P8${i(Rj=(uPV0B2Y_hR|ZS+#Z}7q~wk zx{{$6gRCT>=SQ`76J>NA7u0ri2P9>-2$a5Es9pFoH@($tt~N^OD3lgaP%ng#$VCrI z-i$VKA4{%kDMif9-L_iKT&5@HKd3XeS58OiO_Vty`0{oz>QK}WLzT-_<b>6oTJPde zFd5Jc96AyZTKTugB``mi>XUIgKV%ASZKCT$>avBOPjOLyf@T$Is&Jfwi)scZV`7}) zY1v<<j9p_-gY(cKgbJLzN!E9fC>yv(=0czk%ppl{kV<(iw0LuNqcUg^h}+^=&uIJ9 z0k;${Zk@YTHCmB)iWz(xvu?1OgI*u0e2ZJA?0T(@l<yw*3RL)a1FJr;r}&dmYYpe< z-O#At4Y1Fgqc5kJA^43N5YhN^<A@L>G#)Tl>#ECyGntNH1)6hS$~9y(NPTkUqsCwb zT482nmzAS%U;PNXBU5;t`$CSdc`5$e&o2A)^-ce#6MnjPPV)hu1;^G55Qo8Meb9h< zpt=I%Yko!QzxLq9b@#(ahHf@8`$RLzoCxK{X1}~l1iAf|I$&KT0DpOFQDITU<^$fb zEglsCPK5F!DOJzxw*T08f%uPy*3uF>1R~v2;iq2VIGYqK)=uG#L*@_*)o)#K&3t(E z7v3|;RmP!h>g?}*0o6n!j7CIzVXb$u(poyjm`RcsQERhJQPBnMVmWzsQ~01gVi3Ua zVF-+6)%M#|b2B<eqE6m%x|ugpzAv;g{uSRY|Lb|EXg3)8#x-Rx%4{K`-h{bSD?=DK zcBrxIVA`UyLG{$>f$Xnwi<6=VFd%c`G{v0V+Y$Mqe_cRdsLM?}rJ(nXWH&AHY#yb- zLj2E^?-=-ZZs9vC<Plwm+~Rz!26$~-ZMQo7cgpM<3x=<>4N#K=i1}K>@W>NVcD(yk zkOQF@9F_#OJqQ#td~0j0ItBJJ1+{hOzx-BRm-$Xkut}4$!C_*5PTSfIuI8g5)4sIe zvp+!p=^@;h8%S>=-h)iuWzs2f8_KAMN+kUNB|!)`7U7z)z@9A@SI9;|FNgIL<~5iZ zZZ{cjzyAqno!HMAVHv9yy0CI-m&Y`{4q1LmTiP=B@Uo`W?DtzJbx0CP`VV1#jj%`< z4-r4d%CrRS9GOgGM7;o+qIc?CUhj}s^!kqQ$Z8KVR&no8n0a;e#aP=t>Xtp7>pt=J zBcaMN&6?w$As0wQp%e&6U%h;5M>VUEGBf;P5lh&UA-qYzmbYjv*2_rPstTp;L%f?Z zj}#iD*{a>uF0gsXI3SDU#%l>@QBea3>@|sFaQ2|>fkq!+cRN+Q=rZ51e>r1Jxxk(e z=F4GT_XFCf(Tr28OYnjrJ>D0j6V48Z&;LSE9t)t=8?I&_GOUpLejWa*{-(F}xpoQs z1N?`&xmq)b)qkSS^?E`x8a|VG46}H^h<7s_JRdPj5Gp&YwS`yK0NZG{NQd}5f@id& z0;6uP0)0@glg|~uRF%Aeg7k4!75~OGZPb4}5WhgQo#&7D3e^X-+t}dlbwSuu@!!M2 z{~HZJO%X0-8S53r5Ud37)$o_SR4ZFmTM}-e^+54E_Kn=69(u{J``WIWRaEnyc)5bN zgmJF5N8PZ!w9_p}R!!L7E?ED&qtT|}$>UKci^bOik+21CFCeKg2t|mVAZv4-2S4ck zGH4>{LU_GCHUEHHHmC9LEFtR6p059_mQ=b|uxs()N@l3u-;qo8$VanD=i#vfzllbO z6M|Yf7&0=?s+?P*g@EoZ0mx?qa%s}(={;=k2bm<!Z55iouSdIL(1^Z=Vt9nXZt`93 z(sFrx)Qqw5=c-E4Xyj3&gqwHY42ySW9iOOt_v+ph{5K-!=*aYy2yZk<-3QOgaf~fq zp}vFF!5T)C9D}-1eeDs|RlaKOsB9v&X2A$$LF8saQ;l6$@wmh+Cb7v0nWtTS+QGB` zkI7FO&&O4rD=RziKA}2QKS|g${U9mG9^2@;3!A0+MY|m92Kkd$6<C#&63?=4*Tw+f zQ&UHr<!GOPOjabh?Uwm#!m0J`0gU1=a_eNgCC6Wp?!)mbsxD5i;HY0)?4sHF9A0vP zbr85@$mcW~2+O|otR+ppCYGflRwj5k2|WTQvU6!21%+hWg1H=AEkL%wnN<3r8TNdQ zDLK9}4+<MUT9W%LRd9Vl45i84wxjGa;$~}>q+J`C%gEoRq9sR0_ZhKT4$li5cB#qF zr<$M8rdOKu+{8>^a>yNn)a?2W;1ocYN9YGqI3fTdH|vpd+b}0zeBT)j084KS1{3kJ z56usCQr?YkddCR8hBEHQ`I<FaGu^eCc>`G7ek=w((BPc0`eq^H@g(93A4bhc;PVt# z_9}8x<aud=xWlFhY06$Ea!fCIR9@ccP*xM*{7ZR7r_%Ng`GLC#mM+QZo8H7E3IE~3 zI$vy>NTXSCFQwAi_dxT%zrAP){PkKBGL2{<ls@w^4#Y|aTWM0VvNNB#5!+FK#VWi} zV#BBv)2fRZBL{uTAx?^AJx2Fd-OO*4%&||p+wtl6Q+}tTO{%HLJ9GEd+Ax@urqb{C z>mjQkquW9H>Y1oD=HPA9!0kS7k^=epT>hjt#MR%C=fO2UAibuT0Z8<$GtsR<>NdbS zZioK?RDZBcWQ)0jL}fHhB^F6prJ;kcPxTmtI--$1YcU7Qye)atkCFXUb`|u&wnZ=Q zGtO?mWk>t)`c#p_R~Qbq-QZfieK-`!m&9y_m{ADJHbh_zulDJ39PGvHTBPijF1u!R zAV&TLr8gEJ#*Eb;<m3eA;iEGDL+hB!?uYorRHQJ8XJyr3(co<A_XsiLs9kRCKH=i$ z*I(2%v6T4kAVpaK88_t9H^@jG2)I5Yo#g6%%{Ji;Dz<)4h_%0D3|z$_Rg1lX?fug5 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 Qe}o{g!}Xr);J>N=17GLbd;kCd From ea6d811b5891b3a7876d45720ada16e375b7a73c Mon Sep 17 00:00:00 2001 From: Yuanheng Zhao <54058983+yuanheng-zhao@users.noreply.github.com> Date: Sat, 30 May 2026 16:13:21 +0800 Subject: [PATCH 103/112] [Config Refactor] Migrate Ming-flash-omni-2.0 Image-Gen deploy configs (#3975) Signed-off-by: yuanheng <jonathan.zhaoyh@gmail.com> Co-authored-by: Zheng Wengang <zwg0606@gmail.com> --- .../ming_flash_omni/README.md | 2 +- .../online_serving/ming_flash_omni/README.md | 35 +++---- .../run_curl_multimodal_generation.sh | 2 +- tests/test_config_factory.py | 68 +++++++++++++ vllm_omni/config/pipeline_registry.py | 4 + vllm_omni/deploy/ming_flash_omni_image.yaml | 63 ++++++++++++ .../models/ming_flash_omni/ming_flash_omni.py | 8 +- .../models/ming_flash_omni/pipeline.py | 34 +++++++ .../stage_configs/ming_flash_omni_dual.yaml | 98 ------------------- 9 files changed, 189 insertions(+), 125 deletions(-) create mode 100644 vllm_omni/deploy/ming_flash_omni_image.yaml delete mode 100644 vllm_omni/model_executor/stage_configs/ming_flash_omni_dual.yaml diff --git a/examples/offline_inference/ming_flash_omni/README.md b/examples/offline_inference/ming_flash_omni/README.md index 83259a021..54e993cab 100644 --- a/examples/offline_inference/ming_flash_omni/README.md +++ b/examples/offline_inference/ming_flash_omni/README.md @@ -8,7 +8,7 @@ vLLM-Omni supports three deployment modes: |------|--------------|--------| | Thinker + Talker (omni-speech, default) | `vllm_omni/deploy/ming_flash_omni.yaml` | Text + Audio | | Thinker only (multimodal understanding) | `vllm_omni/deploy/ming_flash_omni_thinker_only.yaml` | Text | -| Thinker + Imagegen (text-to-image / img2img) | `vllm_omni/model_executor/stage_configs/ming_flash_omni_dual.yaml` | Image (online-serving only at the moment) | +| Thinker + Imagegen (text-to-image / img2img) | `vllm_omni/deploy/ming_flash_omni_image.yaml` | Image (online-serving only at the moment) | For standalone TTS (talker only), see the [Ming-flash-omni-TTS section in the Text-To-Speech hub](../text_to_speech/README.md#ming-flash-omni-tts). diff --git a/examples/online_serving/ming_flash_omni/README.md b/examples/online_serving/ming_flash_omni/README.md index 7b538efe8..76b0bef9d 100644 --- a/examples/online_serving/ming_flash_omni/README.md +++ b/examples/online_serving/ming_flash_omni/README.md @@ -10,7 +10,7 @@ Please refer to [README.md](../../../README.md) |------|---------------|--------| | Thinker + Talker (omni-speech, default) | `vllm serve ... --omni` | Text + Audio | | Thinker only (multimodal understanding) | `vllm serve ... --omni --deploy-config vllm_omni/deploy/ming_flash_omni_thinker_only.yaml` | Text | -| Thinker + Imagegen (text-to-image / img2img) | `vllm serve ... --omni --stage-configs-path vllm_omni/model_executor/stage_configs/ming_flash_omni_dual.yaml` | Image | +| Thinker + Imagegen (text-to-image / img2img) | `vllm serve ... --omni --deploy-config vllm_omni/deploy/ming_flash_omni_image.yaml` | Image | For standalone TTS (talker only), see the [Ming-flash-omni-TTS section in the Text-To-Speech hub](../text_to_speech/README.md#ming-flash-omni-tts). @@ -63,34 +63,27 @@ bash run_curl_multimodal_generation.sh use_image_gen ## Image generation (text-to-image) Ming-flash-omni-2.0 also exposes an image-generation (diffusion) stage. -Launch with the dual-stage YAML, which adds an image-gen stage behind the -thinker: +Launch with the image deploy YAML, which adds an image-gen stage behind +the thinker: ```bash vllm serve Jonathan1909/Ming-flash-omni-2.0 --omni \ - --stage-configs-path vllm_omni/model_executor/stage_configs/ming_flash_omni_dual.yaml \ + --deploy-config vllm_omni/deploy/ming_flash_omni_image.yaml \ --stage-init-timeout 1800 \ - --port 8188 + --init-timeout 1800 \ + --port 8091 ``` Then request image output by passing `"modalities": ["image"]`: ```bash -curl http://127.0.0.1:8188/v1/chat/completions \ +curl -s http://127.0.0.1:8091/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "Jonathan1909/Ming-flash-omni-2.0", - "messages": [{"role": "user", "content": "Draw a beautiful girl with short black hair and red dress."}], + "messages": [{"role": "user", "content": "Please draw a cute cat."}], "modalities": ["image"] - }' -o /tmp/ming_response.json - -python - <<'PY' -import base64, json -r = json.load(open('/tmp/ming_response.json')) -url = r['choices'][0]['message']['content'][0]['image_url']['url'] -open('/tmp/ming_gen.png', 'wb').write(base64.b64decode(url.split(',', 1)[1])) -print('saved /tmp/ming_gen.png') -PY + }' | jq -r '.choices[0].message.content[0].image_url.url | split(",")[1]' | base64 -d > ming_imagegen.png ``` ### Optional knobs @@ -109,7 +102,7 @@ Pass image-gen overrides via the diffusion-stage `sampling_params_list[1].extra_ Example with all the knobs: ```bash -curl http://127.0.0.1:8188/v1/chat/completions \ +curl http://127.0.0.1:8091/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "Jonathan1909/Ming-flash-omni-2.0", @@ -122,7 +115,7 @@ curl http://127.0.0.1:8188/v1/chat/completions \ "byte5_text":["理解与生成统一"]}}} ], "messages": [{"role": "user", "content": "Draw a poster."}] - }' -o /tmp/ming_response.json + }' | jq -r '.choices[0].message.content[0].image_url.url | split(",")[1]' | base64 -d > ming_imagegen_knobs.png ``` ### img2img (reference image) @@ -142,9 +135,9 @@ into the diffusion stage as `extra[reference_image]`: ### GPU layout -The shipped `ming_flash_omni_dual.yaml` allocates the thinker on GPUs 0–3 +The shipped `ming_flash_omni_image.yaml` allocates the thinker on GPUs 0–3 (TP=4) and the diffusion stage on GPU 4 (TP=1). Copy the YAML and edit -`runtime.devices` per stage to relocate; with fewer GPUs available, drop +`devices` per stage to relocate; with fewer GPUs available, drop the thinker TP to 2 and run the diffusion stage on a free card. Image-gen warmup takes roughly an extra 30–60 s on top of the thinker — set `--stage-init-timeout 1800` if the default 300 s is too tight. @@ -156,7 +149,7 @@ warmup takes roughly an extra 30–60 s on top of the thinker — set | `["text"]` or omitted | Thinker only | Text | | `["audio"]` | Thinker + Talker | Audio (speech) | | `["text", "audio"]` | Thinker + Talker | Text + Audio | -| `["image"]` | Thinker + Imagegen (dual YAML) | Image (PNG, base64 in `choices[0].message.content`) | +| `["image"]` | Thinker + Imagegen (image deploy YAML) | Image (PNG, base64 in `choices[0].message.content`) | For ready-to-copy curl examples (text / audio / multimodal input, SSE streaming, reasoning mode), see the recipe at diff --git a/examples/online_serving/ming_flash_omni/run_curl_multimodal_generation.sh b/examples/online_serving/ming_flash_omni/run_curl_multimodal_generation.sh index 3b8f4ddc1..92bb32c47 100755 --- a/examples/online_serving/ming_flash_omni/run_curl_multimodal_generation.sh +++ b/examples/online_serving/ming_flash_omni/run_curl_multimodal_generation.sh @@ -103,7 +103,7 @@ echo "" if [[ "$QUERY_TYPE" == "use_image_gen" ]]; then # Image-gen branch: dual-stage diffusion endpoint, modalities=["image"]. - # The shipped ming_flash_omni_dual.yaml runs the thinker on cards 0-3 + # The shipped ming_flash_omni_image.yaml runs the thinker on cards 0-3 # (TP=4) and the diffusion stage on card 4 (TP=1). See the image-gen # section in README.md for knobs and img2img. out_path="${OUT_PATH:-/tmp/ming_imagegen.png}" diff --git a/tests/test_config_factory.py b/tests/test_config_factory.py index 0db1d4fba..e96f79f89 100644 --- a/tests/test_config_factory.py +++ b/tests/test_config_factory.py @@ -1580,6 +1580,74 @@ class TestMingFlashOmniPipeline: assert len(stages) == 1 assert stages[0].yaml_engine_args["model_arch"] == "MingFlashOmniForConditionalGeneration" + def test_image_pipeline_registered(self): + p = _PIPELINE_REGISTRY.get("ming_flash_omni_image") + assert p is not None + assert p.model_arch == "MingFlashOmniForConditionalGeneration" + assert len(p.stages) == 2 + assert p.validate() == [] + + def test_image_thinker_stage(self): + s = _PIPELINE_REGISTRY["ming_flash_omni_image"].get_stage(0) + assert s.model_stage == "thinker" + assert s.execution_type == StageExecutionType.LLM_AR + assert s.input_sources == () + assert s.final_output is False + assert s.owns_tokenizer is True + assert s.requires_multimodal_data is True + # Image variant exports hidden states for the diffusion stage. + assert s.engine_output_type == "latent" + assert s.hf_config_name == "thinker_config" + assert s.sampling_constraints["detokenize"] is False + assert s.prompt_expand_func is not None + + def test_image_dit_stage(self): + s = _PIPELINE_REGISTRY["ming_flash_omni_image"].get_stage(1) + assert s.model_stage == "dit" + assert s.execution_type == StageExecutionType.DIFFUSION + assert s.input_sources == (0,) + assert s.final_output is True + assert s.final_output_type == "image" + assert s.hf_config_name == "image_gen_config" + assert s.model_arch == "MingImagePipeline" + assert s.custom_process_input_func is not None + + def test_image_processor_wiring_resolves(self): + """The prompt_expand_func and custom_process_input_func strings must point to real callables.""" + thinker = _PIPELINE_REGISTRY["ming_flash_omni_image"].get_stage(0) + dit = _PIPELINE_REGISTRY["ming_flash_omni_image"].get_stage(1) + for ref in (thinker.prompt_expand_func, dit.custom_process_input_func): + module_path, _, attr = ref.rpartition(".") + module = importlib.import_module(module_path) + assert callable(getattr(module, attr)) + + def test_image_yaml_loads_and_merges(self): + """deploy/ming_flash_omni_image.yaml parses and routes to the image pipeline.""" + deploy_path = Path(__file__).parent.parent / "vllm_omni" / "deploy" / "ming_flash_omni_image.yaml" + if not deploy_path.exists(): + pytest.skip("ming_flash_omni_image deploy yaml not found") + + deploy = load_deploy_config(deploy_path) + assert len(deploy.stages) == 2 + assert deploy.async_chunk is False + assert deploy.pipeline == "ming_flash_omni_image" + assert deploy.connectors is not None + assert "shared_memory_connector" in deploy.connectors + + pipeline = _PIPELINE_REGISTRY["ming_flash_omni_image"] + stages = merge_pipeline_deploy(pipeline, deploy) + assert len(stages) == 2 + # Stage 0 thinker: AR worker that emits latents. + assert stages[0].yaml_engine_args["model_arch"] == "MingFlashOmniForConditionalGeneration" + assert stages[0].yaml_engine_args["engine_output_type"] == "latent" + assert stages[0].yaml_extras["default_sampling_params"]["detokenize"] is False + assert stages[0].yaml_extras["prompt_expand_func"] is not None + # Stage 1 dit: diffusion stage with MingImagePipeline. + assert stages[1].yaml_engine_args["model_arch"] == "MingImagePipeline" + assert stages[1].custom_process_input_func is not None + assert stages[1].final_output is True + assert stages[1].final_output_type == "image" + class TestBaseConfigInheritance: """Test deploy YAML base_config inheritance.""" diff --git a/vllm_omni/config/pipeline_registry.py b/vllm_omni/config/pipeline_registry.py index 4a8f48de6..140bddf45 100644 --- a/vllm_omni/config/pipeline_registry.py +++ b/vllm_omni/config/pipeline_registry.py @@ -117,6 +117,10 @@ _OMNI_PIPELINES: dict[str, tuple[str, str]] = { "vllm_omni.model_executor.models.ming_flash_omni.pipeline", "MING_FLASH_OMNI_THINKER_ONLY_PIPELINE", ), + "ming_flash_omni_image": ( + "vllm_omni.model_executor.models.ming_flash_omni.pipeline", + "MING_FLASH_OMNI_IMAGE_PIPELINE", + ), "moss_tts_nano": ( "vllm_omni.model_executor.models.moss_tts_nano.pipeline", "MOSS_TTS_NANO_PIPELINE", diff --git a/vllm_omni/deploy/ming_flash_omni_image.yaml b/vllm_omni/deploy/ming_flash_omni_image.yaml new file mode 100644 index 000000000..9f7c6a34d --- /dev/null +++ b/vllm_omni/deploy/ming_flash_omni_image.yaml @@ -0,0 +1,63 @@ +# Ming-flash-omni-2.0 deploy: thinker + image-generation (text-to-image / img2img). +# +# Stage 0: Thinker — multimodal understanding -> final-layer hidden states. +# TP=4 across GPUs 0-3. +# Stage 1: DiT (MingImagePipeline) — condition encoder + FlowMatchEuler +# denoise + VAE decode. Diffusion worker, runs eager on GPU 4. +# +# Cross-stage transport: shared_memory_connector (intra-node). +# +# Fields omitted from a stage fall back to StageDeployConfig dataclass +# defaults (see vllm_omni/config/stage_config.py). +# * Stage 0 max_num_seqs=2 leaves room for the CFG-text companion that +# `expand_cfg_prompts` (pipeline-level) spawns when a request carries +# a negative prompt — positive and negative hidden states both reach +# the diffusion stage. +# * Image-gen warmup adds latency on top of the thinker; +# pass `--stage-init-timeout <seconds>` if the default 300s is tight. +pipeline: ming_flash_omni_image +async_chunk: false + +trust_remote_code: true +distributed_executor_backend: mp +enable_prefix_caching: false + +stages: + - stage_id: 0 + # max_num_seqs=2: parent + 1 CFG-text companion (negative prompt). + max_num_seqs: 2 + gpu_memory_utilization: 0.85 + enforce_eager: true + tensor_parallel_size: 4 + devices: "0,1,2,3" + output_connectors: + to_stage_1: shared_memory_connector + default_sampling_params: + temperature: 0.4 + top_p: 0.9 + top_k: 1 + max_tokens: 1 # image-gen request: prefill only, no AR decode + seed: 42 + + - stage_id: 1 + enforce_eager: true + devices: "4" + input_connectors: + from_stage_0: shared_memory_connector + default_sampling_params: + seed: 42 + +connectors: + shared_memory_connector: + name: SharedMemoryConnector + extra: + shm_threshold_bytes: 65536 # 64KB threshold + +edges: + - from: 0 + to: 1 + window_size: -1 + max_inflight: 1 + +# Ming-flash-omni-2.0 has only been manually verified on CUDA devices +platforms: {} diff --git a/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni.py b/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni.py index fd52dcbf8..4e3da735a 100644 --- a/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni.py +++ b/vllm_omni/model_executor/models/ming_flash_omni/ming_flash_omni.py @@ -128,12 +128,12 @@ class MingFlashOmniForConditionalGeneration( elif self.model_stage == "imagegen": # Image generation is a separate diffusion stage; it does not run # through this AR wrapper. See - # ``vllm_omni/diffusion/models/ming_flash_omni/pipeline_ming_imagegen.py`` - # and ``model_arch: MingImagePipeline`` in the stage YAML. + # `vllm_omni/diffusion/models/ming_flash_omni/pipeline_ming_imagegen.py` + # and `model_arch: MingImagePipeline` in the deploy YAML. raise NotImplementedError( "Image generation stage is not implemented in this AR wrapper. " - "Use stage_type: diffusion with model_arch: MingImagePipeline " - "for the imagegen stage (see stage_configs/ming_flash_omni.yaml)." + "Use the DIFFUSION execution_type with model_arch: MingImagePipeline " + "for the imagegen stage (see vllm_omni/deploy/ming_flash_omni_image.yaml)." ) else: diff --git a/vllm_omni/model_executor/models/ming_flash_omni/pipeline.py b/vllm_omni/model_executor/models/ming_flash_omni/pipeline.py index a1e1ef469..7ebbfd9d8 100644 --- a/vllm_omni/model_executor/models/ming_flash_omni/pipeline.py +++ b/vllm_omni/model_executor/models/ming_flash_omni/pipeline.py @@ -105,3 +105,37 @@ MING_FLASH_OMNI_THINKER_ONLY_PIPELINE = PipelineConfig( ), ), ) + + +# Thinker + image-generation (diffusion) variant: text-to-image / img2img. +MING_FLASH_OMNI_IMAGE_PIPELINE = PipelineConfig( + model_type="ming_flash_omni_image", + model_arch="MingFlashOmniForConditionalGeneration", + stages=( + StagePipelineConfig( + stage_id=0, + model_stage="thinker", + execution_type=StageExecutionType.LLM_AR, + input_sources=(), + final_output=False, + owns_tokenizer=True, + requires_multimodal_data=True, + hf_config_name="thinker_config", + engine_output_type="latent", + prompt_expand_func=f"{_PROC}.expand_cfg_prompts", + sampling_constraints={"detokenize": False}, + ), + StagePipelineConfig( + stage_id=1, + model_stage="dit", + # Resolved in vllm_omni/diffusion/registry.py. + model_arch="MingImagePipeline", + execution_type=StageExecutionType.DIFFUSION, + input_sources=(0,), + final_output=True, + final_output_type="image", + hf_config_name="image_gen_config", + custom_process_input_func=f"{_PROC}.thinker2imagegen", + ), + ), +) diff --git a/vllm_omni/model_executor/stage_configs/ming_flash_omni_dual.yaml b/vllm_omni/model_executor/stage_configs/ming_flash_omni_dual.yaml deleted file mode 100644 index 14e00532f..000000000 --- a/vllm_omni/model_executor/stage_configs/ming_flash_omni_dual.yaml +++ /dev/null @@ -1,98 +0,0 @@ -# Dual-stage single-node deployment of Ming-flash-omni-2.0 text-to-image. -# -# Stage 0 (thinker, llm): BailingMoeV2 LLM + vision/audio encoders, TP=2 -# on cards 0 and 1. Detects text-to-image requests -# (auto-appended with <image><imagePatch>*256</image> -# by serving_chat), replaces those positions with -# learnable ``query_tokens_dict`` embeddings, runs -# forward, and exports the final-layer hidden states -# under ``multimodal_output["final_hidden_states"]``. -# -# Stage 1 (imagegen, diffusion): Qwen2 connector + ZImage DiT + VAE, TP=1 on -# card 3. Consumes the thinker hidden states -# shipped from stage 0 (sliced at <imagePatch> -# positions by ``thinker2imagegen``) as DiT -# conditioning, runs the Ming condition -# encoder + 30-step FlowMatchEuler denoise -# + VAE decode, emits a raw image tensor. -# -# Cross-stage transport: ``shared_memory_connector`` (intra-node). The -# thinker's ``multimodal_output`` flows automatically; ``thinker2imagegen`` -# (custom_process_input_func) slices it and produces a list[dict] with the -# conditioning tensor under ``extra[thinker_hidden_states]``. Msgpack -# (``OmniMsgpackEncoder``) handles bf16/fp16 tensors natively. -# -# TODO(ming-flash-omni-2.0): stage 0 `thinker_config` / stage 1 `image_gen_config` target the -# future unified MingFlashOmniConfig; today's BailingMM2Config checkpoint silently falls back. - -stage_args: - - stage_id: 0 - stage_type: llm - # CFG prompt expansion: when a request carries ``negative_prompt``, one - # companion is spawned so both positive and negative hidden states reach - # the diffusion stage (consumed by MingImagePipeline as real CFG negatives). - prompt_expand_func: vllm_omni.model_executor.stage_input_processors.ming_flash_omni.expand_cfg_prompts - hf_config_name: thinker_config - runtime: - devices: "0,1,2,3" - engine_args: - model_stage: thinker - max_num_seqs: 2 # parent + 1 CFG-text companion (negative prompt) - model_arch: MingFlashOmniForConditionalGeneration - worker_type: ar - scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler - gpu_memory_utilization: 0.85 - enforce_eager: true - trust_remote_code: true - engine_output_type: latent - distributed_executor_backend: "mp" - enable_prefix_caching: false - tensor_parallel_size: 4 - is_comprehension: true - default_sampling_params: - temperature: 0.4 - top_p: 0.9 - top_k: 1 - max_tokens: 1 # image-gen request: prefill only, no AR decode - seed: 42 - detokenize: false - output_connectors: - to_stage_1: shared_memory_connector - - - stage_id: 1 - stage_type: diffusion - hf_config_name: image_gen_config - runtime: - devices: "4" - engine_args: - model_stage: dit - model_arch: MingImagePipeline # resolved in vllm_omni/diffusion/registry.py - enforce_eager: true - trust_remote_code: true - distributed_executor_backend: "mp" - engine_input_source: [0] - custom_process_input_func: vllm_omni.model_executor.stage_input_processors.ming_flash_omni.thinker2imagegen - final_output: true - final_output_type: image - is_comprehension: false - default_sampling_params: - seed: 42 - input_connectors: - from_stage_0: shared_memory_connector - -runtime: - enabled: true - defaults: - window_size: -1 - max_inflight: 1 - - connectors: - shared_memory_connector: - name: SharedMemoryConnector - extra: - shm_threshold_bytes: 65536 # 64KB threshold - - edges: - - from: 0 - to: 1 - window_size: -1 From 802a6242f03b9d501d7ec513b5c16e3c9a7da459 Mon Sep 17 00:00:00 2001 From: wuhang <whlbx@hotmail.com> Date: Sat, 30 May 2026 16:18:48 +0800 Subject: [PATCH 104/112] [Bugfix][Tests] Remove unnecessary device map in tests init (#3958) Signed-off-by: WU Hang <whlbx@hotmail.com> Co-authored-by: Alicia <115451386+congw729@users.noreply.github.com> --- tests/helpers/runtime.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/helpers/runtime.py b/tests/helpers/runtime.py index 789ed229a..a3d337e1b 100644 --- a/tests/helpers/runtime.py +++ b/tests/helpers/runtime.py @@ -531,10 +531,6 @@ class OmniServerStageCli(OmniServer): if self.env_dict is not None: env.update(self.env_dict) - devices = self.stage_runtime_devices.get(stage_id) - if devices: - self._set_stage_device_env(stage_id, env, devices, replica_id=replica_id) - cmd = self._build_stage_cmd(stage_id, headless=headless, replica_id=replica_id) print(f"Launching OmniServerStageCli stage {stage_id} replica {replica_id}: {' '.join(cmd)}") # Capture each subprocess's stdout+stderr to a per-stage log file so From 8d13b54383abdd82a2f049a5e7da302580f1ae7f Mon Sep 17 00:00:00 2001 From: Alex Brooks <albrooks@redhat.com> Date: Sat, 30 May 2026 02:59:42 -0600 Subject: [PATCH 105/112] [CI/Bugfix] Async Request ID Aliasing (#3953) Signed-off-by: Alex Brooks <albrooks@redhat.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com> --- .buildkite/test-merge.yml | 74 +++---- tests/entrypoints/test_async_omni.py | 193 ++++++++++++++++-- tests/entrypoints/test_omni_entrypoints.py | 26 ++- vllm_omni/entrypoints/async_omni.py | 58 +++++- vllm_omni/entrypoints/client_request_state.py | 8 +- 5 files changed, 296 insertions(+), 63 deletions(-) diff --git a/.buildkite/test-merge.yml b/.buildkite/test-merge.yml index 5e75f58ed..318b872b5 100644 --- a/.buildkite/test-merge.yml +++ b/.buildkite/test-merge.yml @@ -106,43 +106,43 @@ steps: - group: ":card_index_dividers: Entrypoints Test" depends_on: upload-merge-pipeline steps: - # - label: "Entrypoint Test with H100" - # timeout_in_minutes: 35 - # commands: - # - pytest -s -v tests/entrypoints/ -m "advanced_model and cuda and H100" --run-level "advanced_model" - # agents: - # queue: "mithril-h100-pool" - # plugins: - # - kubernetes: - # podSpec: - # containers: - # - image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT - # resources: - # limits: - # nvidia.com/gpu: 2 - # volumeMounts: - # - name: devshm - # mountPath: /dev/shm - # - name: hf-cache - # mountPath: /root/.cache/huggingface - # env: - # - name: HF_HOME - # value: /root/.cache/huggingface - # - name: HF_TOKEN - # valueFrom: - # secretKeyRef: - # name: hf-token-secret - # key: token - # nodeSelector: - # node.kubernetes.io/instance-type: gpu-h100-sxm - # volumes: - # - name: devshm - # emptyDir: - # medium: Memory - # - name: hf-cache - # hostPath: - # path: /mnt/hf-cache - # type: DirectoryOrCreate + - label: "Entrypoint Test with H100" + timeout_in_minutes: 35 + commands: + - pytest -s -v tests/entrypoints/ -m "advanced_model and cuda and H100" --run-level "advanced_model" + agents: + queue: "mithril-h100-pool" + plugins: + - kubernetes: + podSpec: + containers: + - image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT + resources: + limits: + nvidia.com/gpu: 2 + volumeMounts: + - name: devshm + mountPath: /dev/shm + - name: hf-cache + mountPath: /root/.cache/huggingface + env: + - name: HF_HOME + value: /root/.cache/huggingface + - name: HF_TOKEN + valueFrom: + secretKeyRef: + name: hf-token-secret + key: token + nodeSelector: + node.kubernetes.io/instance-type: gpu-h100-sxm + volumes: + - name: devshm + emptyDir: + medium: Memory + - name: hf-cache + hostPath: + path: /mnt/hf-cache + type: DirectoryOrCreate - label: "Entrypoint Test with L4" timeout_in_minutes: 30 diff --git a/tests/entrypoints/test_async_omni.py b/tests/entrypoints/test_async_omni.py index c8ef5eddf..5d7176490 100644 --- a/tests/entrypoints/test_async_omni.py +++ b/tests/entrypoints/test_async_omni.py @@ -1,12 +1,19 @@ import asyncio +import re from types import SimpleNamespace import pytest from vllm.sampling_params import RequestOutputKind, SamplingParams +from tests.helpers.mark import hardware_test +from tests.helpers.stage_config import get_deploy_config_path from vllm_omni.entrypoints.async_omni import AsyncOmni -pytestmark = [pytest.mark.core_model, pytest.mark.cpu] +pytestmark = [pytest.mark.core_model] + +DIFFUSION_MODEL = "riverclouds/qwen_image_random" +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): @@ -59,6 +66,89 @@ def get_async_omni_instance(fake_add_request=_noop, fake_abort_request=_noop) -> return omni +@pytest.mark.cpu +def test_generate_submits_randomized_id_to_engine(): + """Ensure the engine receives a UUID-suffixed ID, not the raw request ID""" + + async def run(): + submitted_ids = [] + omni = get_async_omni_instance(fake_add_request=get_fake_add_request(submitted_ids)) + + req_id = "my-req-1" + async for _ in omni.generate( + prompt={"prompt": "test"}, + request_id=req_id, + sampling_params_list=[SimpleNamespace()], + output_modalities=["text"], + ): + pass + + assert len(submitted_ids) == 1 + assert submitted_ids[0] != req_id + assert submitted_ids[0].startswith(f"{req_id}-") + + asyncio.run(run()) + + +@pytest.mark.cpu +@pytest.mark.parametrize( + "req_ids,cancel_prefix,expected_cancel_count", + [ + (["cancel-me"], "cancel-me", 1), + (["cancel-me", "cancel-me"], "cancel-me", 2), + (["cancel-hello", "cancel-hello-world"], "cancel-hello", 1), + ], +) +def test_abort_handles_internal_request_mapping(req_ids: list[str], cancel_prefix: str, expected_cancel_count: int): + """Ensure that abort() with the user-visible ID resolves correctly. + + NOTE: In the case of concurrent / colliding request(s), all requests matching the + user provided request ID will be aborted.""" + + async def run(): + aborted_batches = [] + omni = get_async_omni_instance( + fake_abort_request=get_fake_abort(aborted_batches), + ) + + async def exhaust(agen): + async for _ in agen: + pass + + tasks = [] + for user_request_id in req_ids: + t = asyncio.create_task( + exhaust( + omni.generate( + prompt={"prompt": "test"}, + request_id=user_request_id, + sampling_params_list=[SimpleNamespace()], + ) + ) + ) + await asyncio.sleep(0) + tasks.append(t) + + assert len(omni.request_states) == len(req_ids) + await omni.abort(cancel_prefix) + + assert len(aborted_batches) == 1 + aborted_ids = aborted_batches[0] + # Aborted requests will have fmt {ext_id}-{UUID} to avoid collisions + for rid in aborted_ids: + assert re.fullmatch(rf"{re.escape(cancel_prefix)}-[0-9a-f]+", rid) + assert len(aborted_ids) == expected_cancel_count + assert len(set(aborted_ids)) == expected_cancel_count + assert len(omni.request_states) == len(req_ids) - expected_cancel_count + + for t in tasks: + t.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + + asyncio.run(run()) + + +@pytest.mark.cpu def test_generate_accepts_request_after_repeated_cancellations(): async def run_test(): submitted_request_ids = [] @@ -91,22 +181,23 @@ def test_generate_accepts_request_after_repeated_cancellations(): await task assert len(await collect_outputs("after-cancel")) == 1 - assert submitted_request_ids == [ - "baseline", - "cancel-0", - "cancel-1", - "cancel-2", - "after-cancel", - ] - assert aborted_request_batches == [ - ["cancel-0"], - ["cancel-1"], - ["cancel-2"], - ] + + # Check prefixes instead of equality since generate will add + # a random numeric suffix to ensure the request ID is unique + expected_prefixes = ["baseline-", "cancel-0-", "cancel-1-", "cancel-2-", "after-cancel-"] + assert len(submitted_request_ids) == len(expected_prefixes) + for submitted, prefix in zip(submitted_request_ids, expected_prefixes): + assert submitted.startswith(prefix) + + assert len(aborted_request_batches) == 3 + for batch, prefix in zip(aborted_request_batches, ["cancel-0-", "cancel-1-", "cancel-2-"]): + assert len(batch) == 1 + assert batch[0].startswith(prefix) asyncio.run(run_test()) +@pytest.mark.cpu @pytest.mark.parametrize( "output_kind", [RequestOutputKind.DELTA, RequestOutputKind.FINAL_ONLY, RequestOutputKind.CUMULATIVE] ) @@ -132,3 +223,79 @@ def test_output_kind_is_preserved_with_explicit_sampling_params(output_kind): asyncio.run(run()) assert captured_params[0].output_kind == output_kind + + +# End to end tests for ensuring internal manipulation of request ID +# in diffusion / Omni models don't leak back to the user. +# +# NOTE: It seems like we currently need the shutdowns here, otherwise +# running the tests sequentially seems to leave a zombie process in diffusion +# that can OOM the Omni tests. +@hardware_test(res={"cuda": "L4"}, num_cards=1) +@pytest.mark.omni +@pytest.mark.asyncio +@pytest.mark.parametrize( + "req_id", + ["my-req-1", "img_gen-abc123", "chatcmpl-xyz"], + ids=["plain", "prefixed-img", "prefixed-chat"], +) +async def test_diffusion_generate_preserves_request_id(req_id): + """Ensure diffusion model requests don't leak internal UUID-suffixed req id back to user.""" + engine = AsyncOmni(model=DIFFUSION_MODEL) + try: + async for output in engine.generate("a white cat", request_id=req_id): + assert output.request_id == req_id + finally: + engine.shutdown() + + +@hardware_test(res={"cuda": "H100"}, num_cards=1) +@pytest.mark.omni +@pytest.mark.asyncio +@pytest.mark.parametrize( + "req_id", + ["my-req-1", "img_gen-abc123", "chatcmpl-xyz"], + ids=["plain", "prefixed-img", "prefixed-chat"], +) +async def test_omni_generate_preserves_request_id(req_id): + """Ensure omni model requests don't leak internal UUID-suffixed req id back to user.""" + engine = AsyncOmni(model=OMNI_MODEL, stage_configs_path=OMNI_STAGE_CONFIG) + try: + async for output in engine.generate( + "Say hello in one word.", + request_id=req_id, + output_modalities=["text"], + ): + assert output.request_id == req_id + finally: + engine.shutdown() + + +@hardware_test(res={"cuda": "L4"}, num_cards=1) +@pytest.mark.omni +@pytest.mark.asyncio +async def test_diffusion_generate_empty_request_id(): + """Empty request_id should get a generated internal ID, not stay empty (diffusion)""" + engine = AsyncOmni(model=DIFFUSION_MODEL) + try: + async for output in engine.generate("a white cat", request_id=""): + assert output.request_id != "" + finally: + engine.shutdown() + + +@hardware_test(res={"cuda": "H100"}, num_cards=1) +@pytest.mark.omni +@pytest.mark.asyncio +async def test_omni_generate_empty_request_id(): + """Empty request_id should get a generated internal ID, not stay empty (omni)""" + engine = AsyncOmni(model=OMNI_MODEL, stage_configs_path=OMNI_STAGE_CONFIG) + try: + async for output in engine.generate( + "Say hello in one word.", + request_id="", + output_modalities=["text"], + ): + assert output.request_id != "" + finally: + engine.shutdown() diff --git a/tests/entrypoints/test_omni_entrypoints.py b/tests/entrypoints/test_omni_entrypoints.py index 3612020d4..bbef876d4 100644 --- a/tests/entrypoints/test_omni_entrypoints.py +++ b/tests/entrypoints/test_omni_entrypoints.py @@ -16,6 +16,7 @@ from vllm.v1.engine.exceptions import EngineDeadError, EngineGenerateError from vllm_omni.engine.async_omni_engine import StageRuntimeInfo from vllm_omni.engine.messages import ErrorMessage, OutputMessage from vllm_omni.entrypoints.async_omni import AsyncOmni +from vllm_omni.entrypoints.client_request_state import ClientRequestState from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni_base import OmniEngineDeadError from vllm_omni.outputs import OmniRequestOutput @@ -165,6 +166,14 @@ class FakeAsyncOmniEngine: def _patch_engine(monkeypatch: pytest.MonkeyPatch, engine: FakeAsyncOmniEngine) -> None: monkeypatch.setattr("vllm_omni.entrypoints.omni_base.AsyncOmniEngine", lambda *args, **kwargs: engine) monkeypatch.setattr("vllm_omni.entrypoints.omni_base.omni_snapshot_download", lambda model: model) + # Don't add random UUIDs to requests calling .generate since we usually + # just want to check for present requests anyway, and would need to just + # strip the UUID. Explicit checks against the mapping are in tests for + # AsyncOmni, or explicitly set the client req states. + monkeypatch.setattr( + "vllm_omni.entrypoints.async_omni.AsyncOmni._get_unique_request_id", + staticmethod(lambda request_id: request_id), + ) def test_direct_omni_with_nullified_parser_only_nulls_untyped_override_fields( @@ -623,16 +632,23 @@ async def test_async_omni_llm_diffusion_yields_text_stream_then_image(monkeypatc async def test_async_omni_abort_forwards_to_engine(monkeypatch: pytest.MonkeyPatch): engine = FakeAsyncOmniEngine(stage_metadata=THREE_STAGE_META) _patch_engine(monkeypatch, engine) - app = AsyncOmni("dummy-model") + + # Requests internally have a random UUID appended to the + # external ID to avoid collisions, so this also tests mapping + external_req_id = "req-1" + req_id = "req-1-12345678" try: - app.request_states["req-1"] = object() - await app.abort("req-1") + app.request_states[req_id] = ClientRequestState( + request_id=req_id, + external_request_id=external_req_id, + ) + await app.abort(external_req_id) finally: app.shutdown() - assert engine.aborted == [["req-1"]] - assert "req-1" not in app.request_states + assert engine.aborted == [[req_id]] + assert external_req_id not in app.request_states @pytest.mark.asyncio diff --git a/vllm_omni/entrypoints/async_omni.py b/vllm_omni/entrypoints/async_omni.py index 3485f98f7..c07bb2085 100644 --- a/vllm_omni/entrypoints/async_omni.py +++ b/vllm_omni/entrypoints/async_omni.py @@ -23,6 +23,7 @@ from vllm.pooling_params import PoolingParams from vllm.renderers.inputs.preprocess import extract_prompt_components from vllm.sampling_params import RequestOutputKind, SamplingParams from vllm.tasks import SupportedTask +from vllm.utils import random_uuid from vllm.v1.engine.exceptions import EngineDeadError from vllm_omni.diffusion.data import OmniACK, OmniSleepTask, OmniWakeTask @@ -215,6 +216,19 @@ class AsyncOmni(EngineClient, OmniBase): return None return vllm_config.model_config + @staticmethod + def _get_unique_request_id(external_request_id: str): + """Get a random new request ID for this request; at the server level, + this is usually set by the calling entrypoint, but in direct calls, we + need to set it explicitly since we do not allow empty IDs. + + NOTE: in the upstream vLLM, this is done in the InputProcessor's + `assign_request_id`. + """ + uuid = random_uuid() + prefix = "" if not external_request_id else f"{external_request_id}-" + return f"{prefix}{uuid:.8}" + # ==================== Generate Method ==================== async def generate( @@ -249,7 +263,8 @@ class AsyncOmni(EngineClient, OmniBase): Args: prompt: A single prompt **or** a list of prompts. A list triggers batch mode when the diffusion stage is reached. - request_id: Unique identifier for this request. + request_id: Unique identifier for this request. If one is not provided, + a random one will be generated. sampling_params_list: List of SamplingParams, one per stage. Must have the same length as the number of stages. If *None*, uses default sampling params for each stage. @@ -263,11 +278,17 @@ class AsyncOmni(EngineClient, OmniBase): Raises: ValueError: If sampling_params_list has incorrect length. """ + # Append a random UUID suffix to the request_id to ensure it is unique + # and non-empty, similar to vLLM's input processor. The suffix is used + # only for internal tracking throughout the request's life. + external_request_id = request_id + request_id = self._get_unique_request_id(external_request_id) + # Wait until generation is resumed if the engine is paused async with self._pause_cond: await self._pause_cond.wait_for(lambda: not self._paused) - logger.debug(f"[AsyncOmni] generate() called for request {request_id}") + logger.debug(f"[AsyncOmni] generate() called for request {external_request_id}") input_stream_task: asyncio.Task | None = None try: @@ -302,7 +323,11 @@ class AsyncOmni(EngineClient, OmniBase): wall_start_ts, final_stage_id_for_e2e, ) - req_state = ClientRequestState(request_id) + + req_state = ClientRequestState( + request_id=request_id, + external_request_id=external_request_id, + ) req_state.metrics = metrics self.request_states[request_id] = req_state @@ -353,11 +378,11 @@ class AsyncOmni(EngineClient, OmniBase): except (asyncio.CancelledError, GeneratorExit): if input_stream_task is not None and not input_stream_task.done(): input_stream_task.cancel() - await self.abort(request_id) + await self._abort_internal_requests(request_id) logger.info(f"[AsyncOmni] Request {request_id} aborted.") raise except Exception as e: - await self.abort(request_id) + await self._abort_internal_requests(request_id) logger.info(f"[AsyncOmni] Request {request_id} failed (input error): {e}") raise @@ -541,6 +566,8 @@ class AsyncOmni(EngineClient, OmniBase): ) if output_to_yield: + # Set the external request ID back to the user yielded input + output_to_yield.request_id = req_state.external_request_id or output_to_yield.request_id logger.debug( "[AsyncOmni] req=%s stage-%s yielding final_output_type=%s", request_id, @@ -687,9 +714,26 @@ class AsyncOmni(EngineClient, OmniBase): async def abort(self, request_id: str | Iterable[str]) -> None: """Abort request(s) via the Orchestrator.""" request_ids = [request_id] if isinstance(request_id, str) else list(request_id) + # Map the external user request IDs to internal IDs used by the Orchestrator. + # NOTE: If the user request_id matches multiple requests, all of them will be + # aborted. This is also what happens in this case in vLLM's output processor. + internal_ids = [s.request_id for s in self.request_states.values() if s.external_request_id in request_ids] + await self._abort(internal_ids) + + async def _abort_internal_requests(self, request_id: str | Iterable[str]): + """Abort request(s) via the Orchestrator given internal request IDs, + which take the format <external_request_id>-<UUID>. + """ + request_ids = [request_id] if isinstance(request_id, str) else list(request_id) + # Request IDs are already internal, so we just need to get the matching states. + internal_req_ids = [rid for rid in request_ids if rid in self.request_states] + await self._abort(internal_req_ids) + + async def _abort(self, request_ids: list[str]) -> None: + """Submit request IDs to be aborted to the engine.""" await self.engine.abort_async(request_ids) - for req_id in request_ids: - self.request_states.pop(req_id, None) + for rid in request_ids: + self.request_states.pop(rid, None) if self.log_stats: logger.info("[AsyncOmni] Aborted request(s) %s", ",".join(request_ids)) diff --git a/vllm_omni/entrypoints/client_request_state.py b/vllm_omni/entrypoints/client_request_state.py index 1c9103f79..680ab9838 100644 --- a/vllm_omni/entrypoints/client_request_state.py +++ b/vllm_omni/entrypoints/client_request_state.py @@ -6,8 +6,14 @@ from vllm_omni.metrics import OrchestratorAggregator class ClientRequestState: """Tracks the state of an individual request in the orchestrator.""" - def __init__(self, request_id: str, queue: asyncio.Queue | None = None): + def __init__( + self, + request_id: str, + external_request_id: str | None = None, + queue: asyncio.Queue | None = None, + ): self.request_id = request_id + self.external_request_id = external_request_id self.stage_id: int | None = None self.queue = queue if queue is not None else asyncio.Queue() self.metrics: OrchestratorAggregator | None = None From 2289478e3af3d2acac525e94aa414de10bff8cb9 Mon Sep 17 00:00:00 2001 From: WeiQing Chen <40507679+david6666666@users.noreply.github.com> Date: Sat, 30 May 2026 17:46:29 +0800 Subject: [PATCH 106/112] [CI] Temporarily skip failing Bagel connector tests (#4005) Signed-off-by: David Chen <530634352@qq.com> --- .../omni_connectors/test_bagel_mooncake_connector.py | 1 + .../omni_connectors/test_bagel_shared_memory_connector.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tests/distributed/omni_connectors/test_bagel_mooncake_connector.py b/tests/distributed/omni_connectors/test_bagel_mooncake_connector.py index a308fe5ea..fe705734b 100644 --- a/tests/distributed/omni_connectors/test_bagel_mooncake_connector.py +++ b/tests/distributed/omni_connectors/test_bagel_mooncake_connector.py @@ -269,6 +269,7 @@ 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.""" diff --git a/tests/distributed/omni_connectors/test_bagel_shared_memory_connector.py b/tests/distributed/omni_connectors/test_bagel_shared_memory_connector.py index 57c6a79fb..3906611b1 100644 --- a/tests/distributed/omni_connectors/test_bagel_shared_memory_connector.py +++ b/tests/distributed/omni_connectors/test_bagel_shared_memory_connector.py @@ -258,6 +258,7 @@ 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.""" @@ -275,6 +276,7 @@ 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.""" From b0f3672aec21e0fc8175dba7f74dc7ce9cbf5cf4 Mon Sep 17 00:00:00 2001 From: wuhang <whlbx@hotmail.com> Date: Sun, 31 May 2026 00:13:23 +0800 Subject: [PATCH 107/112] [Bugfix] Fix DiffusionWorker crash on SIGINT: ensure NCCL/ZMQ cleanup on shutdown (#3872) Signed-off-by: WU Hang <whlbx@hotmail.com> --- .../diffusion/executor/multiproc_executor.py | 4 +- .../diffusion/worker/diffusion_worker.py | 69 ++++++++++++------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/vllm_omni/diffusion/executor/multiproc_executor.py b/vllm_omni/diffusion/executor/multiproc_executor.py index 3aa409e14..1bd848226 100644 --- a/vllm_omni/diffusion/executor/multiproc_executor.py +++ b/vllm_omni/diffusion/executor/multiproc_executor.py @@ -60,11 +60,11 @@ class BackgroundResources: for proc in self.processes: if not proc.is_alive(): continue - proc.join(30) + proc.join(5) if proc.is_alive(): logger.warning("Terminating diffusion worker %s after timeout", proc.name) proc.terminate() - proc.join(30) + proc.join(5) class MultiprocDiffusionExecutor(DiffusionExecutor): diff --git a/vllm_omni/diffusion/worker/diffusion_worker.py b/vllm_omni/diffusion/worker/diffusion_worker.py index 93bf71730..0e8980a29 100644 --- a/vllm_omni/diffusion/worker/diffusion_worker.py +++ b/vllm_omni/diffusion/worker/diffusion_worker.py @@ -823,11 +823,6 @@ class WorkerProc: continue logger.info("event loop terminated.") - try: - self.worker.shutdown() - except Exception as exc: - logger.warning("Worker %s: Shutdown encountered an error: %s", self.gpu_id, exc) - self.context.term() @staticmethod def worker_main( @@ -842,34 +837,62 @@ class WorkerProc: """Worker initialization and execution loops.""" from vllm_omni.plugins import load_omni_general_plugins + shutdown_triggered = False + + def signal_handler(signum: int, frame) -> None: + nonlocal shutdown_triggered + if not shutdown_triggered: + shutdown_triggered = True + raise SystemExit(128 + signum) + + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + set_death_signal(signal.SIGTERM) # Set process title for visibility in nvidia-smi / htop (optional, non-fatal) try: import setproctitle - setproctitle.setproctitle(f"vLLM-Omni::StageDiffusionProc-{rank}") + setproctitle.setproctitle(f"vLLM-Omni::DiffusionWorker-{rank}") except ImportError: pass # setproctitle not installed, skip process title setting load_omni_general_plugins() - worker_proc = WorkerProc( - od_config, - gpu_id=rank, - broadcast_handle=broadcast_handle, - wake_event=wake_event, - worker_extension_cls=worker_extension_cls, - custom_pipeline_args=custom_pipeline_args, - ) - logger.info(f"Worker {rank}: Scheduler loop started.") - pipe_writer.send( - { - "status": "ready", - "result_handle": worker_proc.result_mq_handle if rank == 0 else None, - } - ) - worker_proc.worker_busy_loop() - logger.info(f"Worker {rank}: Shutdown complete.") + worker_proc = None + try: + worker_proc = WorkerProc( + od_config, + gpu_id=rank, + broadcast_handle=broadcast_handle, + wake_event=wake_event, + worker_extension_cls=worker_extension_cls, + custom_pipeline_args=custom_pipeline_args, + ) + logger.info(f"Worker {rank}: Scheduler loop started.") + pipe_writer.send( + { + "status": "ready", + "result_handle": worker_proc.result_mq_handle if rank == 0 else None, + } + ) + worker_proc.worker_busy_loop() + except SystemExit: + logger.info("Worker %d: Shutdown signal received, starting cleanup.", rank) + raise + finally: + if worker_proc is not None: + try: + worker_proc.worker.shutdown() + except Exception as exc: + logger.warning("Worker %d: Shutdown encountered an error: %s", rank, exc) + worker_proc.context.term() + else: + # In case of signal interrupting worker_proc initialization + # where distributed env is initialized but worker_proc is still None + destroy_distributed_env() + + logger.info("Worker %d: Shutdown complete.", rank) class WorkerWrapperBase: From 11c4fcedd339786decd95099db1e233678e2a635 Mon Sep 17 00:00:00 2001 From: DMAN <32004742+Dmaner@users.noreply.github.com> Date: Sun, 31 May 2026 00:18:52 +0800 Subject: [PATCH 108/112] [Recipe] add mistralai voxtral tts recipe (#3498) Signed-off-by: Dmaner <2663515256@qq.com> --- recipes/README.md | 1 + recipes/mistralai/Voxtral-TTS.md | 107 +++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 recipes/mistralai/Voxtral-TTS.md diff --git a/recipes/README.md b/recipes/README.md index ef11edeb3..20248b59c 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -34,6 +34,7 @@ recipes/ | [`inclusionAI/Ming-flash-omni-2.0.md`](./inclusionAI/Ming-flash-omni-2.0.md) | Online serving for multimodal chat + standalone TTS | 4x H100 / 1x H100 80GB | | [`LTX/LTX-2.md`](./LTX/LTX-2.md) | Text-to-video and image-to-video serving | 1x H200 141GB | | [`LTX/LTX-2.3.md`](./LTX/LTX-2.3.md) | Text-to-video with audio generation (22B) | 1x GPU (96GB VRAM) | +| [`mistralai/Voxtral-TTS.md`](./mistralai/Voxtral-TTS.md) | Online serving for TTS | 1x RTX 4090 24GB | | [`Qwen/Qwen-Image.md`](./Qwen/Qwen-Image.md) | Text-to-image serving with step-wise continuous batching replay and ModelOpt mixed FP8/NVFP4 | 1x A100 80GB / 2x B200 | | [`Qwen/Qwen-Image.md`](./Qwen/Qwen-Image.md) | Text-to-image serving with step-wise continuous batching replay | 1x A100 80GB | | [`Qwen/Qwen-Image-Edit.md`](./Qwen/Qwen-Image-Edit.md) | Text-guided single-image editing | 1x or 2x H200 141GB | diff --git a/recipes/mistralai/Voxtral-TTS.md b/recipes/mistralai/Voxtral-TTS.md new file mode 100644 index 000000000..595d5c29e --- /dev/null +++ b/recipes/mistralai/Voxtral-TTS.md @@ -0,0 +1,107 @@ +# Voxtral TTS for text-to-speech + +## Summary + +- Vendor: Mistral AI +- Model: `mistralai/Voxtral-4B-TTS-2603` +- Task: Text-to-speech with model-provided voice presets or voice cloning +- Mode: Online serving with the OpenAI-compatible `/v1/audio/speech` API +- Maintainer: Community + +## When to use this recipe + +Use this recipe when you want a known-good starting point for serving Voxtral TTS +models with vLLM-Omni and validate the deployment with the existing TTS client +examples in this repository. + +## References + +- Upstream model card: <https://huggingface.co/mistralai/Voxtral-4B-TTS-2603> +- Related examples under `examples/`: + [`examples/offline_inference/text_to_speech/voxtral_tts/end2end.py`](../../examples/offline_inference/text_to_speech/voxtral_tts/end2end.py) + [`examples/online_serving/text_to_speech/voxtral_tts/gradio_demo.py`](../../examples/online_serving/text_to_speech/voxtral_tts/gradio_demo.py) +- Related issue or discussion: + [RFC: add recipes folder](https://github.com/vllm-project/vllm-omni/issues/2645) + +## Hardware Support + +## GPU + +### 1x RTX 4090 24GB + +#### Environment + +- OS: Ubuntu 22.04.5 LTS +- Python: 3.12.11 +- PyTorch: 2.11.0+cu130 +- Driver / runtime: CUDA 13.0 +- GPU: NVIDIA GeForce RTX 4090, 24 GB +- vLLM version: 0.20.0 +- vLLM-Omni version or commit: 0.20.0rc2.dev99+g857356d5b +- Required package: mistral_common >= 1.10.0 + +#### Command + +Online: + +```bash +vllm serve mistralai/Voxtral-4B-TTS-2603 \ + --omni \ + --port 8091 +``` + +The deploy config is auto-loaded from `vllm_omni/deploy/voxtral_tts.yaml` + +#### Verification + +**Quick smoke test with curl:** + +```bash +curl -X POST http://127.0.0.1:8091/v1/audio/speech \ + -H "Content-Type: application/json" \ + -d '{ + "model": "mistralai/Voxtral-4B-TTS-2603", + "input": "Hello, this is Voxtral TTS running with vLLM-Omni.", + "voice": "casual_female", + "language": "English", + "response_format": "wav" + }' \ + --output voxtral.wav +``` + +**Streaming audio:** + +```bash +curl -X POST http://127.0.0.1:8091/v1/audio/speech \ + -H "Content-Type: application/json" \ + -d '{ + "model": "mistralai/Voxtral-4B-TTS-2603", + "input": "Hello, this is Voxtral TTS streaming PCM.", + "voice": "casual_female", + "language": "English", + "stream": true, + "response_format": "pcm" + }' \ + --output voxtral_stream.pcm +``` +Convert the raw PCM output to WAV +```bash +ffmpeg -f s16le -ar 24000 -ac 1 -i voxtral_stream.pcm voxtral_stream.wav -y +ffprobe -hide_banner voxtral_stream.wav +``` + +#### Notes + +- Memory usage: Stage 0 (`audio_generation`) ~18.95 GiB, Stage 1 + (`audio_tokenizer`) ~1.55 GiB. The observed server-startup peak was + `21006 MiB / 24564 MiB` (`20.51 GiB / 23.99 GiB`, about 85.5%). + Both stages share GPU 0 via the deploy config (`gpu_memory_utilization: 0.8` + for Stage 0, `0.1` for Stage 1). +- Key flags: `--omni` is required. `--port 8091` is optional and only selects + the HTTP port. +- Known limitation: Voxtral TTS outputs mono audio at 24 kHz. And with the public +mistralai/Voxtral-4B-TTS-2603 checkpoint. running the +voice-cloning e2e test ([`Voice cloning (capability gated upstream)`](../../examples/offline_inference/text_to_speech/README.md#voice-cloning-capability-gated-upstream) +) or adding a `ref_audio` request parameter will fail +because the public checkpoint does not provide the encoder weights needed to +convert reference audio into conditioning features. The expected failure is: `RuntimeError: encode_waveforms requires encoder weights which are not available in the open-source checkpoint.` From ffd9771c54d23ccf7383c86c5c7a776f29ba588f Mon Sep 17 00:00:00 2001 From: zzh <67720593+BLANKETusers@users.noreply.github.com> Date: Sun, 31 May 2026 07:47:36 +0800 Subject: [PATCH 109/112] Fix hunyuan resolve stop token ids (#3896) Signed-off-by: zzh <943967662@qq.com> --- .../hunyuan_image3/end2end.py | 20 +++- .../hunyuan_image3/test_prompt_utils.py | 52 +++++++++ ...test_serving_chat_multistage_generation.py | 101 ++++++++++++++++++ .../models/hunyuan_image3/prompt_utils.py | 9 ++ vllm_omni/entrypoints/openai/serving_chat.py | 20 ++++ 5 files changed, 199 insertions(+), 3 deletions(-) diff --git a/examples/offline_inference/hunyuan_image3/end2end.py b/examples/offline_inference/hunyuan_image3/end2end.py index 537379b89..e8cd8a36f 100644 --- a/examples/offline_inference/hunyuan_image3/end2end.py +++ b/examples/offline_inference/hunyuan_image3/end2end.py @@ -61,8 +61,8 @@ def parse_args(): parser.add_argument("--steps", type=int, default=50, help="Number of inference steps.") parser.add_argument("--guidance-scale", type=float, default=5.0, help="Classifier-free guidance scale.") parser.add_argument("--seed", type=int, default=42, help="Random seed.") - parser.add_argument("--height", type=int, default=1024, help="Output image height.") - parser.add_argument("--width", type=int, default=1024, help="Output image width.") + parser.add_argument("--height", type=int, default=None, help="Output image height.") + parser.add_argument("--width", type=int, default=None, help="Output image width.") parser.add_argument("--vae-use-tiling", action="store_true", help="Enable VAE tiling.") parser.add_argument( "--bot-task", @@ -229,7 +229,21 @@ def main(): from vllm_omni.inputs.data import OmniDiffusionSamplingParams - ar_stop_token_ids = resolve_stop_token_ids(task=task, bot_task=bot_task, tokenizer=tokenizer) + if (args.height is None) != (args.width is None): + raise ValueError("--height and --width must both be specified or both omitted.") + user_specified_size = args.height is not None and args.width is not None + if args.modality in ("img2text", "text2text"): + ar_image_size = "auto" + elif user_specified_size: + ar_image_size = f"{args.width}x{args.height}" + else: + ar_image_size = None + ar_stop_token_ids = resolve_stop_token_ids( + task=task, bot_task=bot_task, tokenizer=tokenizer, image_size=ar_image_size + ) + print( + f"[AR Config] task={task}, bot_task={bot_task}, image_size={ar_image_size}, stop_token_ids={ar_stop_token_ids}" + ) for sp in params_list: if isinstance(sp, OmniDiffusionSamplingParams): sp.num_inference_steps = args.steps diff --git a/tests/diffusion/models/hunyuan_image3/test_prompt_utils.py b/tests/diffusion/models/hunyuan_image3/test_prompt_utils.py index 7c3256eee..7cb7ea83a 100644 --- a/tests/diffusion/models/hunyuan_image3/test_prompt_utils.py +++ b/tests/diffusion/models/hunyuan_image3/test_prompt_utils.py @@ -124,6 +124,58 @@ def test_resolve_stop_token_ids_image_tasks_stop_on_ratio_range(): assert resolve_stop_token_ids(task="i2t", bot_task=None, tokenizer=tok) == [answer_id] assert resolve_stop_token_ids(task="t2t", bot_task=None, tokenizer=tok) == [answer_id] + # image_size="auto" is equivalent to None (need_ratio=True). + for bot in ("think", "recaption", "think_recaption", "vanilla"): + assert resolve_stop_token_ids(task="t2i", bot_task=bot, tokenizer=tok, image_size="auto") == expected + assert resolve_stop_token_ids(task="it2i", bot_task=bot, tokenizer=tok, image_size="auto") == expected + + +def test_resolve_stop_token_ids_explicit_image_size_stops_on_terminator(): + """When image_size is an explicit "WxH" string, need_ratio=False: AR + already knows the size and stops at the cot terminator instead of the + ratio range. + + Mirrors upstream logic where ``need_ratio`` is false when the user + supplies a target size, so ``final_stop_tokens`` is set to the + recaption/think terminator (not the ratio tokens). + """ + tok = FakeTokenizer() + + end_of_think_id = HUNYUAN_IMAGE3_SPECIAL_TOKEN_IDS["</think>"] + end_of_recaption_id = HUNYUAN_IMAGE3_SPECIAL_TOKEN_IDS["</recaption>"] + + # think_recaption / recaption -> [end_of_recaption] + for bot in ("think_recaption", "recaption"): + for task in ("t2i", "it2i"): + result = resolve_stop_token_ids(task=task, bot_task=bot, tokenizer=tok, image_size="1024x1024") + assert result == [end_of_recaption_id], ( + f"task={task}, bot_task={bot}, image_size='1024x1024': expected [{end_of_recaption_id}], got {result}" + ) + + # think -> [end_of_think, end_of_recaption] + for task in ("t2i", "it2i"): + result = resolve_stop_token_ids(task=task, bot_task="think", tokenizer=tok, image_size="1024x1024") + assert result == [end_of_think_id, end_of_recaption_id], ( + f"task={task}, bot_task='think', image_size='1024x1024': " + f"expected [{end_of_think_id}, {end_of_recaption_id}], got {result}" + ) + + # vanilla -> no early return, falls through to ratio range + start = HUNYUAN_IMAGE3_SPECIAL_TOKEN_IDS["<img_ratio_0>"] + end = HUNYUAN_IMAGE3_SPECIAL_TOKEN_IDS["<img_ratio_32>"] + other_start = HUNYUAN_IMAGE3_SPECIAL_TOKEN_IDS["<img_ratio_33>"] + other_end = HUNYUAN_IMAGE3_SPECIAL_TOKEN_IDS["<img_ratio_36>"] + expected_ratio = list(range(start, end + 1)) + list(range(other_start, other_end + 1)) + for task in ("t2i", "it2i"): + result = resolve_stop_token_ids(task=task, bot_task="vanilla", tokenizer=tok, image_size="1024x1024") + assert result == expected_ratio, ( + f"task={task}, bot_task='vanilla', image_size='1024x1024': expected ratio range, got {result}" + ) + + # Text tasks are unaffected by image_size. + answer_id = HUNYUAN_IMAGE3_SPECIAL_TOKEN_IDS["<answer>"] + assert resolve_stop_token_ids(task="i2t", bot_task=None, tokenizer=tok, image_size="1024x1024") == [answer_id] + @pytest.mark.parametrize( "task,bot_task", diff --git a/tests/entrypoints/openai_api/test_serving_chat_multistage_generation.py b/tests/entrypoints/openai_api/test_serving_chat_multistage_generation.py index 4b63588ba..c8bb050e7 100644 --- a/tests/entrypoints/openai_api/test_serving_chat_multistage_generation.py +++ b/tests/entrypoints/openai_api/test_serving_chat_multistage_generation.py @@ -363,3 +363,104 @@ def test_build_multistage_generation_inputs_custom_system_prompt(serving_chat): f"custom system_prompt content must reach the rendered prompt; " f"marker {marker!r} not found in prompt of length {len(out['prompt'])}" ) + + +def test_build_multistage_generation_inputs_sets_ar_stop_token_ids_with_explicit_size(serving_chat): + """When height+width are provided with bot_task, the AR (llm) stage + must receive stop_token_ids from resolve_stop_token_ids so AR stops + at the correct terminator instead of generating until max_tokens. + + Without this, AR would generate past the cot boundary and produce + garbage that the DiT bridge cannot parse. + """ + from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat + + class FakeTokenizer: + SPECIAL = { + "<|startoftext|>": 1, + "<img>": 2, + "<recaption>": 4, + } + + def convert_tokens_to_ids(self, tok: str) -> int: + return self.SPECIAL.get(tok, 0) + + def encode(self, text: str, add_special_tokens: bool = False) -> list[int]: + return list(range(100, 100 + len(text))) + + engine = SimpleNamespace( + stage_configs=[ + SimpleNamespace(stage_type="llm", is_comprehension=True), + SimpleNamespace(stage_type="diffusion", is_comprehension=False), + ], + default_sampling_params_list=[ + SamplingParams(temperature=0.0), + OmniDiffusionSamplingParams(), + ], + ) + images = [Image.new("RGB", (32, 32), color="red")] + + # With height+width and bot_task=think, ar_image_size="1024x768" -> + # need_ratio=False -> stop_token_ids=[end_of_think, end_of_recaption] + _, sampling_params_list = OmniOpenAIServingChat._build_multistage_generation_inputs( + serving_chat, + engine=engine, + prompt="draw a cat", + extra_body={"bot_task": "think"}, + reference_images=images, + gen_params=OmniDiffusionSamplingParams(height=768, width=1024), + tokenizer=FakeTokenizer(), + ) + + # AR stage (index 0) must have stop_token_ids set. + ar_params = sampling_params_list[0] + assert ar_params.stop_token_ids is not None, ( + "AR stage must have stop_token_ids set when height+width and bot_task are provided" + ) + assert len(ar_params.stop_token_ids) > 0, "stop_token_ids must be non-empty" + + # Diffusion stage (index 1) must NOT have stop_token_ids set. + diff_params = sampling_params_list[1] + assert getattr(diff_params, "stop_token_ids", None) is None, "Diffusion stage must not have stop_token_ids set" + + +def test_build_multistage_generation_inputs_no_stop_token_ids_without_size(serving_chat): + """Without height+width, ar_image_size=None -> need_ratio=True -> + stop_token_ids is set to the ratio range (AR predicts ratio). + This still assigns stop_token_ids, but the set is the ratio range, + not the terminator. + + Without bot_task at all, ar_stop_token_ids stays None and + stop_token_ids is not set on any stage. + """ + from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat + + engine = SimpleNamespace( + stage_configs=[ + SimpleNamespace(stage_type="llm", is_comprehension=True), + SimpleNamespace(stage_type="diffusion", is_comprehension=False), + ], + default_sampling_params_list=[ + SamplingParams(temperature=0.0), + OmniDiffusionSamplingParams(), + ], + ) + images = [Image.new("RGB", (32, 32), color="red")] + + # No height/width, no bot_task -> ar_stop_token_ids=None -> + # stop_token_ids not set on any stage. + _, sampling_params_list = OmniOpenAIServingChat._build_multistage_generation_inputs( + serving_chat, + engine=engine, + prompt="draw a cat", + extra_body={}, + reference_images=images, + gen_params=OmniDiffusionSamplingParams(), + ) + + # SamplingParams defaults stop_token_ids=[], not None. + # The key contract: it was NOT set by resolve_stop_token_ids, + # so it stays as the SamplingParams default (empty list). + assert sampling_params_list[0].stop_token_ids == [], ( + "Without bot_task, AR stage stop_token_ids must be the default empty list" + ) diff --git a/vllm_omni/diffusion/models/hunyuan_image3/prompt_utils.py b/vllm_omni/diffusion/models/hunyuan_image3/prompt_utils.py index f054fef26..b64fe2e97 100644 --- a/vllm_omni/diffusion/models/hunyuan_image3/prompt_utils.py +++ b/vllm_omni/diffusion/models/hunyuan_image3/prompt_utils.py @@ -146,6 +146,7 @@ def resolve_stop_token_ids( task: str = "it2i", bot_task: str | None | _DefaultBotTask = _DEFAULT_BOT_TASK, tokenizer: Any | None = None, + image_size: str | None = None, ) -> list[int]: """AR stop-token ids for a given (task, bot_task) generation request. @@ -171,6 +172,14 @@ def resolve_stop_token_ids( if bot_task not in _BOT_TASK_PRESETS: raise ValueError(f"Unknown bot_task {bot_task!r}. Choose from: {available_bot_tasks()}") if task in ("it2i", "t2i"): + need_ratio = image_size is None or image_size == "auto" + if not need_ratio: + end_of_think_id = HUNYUAN_IMAGE3_SPECIAL_TOKEN_IDS["</think>"] + end_of_recaption_id = HUNYUAN_IMAGE3_SPECIAL_TOKEN_IDS["</recaption>"] + if bot_task in ("think_recaption", "recaption"): + return [end_of_recaption_id] + if bot_task == "think": + return [end_of_think_id, end_of_recaption_id] # Main ratio range: <img_ratio_0> .. <img_ratio_32>. start = HUNYUAN_IMAGE3_SPECIAL_TOKEN_IDS["<img_ratio_0>"] end = HUNYUAN_IMAGE3_SPECIAL_TOKEN_IDS["<img_ratio_32>"] diff --git a/vllm_omni/entrypoints/openai/serving_chat.py b/vllm_omni/entrypoints/openai/serving_chat.py index 06ccfcd70..f98732984 100644 --- a/vllm_omni/entrypoints/openai/serving_chat.py +++ b/vllm_omni/entrypoints/openai/serving_chat.py @@ -2335,11 +2335,13 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): prompt_token_ids: list[int] | None = None system_prompt_type: str | None = None build_kwargs: dict[str, Any] = {} + ar_stop_token_ids: list[int] | None = None if bot_task is not None or use_system_prompt is not None or custom_system_prompt is not None: from vllm_omni.diffusion.models.hunyuan_image3.prompt_utils import ( build_prompt, build_prompt_tokens, + resolve_stop_token_ids, ) build_kwargs: dict[str, Any] = { @@ -2368,6 +2370,19 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): engine_prompt_data = {"image": reference_images[0]} modalities = ["image"] + ar_task = "it2i" if reference_images else "t2i" + # ar_image_size: None -> need_ratio=True (AR predicts ratio); + # explicit size -> need_ratio=False (AR stops at terminator). + ar_image_size: str | None = None + if height is not None and width is not None: + ar_image_size = f"{width}x{height}" + ar_stop_token_ids = resolve_stop_token_ids( + task=ar_task, + bot_task=bot_task, + tokenizer=tokenizer, + image_size=ar_image_size, + ) + engine_prompt: OmniTextPrompt = {"prompt": prompt} if prompt_token_ids is not None: engine_prompt["prompt_token_ids"] = prompt_token_ids @@ -2413,6 +2428,11 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin): stage_type = get_stage_type(stage_cfg) default_stage_params = sampling_params_list[idx] + # AR stop tokens: use stage_type=="llm" instead of comprehension_idx + # (None for DictConfig where is_comprehension is nested in engine_args). + if stage_type == "llm" and ar_stop_token_ids is not None: + default_stage_params.stop_token_ids = ar_stop_token_ids + if ( comprehension_idx is not None and idx == comprehension_idx From 412bf42a75e58091e74ed89e03221170961e1f4b Mon Sep 17 00:00:00 2001 From: Canlin Guo <canlinguosdu@gmail.com> Date: Sun, 31 May 2026 07:58:00 +0800 Subject: [PATCH 110/112] [Refactor] Unify _talker_mtp_forward across GPU and NPU model runners (#3476) Signed-off-by: gcanlin <canlinguosdu@gmail.com> --- tests/worker/test_omni_gpu_model_runner.py | 16 +-- vllm_omni/platforms/interface.py | 36 +++++++ vllm_omni/platforms/npu/platform.py | 24 +++++ .../platforms/npu/worker/npu_model_runner.py | 98 +------------------ vllm_omni/worker/gpu_ar_model_runner.py | 2 +- vllm_omni/worker/gpu_model_runner.py | 53 +++++----- 6 files changed, 98 insertions(+), 131 deletions(-) diff --git a/tests/worker/test_omni_gpu_model_runner.py b/tests/worker/test_omni_gpu_model_runner.py index 9c8640b51..ae6ecc57c 100644 --- a/tests/worker/test_omni_gpu_model_runner.py +++ b/tests/worker/test_omni_gpu_model_runner.py @@ -161,11 +161,11 @@ def _make_runner_for_mimo(req_id="r_mimo"): def test_talker_mtp_forward_cpu_updates_inputs_and_info(monkeypatch): - # Patch the module-level `set_forward_context` symbol used inside - # OmniGPUModelRunner._talker_mtp_forward. + # `_talker_mtp_forward` calls `current_omni_platform.set_forward_context`, + # which would otherwise dispatch to the real device implementation. import vllm_omni.worker.gpu_model_runner as mod # Must be the same module that defines OmniGPUModelRunner - monkeypatch.setattr(mod, "set_forward_context", _noop_forward_context) + monkeypatch.setattr(mod.current_omni_platform, "set_forward_context", _noop_forward_context) runner = _make_runner(req_ids=("r1", "r2"), hidden_size=4) @@ -199,7 +199,7 @@ def test_talker_mtp_forward_cpu_updates_inputs_and_info(monkeypatch): def test_talker_mtp_forward_cpu_empty_batch_noop(monkeypatch): import vllm_omni.worker.gpu_model_runner as mod - monkeypatch.setattr(mod, "set_forward_context", _noop_forward_context) + monkeypatch.setattr(mod.current_omni_platform, "set_forward_context", _noop_forward_context) runner = _make_runner(req_ids=("r1",), hidden_size=4) @@ -215,7 +215,7 @@ def test_talker_mtp_forward_cpu_empty_batch_noop(monkeypatch): def test_talker_mtp_forward_ignores_default_sampling_seed_without_request_marker(monkeypatch): import vllm_omni.worker.gpu_model_runner as mod - monkeypatch.setattr(mod, "set_forward_context", _noop_forward_context) + monkeypatch.setattr(mod.current_omni_platform, "set_forward_context", _noop_forward_context) runner = _make_runner(req_ids=("r1",), hidden_size=4) runner.requests["r1"].sampling_params = SimpleNamespace(seed=42) @@ -237,7 +237,7 @@ def test_talker_mtp_forward_ignores_default_sampling_seed_without_request_marker def test_talker_mtp_forward_passes_qwen3_tts_subtalker_sampling_params_to_talker(monkeypatch): import vllm_omni.worker.gpu_model_runner as mod - monkeypatch.setattr(mod, "set_forward_context", _noop_forward_context) + monkeypatch.setattr(mod.current_omni_platform, "set_forward_context", _noop_forward_context) runner = _make_runner(req_ids=("r1",), hidden_size=4) runner.requests["r1"].sampling_params = SimpleNamespace( @@ -281,7 +281,7 @@ def test_talker_mtp_forward_passes_qwen3_tts_subtalker_sampling_params_to_talker def test_talker_mtp_forward_keeps_explicit_seeded_requests_scalar(monkeypatch): import vllm_omni.worker.gpu_model_runner as mod - monkeypatch.setattr(mod, "set_forward_context", _noop_forward_context) + monkeypatch.setattr(mod.current_omni_platform, "set_forward_context", _noop_forward_context) runner = _make_runner(req_ids=("r1", "r2"), hidden_size=4) runner.requests["r1"].sampling_params = SimpleNamespace( @@ -322,7 +322,7 @@ def test_update_intermediate_buffer_writes_to_buffer_and_setattr(monkeypatch): (forward path) and mirrors to additional_information_cpu setattr (backward compat).""" import vllm_omni.worker.gpu_model_runner as mod - monkeypatch.setattr(mod, "set_forward_context", _noop_forward_context) + monkeypatch.setattr(mod.current_omni_platform, "set_forward_context", _noop_forward_context) runner = _make_runner(req_ids=("r1",), hidden_size=4) diff --git a/vllm_omni/platforms/interface.py b/vllm_omni/platforms/interface.py index f9ec84300..48c513da3 100644 --- a/vllm_omni/platforms/interface.py +++ b/vllm_omni/platforms/interface.py @@ -7,6 +7,8 @@ from typing import Any import torch import torch.nn as nn +from vllm.config import CUDAGraphMode, VllmConfig +from vllm.forward_context import BatchDescriptor from vllm.logger import init_logger from vllm.platforms import Platform @@ -200,6 +202,40 @@ class OmniPlatform(Platform): """ return "vllm_omni.profiler.omni_torch_profiler.OmniTorchProfilerWrapper" + @classmethod + def get_graph_wrapper_cls(cls) -> type: + """Return the platform's full-graph wrapper class. + + Defaults to vLLM's CUDAGraphWrapper; NPU overrides with ACLGraphWrapper. + """ + from vllm.compilation.cuda_graph import CUDAGraphWrapper + + return CUDAGraphWrapper + + @classmethod + def set_forward_context( + cls, + attn_metadata: Any, + vllm_config: VllmConfig, + *, + cudagraph_runtime_mode: CUDAGraphMode, + batch_descriptor: BatchDescriptor, + ): + """Platform-neutral wrapper around the device's set_forward_context. + + Defaults to vLLM's ``set_forward_context``; NPU overrides to dispatch + to ``set_ascend_forward_context`` (renaming ``cudagraph_runtime_mode`` + to ``aclgraph_runtime_mode``). + """ + from vllm.forward_context import set_forward_context + + return set_forward_context( + attn_metadata, + vllm_config, + cudagraph_runtime_mode=cudagraph_runtime_mode, + batch_descriptor=batch_descriptor, + ) + class UnspecifiedOmniPlatform(OmniPlatform): _omni_enum = OmniPlatformEnum.UNSPECIFIED diff --git a/vllm_omni/platforms/npu/platform.py b/vllm_omni/platforms/npu/platform.py index e371c73a6..faf69c1b8 100644 --- a/vllm_omni/platforms/npu/platform.py +++ b/vllm_omni/platforms/npu/platform.py @@ -153,3 +153,27 @@ class NPUOmniPlatform(OmniPlatform, NPUPlatform): @classmethod def get_profiler_cls(cls) -> str: return "vllm_omni.platforms.npu.profiler.NPUTorchProfilerWrapper" + + @classmethod + def get_graph_wrapper_cls(cls) -> type: + from vllm_ascend.compilation.acl_graph import ACLGraphWrapper + + return ACLGraphWrapper + + @classmethod + def set_forward_context( + cls, + attn_metadata, + vllm_config, + *, + cudagraph_runtime_mode, + batch_descriptor, + ): + from vllm_ascend.ascend_forward_context import set_ascend_forward_context + + return set_ascend_forward_context( + attn_metadata, + vllm_config, + aclgraph_runtime_mode=cudagraph_runtime_mode, + batch_descriptor=batch_descriptor, + ) diff --git a/vllm_omni/platforms/npu/worker/npu_model_runner.py b/vllm_omni/platforms/npu/worker/npu_model_runner.py index c6963ac67..4137ffe92 100644 --- a/vllm_omni/platforms/npu/worker/npu_model_runner.py +++ b/vllm_omni/platforms/npu/worker/npu_model_runner.py @@ -16,7 +16,6 @@ from vllm.v1.worker.gpu_model_runner import PerLayerAttnMetadata from vllm_ascend.ascend_forward_context import get_forward_context, set_ascend_forward_context from vllm_ascend.attention.attention_v1 import AscendAttentionState from vllm_ascend.attention.utils import using_paged_attention -from vllm_ascend.compilation.acl_graph import ACLGraphWrapper from vllm_ascend.ops.rotary_embedding import update_cos_sin from vllm_ascend.utils import enable_sp, lmhead_tp_enable from vllm_ascend.worker.model_runner_v1 import SEQ_LEN_WITH_MAX_PA_WORKSPACE, NPUModelRunner @@ -34,30 +33,7 @@ class OmniNPUModelRunner(OmniGPUModelRunner, NPUModelRunner): # in _pad_for_sequence_parallelism during execute_model. # This is a workaround for vllm-ascend not passing vllm_config to enable_sp(). enable_sp(self.vllm_config) - # TODO move this model specific logic to a separate class - # TTS model IS the talker (no .talker sub-attr); use getattr to support both Omni and TTS. - self.has_talker_mtp = False - talker_mtp = getattr(self.model, "talker_mtp", None) - if talker_mtp is not None: - self.talker_mtp = talker_mtp # type: ignore[assignment] - self.has_talker_mtp = True - cudagraph_mode = self.compilation_config.cudagraph_mode - assert cudagraph_mode is not None - has_separate_talker = getattr(self.model, "talker", None) is not None - talker_mtp_graph_safe = getattr(self.model, "talker_mtp_graph_safe", False) - if cudagraph_mode.has_full_cudagraphs() and (has_separate_talker or talker_mtp_graph_safe): - self.talker_mtp = ACLGraphWrapper(talker_mtp, self.vllm_config, runtime_mode=CUDAGraphMode.FULL) - # TTS exposes mtp_hidden_size; Omni uses hf_text_config.hidden_size. - hidden_size = int( - getattr(self.model, "mtp_hidden_size", 0) or getattr(self.model_config.hf_text_config, "hidden_size") - ) - max_batch_size = max(self.max_num_reqs, self.compilation_config.max_cudagraph_capture_size) - self.talker_mtp_input_ids = self._make_buffer(max_batch_size, dtype=torch.int32) - self.talker_mtp_inputs_embeds = self._make_buffer( - max_batch_size, hidden_size, dtype=self.dtype, numpy=False - ) - self.last_talker_hidden = self._make_buffer(max_batch_size, hidden_size, dtype=self.dtype, numpy=False) - self.text_step = self._make_buffer(max_batch_size, hidden_size, dtype=self.dtype, numpy=False) + self._init_talker_mtp() @torch.inference_mode() def _dummy_run( @@ -397,75 +373,3 @@ class OmniNPUModelRunner(OmniGPUModelRunner, NPUModelRunner): model_output = self._all_gather_hidden_states_and_aux(model_output) return model_output - - def _talker_mtp_forward(self, decode_req_ids: list[str], inputs_embeds: torch.Tensor) -> None: - decode_batch_size = len(decode_req_ids) - if decode_batch_size == 0: - return - _cudagraph_mode, batch_desc, _, _, _ = self._determine_batch_execution_and_padding( - num_tokens=decode_batch_size, - num_reqs=decode_batch_size, - num_scheduled_tokens_np=np.ones(decode_batch_size, dtype=np.int32), - max_num_scheduled_tokens=1, - use_cascade_attn=False, - ) - # Force eager for unwrapped code predictors (AR loops / multinomial). - if not isinstance(self.talker_mtp, ACLGraphWrapper): - _cudagraph_mode = CUDAGraphMode.NONE - num_tokens_padded = batch_desc.num_tokens - req_input_ids = self.talker_mtp_input_ids.gpu[:num_tokens_padded] - req_embeds = self.talker_mtp_inputs_embeds.gpu[:num_tokens_padded] - last_talker_hidden = self.last_talker_hidden.gpu[:num_tokens_padded] - text_step = self.text_step.gpu[:num_tokens_padded] - subtalker_params = getattr(self.vllm_config.model_config, "subtalker_sampling_params", None) - if not isinstance(subtalker_params, dict): - subtalker_params = {} - # Extract seed from the first request's sampling params for Fast AR - # determinism. NOTE: when batch_size > 1, all requests share the first - # request's seed. Per-request Fast AR seeding requires row-by-row - # torch.multinomial calls and is left as a follow-up optimisation. - _seed = None - if decode_req_ids: - _first_sp = getattr(self.requests[decode_req_ids[0]], "sampling_params", None) - if _first_sp is not None and getattr(_first_sp, "seed", None) is not None: - _seed = _first_sp.seed - # Warn when batched requests have different seeds. - if len(decode_req_ids) > 1 and _seed is not None: - _other_seeds = { - getattr(getattr(self.requests[rid], "sampling_params", None), "seed", None) - for rid in decode_req_ids[1:] - } - if _other_seeds != {_seed}: - logger.warning( - "Fast AR seed: batch has mixed seeds; using first request's seed=%d for all %d requests.", - _seed, - len(decode_req_ids), - ) - talker_kwargs = { - "do_sample": subtalker_params.get("do_sample"), - "temperature": subtalker_params.get("temperature"), - "top_k": subtalker_params.get("top_k"), - "top_p": subtalker_params.get("top_p"), - } - if _seed is not None: - talker_kwargs["seed"] = _seed - with set_ascend_forward_context( - None, self.vllm_config, aclgraph_runtime_mode=_cudagraph_mode, batch_descriptor=batch_desc - ): - req_embeds, code_predictor_codes = self.talker_mtp( - req_input_ids, - req_embeds, - last_talker_hidden, - text_step, - **talker_kwargs, - ) - # update the inputs_embeds and code_predictor_codes - out_key = getattr(self.model, "talker_mtp_output_key", ("codes", "audio")) - if not isinstance(out_key, tuple) or len(out_key) != 2: - raise TypeError(f"talker_mtp_output_key must be a 2-tuple, got {type(out_key).__name__}: {out_key!r}") - for idx, req_id in enumerate(decode_req_ids): - req_index = self.input_batch.req_ids.index(req_id) - start_offset = int(self.query_start_loc.cpu[req_index]) - inputs_embeds[start_offset : start_offset + 1] = req_embeds[idx : idx + 1] - update_dict = {out_key[0]: {out_key[1]: code_predictor_codes[idx : idx + 1]}} - self._merge_additional_information_update(req_id, update_dict) diff --git a/vllm_omni/worker/gpu_ar_model_runner.py b/vllm_omni/worker/gpu_ar_model_runner.py index f9ce18c6a..78e2acdb5 100644 --- a/vllm_omni/worker/gpu_ar_model_runner.py +++ b/vllm_omni/worker/gpu_ar_model_runner.py @@ -210,7 +210,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin): return result def _capture_talker_mtp_graphs(self) -> None: - from vllm_omni.worker.gpu_model_runner import CUDAGraphWrapper + from vllm.compilation.cuda_graph import CUDAGraphWrapper if not self.has_talker_mtp or not isinstance(self.talker_mtp, CUDAGraphWrapper): return diff --git a/vllm_omni/worker/gpu_model_runner.py b/vllm_omni/worker/gpu_model_runner.py index 39e70273b..a97cc2b65 100644 --- a/vllm_omni/worker/gpu_model_runner.py +++ b/vllm_omni/worker/gpu_model_runner.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Any, cast import numpy as np import torch -from vllm.compilation.cuda_graph import CUDAGraphWrapper from vllm.config import CUDAGraphMode from vllm.distributed.parallel_state import get_pp_group from vllm.forward_context import set_forward_context @@ -33,6 +32,7 @@ from vllm_omni.core.prefix_cache import OmniTensorPrefixCache from vllm_omni.engine.serialization import deserialize_additional_information from vllm_omni.model_executor.layers.rotary_embedding.mrope import OmniMRotaryEmbedding as MRotaryEmbedding from vllm_omni.model_executor.models.output_templates import OmniOutput +from vllm_omni.platforms import current_omni_platform if TYPE_CHECKING: from vllm.v1.core.sched.output import SchedulerOutput @@ -96,32 +96,34 @@ class OmniGPUModelRunner(GPUModelRunner): @instrument(span_name="Loading (GPU)") def load_model(self, *args, **kwargs) -> None: super().load_model(*args, **kwargs) + self._init_talker_mtp() self._prewarm_attention_capture_workspaces() + def _init_talker_mtp(self) -> None: # TODO move this model specific logic to a separate class # TTS model IS the talker (no .talker sub-attr); use getattr to support both Omni and TTS. self.has_talker_mtp = False talker_mtp = getattr(self.model, "talker_mtp", None) - if talker_mtp is not None: - self.talker_mtp = talker_mtp # type: ignore[assignment] - self.has_talker_mtp = True - cudagraph_mode = self.compilation_config.cudagraph_mode - assert cudagraph_mode is not None - has_separate_talker = getattr(self.model, "talker", None) is not None - talker_mtp_graph_safe = getattr(self.model, "talker_mtp_graph_safe", False) - if cudagraph_mode.has_full_cudagraphs() and (has_separate_talker or talker_mtp_graph_safe): - self.talker_mtp = CUDAGraphWrapper(talker_mtp, self.vllm_config, runtime_mode=CUDAGraphMode.FULL) - # TTS exposes mtp_hidden_size; Omni uses hf_text_config.hidden_size. - hidden_size = int( - getattr(self.model, "mtp_hidden_size", 0) or getattr(self.model_config.hf_text_config, "hidden_size") - ) - max_batch_size = max(self.max_num_reqs, self.compilation_config.max_cudagraph_capture_size) - self.talker_mtp_input_ids = self._make_buffer(max_batch_size, dtype=torch.int32) - self.talker_mtp_inputs_embeds = self._make_buffer( - max_batch_size, hidden_size, dtype=self.dtype, numpy=False - ) - self.last_talker_hidden = self._make_buffer(max_batch_size, hidden_size, dtype=self.dtype, numpy=False) - self.text_step = self._make_buffer(max_batch_size, hidden_size, dtype=self.dtype, numpy=False) + if talker_mtp is None: + return + self.talker_mtp = talker_mtp # type: ignore[assignment] + self.has_talker_mtp = True + cudagraph_mode = self.compilation_config.cudagraph_mode + assert cudagraph_mode is not None + has_separate_talker = getattr(self.model, "talker", None) is not None + talker_mtp_graph_safe = getattr(self.model, "talker_mtp_graph_safe", False) + if cudagraph_mode.has_full_cudagraphs() and (has_separate_talker or talker_mtp_graph_safe): + graph_wrapper_cls = current_omni_platform.get_graph_wrapper_cls() + self.talker_mtp = graph_wrapper_cls(talker_mtp, self.vllm_config, runtime_mode=CUDAGraphMode.FULL) + # TTS exposes mtp_hidden_size; Omni uses hf_text_config.hidden_size. + hidden_size = int( + getattr(self.model, "mtp_hidden_size", 0) or getattr(self.model_config.hf_text_config, "hidden_size") + ) + max_batch_size = max(self.max_num_reqs, self.compilation_config.max_cudagraph_capture_size) + self.talker_mtp_input_ids = self._make_buffer(max_batch_size, dtype=torch.int32) + self.talker_mtp_inputs_embeds = self._make_buffer(max_batch_size, hidden_size, dtype=self.dtype, numpy=False) + self.last_talker_hidden = self._make_buffer(max_batch_size, hidden_size, dtype=self.dtype, numpy=False) + self.text_step = self._make_buffer(max_batch_size, hidden_size, dtype=self.dtype, numpy=False) def _prewarm_attention_capture_workspaces(self) -> None: capture_sizes = getattr(self.compilation_config, "cudagraph_capture_sizes", None) @@ -1645,9 +1647,10 @@ class OmniGPUModelRunner(GPUModelRunner): use_cascade_attn=False, ) # Force eager for unwrapped code predictors (AR loops / multinomial). - # When talker_mtp is not a CUDAGraphWrapper, it manages its own CUDA - # graphs internally (code_predictor has its own bucket sizes). - if not isinstance(self.talker_mtp, CUDAGraphWrapper): + # When talker_mtp is not wrapped by the platform's full-graph wrapper, + # it manages its own device graphs internally (code_predictor has its + # own bucket sizes). + if not isinstance(self.talker_mtp, current_omni_platform.get_graph_wrapper_cls()): _cudagraph_mode = CUDAGraphMode.NONE num_tokens_padded = decode_batch_size else: @@ -1711,7 +1714,7 @@ class OmniGPUModelRunner(GPUModelRunner): } if generator is not None: talker_kwargs["generator"] = generator - with set_forward_context( + with current_omni_platform.set_forward_context( None, self.vllm_config, cudagraph_runtime_mode=_cudagraph_mode, batch_descriptor=batch_desc ): req_embeds, code_predictor_codes = self.talker_mtp( From 6f4bd3e45cb8a349879959c4a73bffb8eeb7f1db Mon Sep 17 00:00:00 2001 From: NumberWan <wantszkin2003@gmail.com> Date: Sun, 31 May 2026 08:06:18 +0800 Subject: [PATCH 111/112] [BugFix]Qwen-Image performance regression by using omni RMSNorm(RMSNorm backend) (#3933) Signed-off-by: NumberWan <wantszkin2003@gmail.com> --- vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py b/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py index 5e4d526d8..abdca8b1a 100644 --- a/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py +++ b/vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py @@ -17,7 +17,6 @@ from diffusers.models.embeddings import TimestepEmbedding, Timesteps from diffusers.models.modeling_outputs import Transformer2DModelOutput from diffusers.models.normalization import AdaLayerNormContinuous from vllm.logger import init_logger -from vllm.model_executor.layers.layernorm import RMSNorm from vllm.model_executor.layers.linear import ( ColumnParallelLinear, QKVParallelLinear, @@ -44,6 +43,7 @@ from vllm_omni.diffusion.distributed.sp_plan import ( ) from vllm_omni.diffusion.forward_context import get_forward_context from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNorm +from vllm_omni.diffusion.layers.norm import RMSNorm from vllm_omni.diffusion.layers.rope import RotaryEmbedding logger = init_logger(__name__) From 5dfdf584dd4def5fe43e10b19ef646807de2121d Mon Sep 17 00:00:00 2001 From: ChenWenjing <54166744+Shirley125@users.noreply.github.com> Date: Sun, 31 May 2026 17:39:25 +0800 Subject: [PATCH 112/112] [Feat]audio streaming input for async chunk (#3614) Signed-off-by: CHEN <116010019@link.cuhk.edu.cn> --- examples/online_serving/qwen3_omni/README.md | 6 +- .../sched/test_omni_ar_scheduler_streaming.py | 92 ++++++++++++ ...ni_generation_scheduler_update_session.py} | 35 +++-- .../test_chunk_transfer_adapter.py | 52 ++++++- tests/engine/test_orchestrator.py | 1 + .../test_qwen3_omni_realtime_websocket.py | 1 - .../test_qwen3_omni_streaming_helpers.py | 78 ++++++++-- vllm_omni/core/sched/omni_ar_scheduler.py | 50 ++++++- .../core/sched/omni_generation_scheduler.py | 37 ++++- vllm_omni/core/sched/omni_scheduler_mixin.py | 28 +--- vllm_omni/data_entry_keys.py | 2 + .../distributed/omni_connectors/adapter.py | 31 ++++ .../chunk_transfer_adapter.py | 59 +++++++- vllm_omni/engine/orchestrator.py | 8 +- vllm_omni/entrypoints/openai/api_server.py | 16 --- .../entrypoints/openai/realtime_connection.py | 35 ++--- .../models/qwen3_omni/qwen3_omni.py | 8 +- .../models/qwen3_tts/prompt_embeds_builder.py | 8 +- .../stage_input_processors/qwen3_omni.py | 135 ++++++++++++++---- vllm_omni/worker/gpu_model_runner.py | 38 ++--- 20 files changed, 536 insertions(+), 184 deletions(-) create mode 100644 tests/core/sched/test_omni_ar_scheduler_streaming.py rename tests/core/sched/{test_omni_scheduler_mixin.py => test_omni_generation_scheduler_update_session.py} (79%) diff --git a/examples/online_serving/qwen3_omni/README.md b/examples/online_serving/qwen3_omni/README.md index a012cf21f..a13ab52df 100644 --- a/examples/online_serving/qwen3_omni/README.md +++ b/examples/online_serving/qwen3_omni/README.md @@ -18,10 +18,6 @@ Asynchronous chunk streaming operates as **enabled by default** within this bund Additionally, NPU, ROCm, and XPU per-platform configuration deltas are deterministically merged from the `platforms`: section of the corresponding YAML. -**Note:** The OpenAI-style **`/v1/realtime`** WebSocket interface (facilitating streaming PCM audio input alongside audio and transcription output) -is currently **unsupported** while the `async_chunk` configuration attribute is enabled. -It is requisite to instantiate the default omni architecture or utilize a deployment configuration specifying `async_chunk: false` to facilitate real-time streaming sessions. - To explicitly utilize a custom deployment YAML, mandate the configuration path accordingly: ```bash vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 \ @@ -289,7 +285,7 @@ python openai_realtime_client.py \ | `--num-requests` | `1` | Number of sequential sessions (see `--concurrency`) | | `--concurrency` | `1` | Max concurrent WebSocket sessions when `--num-requests` > 1 | -Ensure the server is running **without** `async_chunk` if you use `/v1/realtime`, for example: +Ensure the server is running, for example: ```bash vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 diff --git a/tests/core/sched/test_omni_ar_scheduler_streaming.py b/tests/core/sched/test_omni_ar_scheduler_streaming.py new file mode 100644 index 000000000..f6539a40a --- /dev/null +++ b/tests/core/sched/test_omni_ar_scheduler_streaming.py @@ -0,0 +1,92 @@ +"""Unit tests for Omni AR streaming-session async placeholder handling.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +# Imports must run in this order: vllm_omni applies patches to vllm.v1.request before +# Request / StreamingUpdate are bound in this module. Ruff isort would reorder them. +# isort: off +import vllm_omni # noqa: F401 - import for side effects (patch vLLM) +from vllm.sampling_params import SamplingParams +from vllm.v1.request import Request, RequestStatus, StreamingUpdate +from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler + +# isort: on + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + + +def _make_scheduler(*, stage_id: int = 0) -> OmniARScheduler: + sched = OmniARScheduler.__new__(OmniARScheduler) + sched._new_prompt_len_snapshot = {} + sched.vllm_config = SimpleNamespace(model_config=SimpleNamespace(stage_id=stage_id)) + sched.num_waiting_for_streaming_input = 0 + sched.log_stats = False + sched.chunk_transfer_adapter = None + return sched + + +def _make_request() -> Request: + return Request( + request_id="req-ar-streaming-test", + prompt_token_ids=[1, 2, 3], + sampling_params=SamplingParams(max_tokens=8), + pooling_params=None, + arrival_time=100.0, + block_hasher=None, + ) + + +def _make_update(prompt_token_ids: list[int] | None = None) -> StreamingUpdate: + return StreamingUpdate( + mm_features=None, + prompt_token_ids=[10, 20] if prompt_token_ids is None else prompt_token_ids, + max_tokens=32, + arrival_time=200.0, + sampling_params=SamplingParams(max_tokens=16), + ) + + +def test_stage0_streaming_update_discards_outstanding_async_placeholder_token() -> None: + sched = _make_scheduler(stage_id=0) + session = _make_request() + session.status = RequestStatus.WAITING_FOR_STREAMING_REQ + session.append_output_token_ids([7, 8, 9]) + session.num_computed_tokens = 6 + session.num_output_placeholders = 1 + session.spec_token_ids = [-1] + + sched._update_request_as_session(session, _make_update([10, 20])) + + assert session.discard_latest_async_tokens is True + assert session.num_output_placeholders == 0 + assert session.spec_token_ids == [] + # The async placeholder makes token 9 unconfirmed, so only 7 and 8 are + # carried into the next streaming prompt before the new chunk tokens. + assert session.prompt_token_ids == [1, 2, 3, 7, 8, 10, 20] + assert list(session._all_token_ids) == [1, 2, 3, 7, 8, 10, 20] + assert session._output_token_ids == [] + assert session.num_prompt_tokens == 7 + assert sched._new_prompt_len_snapshot[session.request_id] == 2 + + +def test_stage0_streaming_update_keeps_all_computed_tokens_without_placeholder() -> None: + sched = _make_scheduler(stage_id=0) + session = _make_request() + session.status = RequestStatus.WAITING_FOR_STREAMING_REQ + session.append_output_token_ids([7, 8, 9]) + session.num_computed_tokens = 6 + session.num_output_placeholders = 0 + + sched._update_request_as_session(session, _make_update([10, 20])) + + assert session.discard_latest_async_tokens is False + assert session.num_output_placeholders == 0 + assert session.prompt_token_ids == [1, 2, 3, 7, 8, 9, 10, 20] + assert list(session._all_token_ids) == [1, 2, 3, 7, 8, 9, 10, 20] + assert session._output_token_ids == [] + assert session.num_prompt_tokens == 8 + assert sched._new_prompt_len_snapshot[session.request_id] == 2 diff --git a/tests/core/sched/test_omni_scheduler_mixin.py b/tests/core/sched/test_omni_generation_scheduler_update_session.py similarity index 79% rename from tests/core/sched/test_omni_scheduler_mixin.py rename to tests/core/sched/test_omni_generation_scheduler_update_session.py index e04a9c39f..cf492e7cc 100644 --- a/tests/core/sched/test_omni_scheduler_mixin.py +++ b/tests/core/sched/test_omni_generation_scheduler_update_session.py @@ -1,6 +1,6 @@ -"""Unit tests for OmniSchedulerMixin streaming session replacement. +"""Unit tests for generation streaming session replacement. -These tests pin the behavior of `_replace_session_with_streaming_update` against +These tests pin the behavior of `_update_request_as_session` against current vLLM `Request` / `StreamingUpdate` (and Omni patches). When upgrading vLLM, failures here should highlight incompatible changes to request state or update payloads early. @@ -19,19 +19,34 @@ import vllm_omni # noqa: F401 - import for side effects (patch vLLM) from vllm.sampling_params import SamplingParams from vllm.v1.engine import EngineCoreEventType from vllm.v1.request import Request, RequestStatus, StreamingUpdate -from vllm_omni.core.sched.omni_scheduler_mixin import OmniSchedulerMixin +from vllm_omni.core.sched.omni_generation_scheduler import OmniGenerationScheduler # isort: on pytestmark = [pytest.mark.core_model, pytest.mark.cpu] -class _SchedulerStub(OmniSchedulerMixin): - """Minimal scheduler surface required by OmniSchedulerMixin.""" +class _SkippedWaitingStub: + def __contains__(self, request: Request) -> bool: + return False + + +class _ChunkTransferAdapterStub: + def __init__(self) -> None: + self.segment_finished_requests: set[str] = set() + + +class _SchedulerStub(OmniGenerationScheduler): + """Minimal scheduler surface required by OmniGenerationScheduler.""" def __init__(self, *, log_stats: bool = False) -> None: self.num_waiting_for_streaming_input = 0 self.log_stats = log_stats + self.chunk_transfer_adapter = _ChunkTransferAdapterStub() + self.skipped_waiting = _SkippedWaitingStub() + + def _enqueue_waiting_request(self, session: Request) -> None: + raise AssertionError("unexpected enqueue for skipped_waiting miss") def _make_request(**kwargs) -> Request: @@ -71,7 +86,7 @@ class TestReplaceSessionWithStreamingUpdate: update = _make_update(prompt_token_ids=[40, 41, 42]) sched.num_waiting_for_streaming_input = 3 - sched._replace_session_with_streaming_update(session, update) + sched._update_request_as_session(session, update) assert session._output_token_ids == [] assert list(session._all_token_ids) == [40, 41, 42] @@ -88,7 +103,7 @@ class TestReplaceSessionWithStreamingUpdate: session = _make_request() session.status = RequestStatus.RUNNING update = _make_update(prompt_token_ids=None) - sched._replace_session_with_streaming_update(session, update) + sched._update_request_as_session(session, update) assert session.prompt_token_ids == () assert list(session._all_token_ids) == [] @@ -108,7 +123,7 @@ class TestReplaceSessionWithStreamingUpdate: pytest.skip("StreamingUpdate has no additional_information (Omni patch inactive?)") update = replace(base, additional_information=None) - sched._replace_session_with_streaming_update(session, update) + sched._update_request_as_session(session, update) assert session.additional_information is None def test_does_not_decrement_waiting_when_not_streaming_status(self) -> None: @@ -116,14 +131,14 @@ class TestReplaceSessionWithStreamingUpdate: session = _make_request() session.status = RequestStatus.RUNNING sched.num_waiting_for_streaming_input = 5 - sched._replace_session_with_streaming_update(session, _make_update()) + sched._update_request_as_session(session, _make_update()) assert sched.num_waiting_for_streaming_input == 5 def test_records_queued_event_when_log_stats_enabled(self) -> None: sched = _SchedulerStub(log_stats=True) session = _make_request() session.status = RequestStatus.WAITING_FOR_STREAMING_REQ - sched._replace_session_with_streaming_update(session, _make_update()) + sched._update_request_as_session(session, _make_update()) assert session.events assert session.events[-1].type == EngineCoreEventType.QUEUED diff --git a/tests/distributed/omni_connectors/test_chunk_transfer_adapter.py b/tests/distributed/omni_connectors/test_chunk_transfer_adapter.py index 24ba819b1..8bfe837e2 100644 --- a/tests/distributed/omni_connectors/test_chunk_transfer_adapter.py +++ b/tests/distributed/omni_connectors/test_chunk_transfer_adapter.py @@ -230,9 +230,11 @@ def test_send_single_request_struct_without_meta_does_not_crash(build_adapter, m cleanup_calls = [] monkeypatch.setattr(adapter, "cleanup", lambda *a, **kw: cleanup_calls.append((a, kw))) - adapter._send_single_request({"pooling_output": None, "request": request, "is_finished": False}) + adapter._send_single_request( + {"pooling_output": None, "request": request, "is_finished": False, "is_segment_finished": False} + ) - assert cleanup_calls == [] # no terminal cleanup; meta.finished is unobservable + assert cleanup_calls == [] # no terminal cleanup; meta.finished is false def test_send_single_request_empty_struct_goes_on_wire(build_adapter, monkeypatch): @@ -248,12 +250,50 @@ def test_send_single_request_empty_struct_goes_on_wire(build_adapter, monkeypatc adapter.custom_process_next_stage_input_func = lambda **kwargs: OmniPayloadStruct() monkeypatch.setattr(adapter, "cleanup", lambda *a, **kw: None) - adapter._send_single_request({"pooling_output": None, "request": request, "is_finished": False}) + adapter._send_single_request( + {"pooling_output": None, "request": request, "is_finished": False, "is_segment_finished": False} + ) assert connector.put.called sent_payload = connector.put.call_args.kwargs["data"] assert isinstance(sent_payload, OmniPayloadStruct) - assert sent_payload.meta is None # confirms it's the empty struct on the wire + assert sent_payload.meta.finished.item() is False + assert sent_payload.meta.is_segment_finished.item() is False + + +def test_send_single_request_struct_preserves_segment_finished(build_adapter, monkeypatch): + adapter, connector = build_adapter(stage_id=1) + request = _req("req-segment", RequestStatus.WAITING, external_req_id="ext-segment") + + adapter.custom_process_next_stage_input_func = lambda **kwargs: OmniPayloadStruct() + monkeypatch.setattr(adapter, "cleanup", lambda *a, **kw: None) + + adapter._send_single_request( + {"pooling_output": None, "request": request, "is_finished": False, "is_segment_finished": True} + ) + + sent_payload = connector.put.call_args.kwargs["data"] + assert sent_payload.meta.finished.item() is False + assert sent_payload.meta.is_segment_finished.item() is True + + +def test_save_async_skips_stale_resumable_chunk_until_dedup_is_reset(build_adapter): + adapter, _ = build_adapter(stage_id=1) + request = _req("req-stream", RequestStatus.WAITING, external_req_id="ext-stream") + request.resumable = True + request.num_computed_tokens = 0 + adapter.requests_num_chunks_sent["ext-stream"] = 111 + + adapter.save_async(pooling_output=None, request=request, is_segment_finished=False) + + assert len(adapter._pending_save_reqs) == 0 + assert adapter.requests_num_chunks_sent["ext-stream"] == 111 + + adapter.requests_num_chunks_sent.pop("ext-stream") + adapter.save_async(pooling_output=None, request=request, is_segment_finished=False) + + assert len(adapter._pending_save_reqs) == 1 + assert adapter.requests_num_chunks_sent["ext-stream"] == 0 def test_send_single_request_cleans_up_after_finished_payload(build_adapter, monkeypatch): @@ -266,7 +306,9 @@ def test_send_single_request_cleans_up_after_finished_payload(build_adapter, mon cleanup_calls = [] monkeypatch.setattr(adapter, "cleanup", lambda *a, **kw: cleanup_calls.append((a, kw))) - adapter._send_single_request({"pooling_output": None, "request": request, "is_finished": True}) + adapter._send_single_request( + {"pooling_output": None, "request": request, "is_finished": True, "is_segment_finished": True} + ) assert len(cleanup_calls) == 1 args, _ = cleanup_calls[0] diff --git a/tests/engine/test_orchestrator.py b/tests/engine/test_orchestrator.py index 9e050a3f8..55e3edc41 100644 --- a/tests/engine/test_orchestrator.py +++ b/tests/engine/test_orchestrator.py @@ -964,6 +964,7 @@ async def test_handle_streaming_update_passes_prompt_text_to_stage_pool() -> Non pool = RecordingPool() orchestrator = object.__new__(Orchestrator) + orchestrator.async_chunk = False orchestrator.request_states = { "req-stream": OrchestratorRequestState( request_id="req-stream", diff --git a/tests/entrypoints/openai_api/test_qwen3_omni_realtime_websocket.py b/tests/entrypoints/openai_api/test_qwen3_omni_realtime_websocket.py index 00a6565d9..39b27103e 100644 --- a/tests/entrypoints/openai_api/test_qwen3_omni_realtime_websocket.py +++ b/tests/entrypoints/openai_api/test_qwen3_omni_realtime_websocket.py @@ -44,7 +44,6 @@ realtime_server_params = [ model=MODEL, stage_config_path=default_stage_config, use_stage_cli=True, - server_args=["--no-async-chunk"], ), id="default", ), diff --git a/tests/model_executor/stage_input_processors/test_qwen3_omni_streaming_helpers.py b/tests/model_executor/stage_input_processors/test_qwen3_omni_streaming_helpers.py index a04a75de8..4b2bdf3fe 100644 --- a/tests/model_executor/stage_input_processors/test_qwen3_omni_streaming_helpers.py +++ b/tests/model_executor/stage_input_processors/test_qwen3_omni_streaming_helpers.py @@ -20,7 +20,7 @@ def _streaming_context() -> SimpleNamespace: def test_get_streaming_talker_tokens_first_segment(_streaming_context: SimpleNamespace) -> None: - inc_p, inc_o, merged, thinker_in = q3._get_streaming_talker_tokens( + inc_p, inc_o = q3._get_streaming_talker_tokens( "r1", [1, 2], [10, 11], @@ -28,13 +28,11 @@ def test_get_streaming_talker_tokens_first_segment(_streaming_context: SimpleNam ) assert inc_p == [1, 2] assert inc_o == [10, 11] - assert merged == [1, 2, 10, 11] - assert thinker_in == [1, 2] def test_get_streaming_talker_tokens_second_segment_accumulates(_streaming_context: SimpleNamespace) -> None: q3._get_streaming_talker_tokens("r2", [1, 2], [10, 11], streaming_context=_streaming_context) - inc_p, inc_o, merged, thinker_in = q3._get_streaming_talker_tokens( + inc_p, inc_o = q3._get_streaming_talker_tokens( "r2", [1, 2, 3, 4], [10, 11, 12, 13], @@ -42,14 +40,12 @@ def test_get_streaming_talker_tokens_second_segment_accumulates(_streaming_conte ) assert inc_p == [3, 4] assert inc_o == [12, 13] - assert merged == [1, 2, 10, 3, 4, 12, 13] - assert thinker_in == [1, 2, 10, 3, 4] def test_get_streaming_talker_tokens_new_prompt_len_snapshot_truncates( _streaming_context: SimpleNamespace, ) -> None: - inc_p, inc_o, merged, thinker_in = q3._get_streaming_talker_tokens( + inc_p, inc_o = q3._get_streaming_talker_tokens( "r3", [1, 2, 3, 4, 5, 6], [10], @@ -58,8 +54,6 @@ def test_get_streaming_talker_tokens_new_prompt_len_snapshot_truncates( ) assert inc_p == [1, 2, 3, 4] assert inc_o == [10] - assert merged == [1, 2, 3, 4, 10] - assert thinker_in == [1, 2, 3, 4] def test_get_streaming_talker_tokens_clear_state(_streaming_context: SimpleNamespace) -> None: @@ -82,6 +76,72 @@ def test_get_streaming_codec_delta_len_increments_and_finishes(_streaming_contex assert state.talker2code2wav_last_seq_len == 0 +def test_streaming_input_prefill_chunk_is_cached() -> None: + transfer_manager = SimpleNamespace(_pending_streaming_prefills={}) + request = SimpleNamespace( + external_req_id="rt-1", + output_token_ids=[100], + all_token_ids=[151644, 872, 100], + prompt_token_ids=[151644, 872], + resumable=True, + additional_information=None, + ) + thinker_emb = torch.ones(2, 3) + thinker_hid = torch.full((2, 3), 2.0) + + payload = q3._construct_thinker2talker_streaming_input_async_chunk( + False, + request, + thinker_emb, + thinker_hid, + transfer_manager, + ) + + assert payload is None + cached = transfer_manager._pending_streaming_prefills["rt-1"] + assert torch.equal(cached["embed"]["prefill"], thinker_emb) + assert torch.equal(cached["hidden_states"]["output"], thinker_hid) + assert cached["ids"]["all"] == request.all_token_ids + assert cached["ids"]["prompt"] == request.prompt_token_ids + + +def test_streaming_input_prefill_flushes_with_next_decode_chunk() -> None: + transfer_manager = SimpleNamespace( + _pending_streaming_prefills={ + "rt-2": { + "embed": {"prefill": torch.ones(2, 3)}, + "hidden_states": {"output": torch.full((2, 3), 2.0)}, + "ids": {"all": [151644, 872, 100], "prompt": [151644, 872]}, + } + } + ) + request = SimpleNamespace( + external_req_id="rt-2", + output_token_ids=[101], + all_token_ids=[151644, 872, 100, 101], + prompt_token_ids=[151644, 872], + resumable=True, + additional_information=None, + ) + thinker_emb = torch.full((1, 3), 3.0) + thinker_hid = torch.full((1, 3), 4.0) + + payload = q3._construct_thinker2talker_streaming_input_async_chunk( + False, + request, + thinker_emb, + thinker_hid, + transfer_manager, + ) + + assert payload is not None + assert payload.embed.prefill.shape == (3, 3) + assert payload.hidden_states.output.shape == (3, 3) + assert payload.ids.all == [151644, 872, 100] + assert payload.ids.prompt == [151644, 872] + assert "rt-2" not in transfer_manager._pending_streaming_prefills + + def test_talker2code2wav_full_payload_filters_by_output_token_ids() -> None: request = SimpleNamespace( request_id="codec", diff --git a/vllm_omni/core/sched/omni_ar_scheduler.py b/vllm_omni/core/sched/omni_ar_scheduler.py index 3d51765a9..5c0c489d1 100644 --- a/vllm_omni/core/sched/omni_ar_scheduler.py +++ b/vllm_omni/core/sched/omni_ar_scheduler.py @@ -13,7 +13,7 @@ from vllm.v1.core.sched.async_scheduler import AsyncScheduler as AsyncVLLMSchedu from vllm.v1.core.sched.output import SchedulerOutput from vllm.v1.core.sched.scheduler import Scheduler as VLLMScheduler from vllm.v1.core.sched.utils import remove_all -from vllm.v1.engine import EngineCoreOutput, EngineCoreOutputs +from vllm.v1.engine import EngineCoreEventType, EngineCoreOutput, EngineCoreOutputs from vllm.v1.metrics.perf import PerfStats from vllm.v1.outputs import ModelRunnerOutput from vllm.v1.request import Request, RequestStatus, StreamingUpdate @@ -426,8 +426,19 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler): # Capture finish_reason BEFORE _handle_stopped_request, which may # reset the status to WAITING for streaming requests that continue. finish_reason = request.get_finished_reason() - is_segment_finished = request.is_finished() and request.resumable + is_segment_finished = True finished = self._handle_stopped_request(request) + if not finished: + # for streaming input request only + if self.chunk_transfer_adapter: + if self.vllm_config.model_config.stage_id != 0: + # Downstream async-chunk stages receive real payloads from the + # connector. This update only resumes polling for the next segment. + self.chunk_transfer_adapter.segment_finished_requests.discard(request.request_id) + request.discard_latest_async_tokens = True + request.num_output_placeholders = 0 + request.spec_token_ids = [] + request._output_token_ids.clear() if finished: kv_transfer_params = self._free_request(request) if status_before_stop == RequestStatus.RUNNING: @@ -471,7 +482,7 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler): ) ) if self.chunk_transfer_adapter is not None: - self.chunk_transfer_adapter.save_async(pooler_output, request) + self.chunk_transfer_adapter.save_async(pooler_output, request, is_segment_finished) else: # Invariant: EngineCore returns no partial prefill outputs. assert not prompt_logprobs_tensors @@ -622,11 +633,36 @@ class OmniARScheduler(OmniSchedulerMixin, VLLMScheduler): """ req_id = session.request_id self._new_prompt_len_snapshot[req_id] = len(update.prompt_token_ids) - if self.vllm_config.model_config.stage_id != 0: - self._replace_session_with_streaming_update(session, update) + outstanding_async_tokens = getattr(session, "num_output_placeholders", 0) + if outstanding_async_tokens > 0: + # Async scheduling may already have sampled the previous + # segment's next token. Drop that late token instead of + # appending it to the new streaming segment. + session.discard_latest_async_tokens = True + session.num_computed_tokens -= session.num_output_placeholders + session.num_output_placeholders = 0 + session.spec_token_ids = [] + if self.chunk_transfer_adapter: + self.chunk_transfer_adapter.requests_num_chunks_sent.pop(session.external_req_id, None) + if self.vllm_config.model_config.stage_id != 0: + # Downstream async-chunk stages receive real payloads from the + # connector. This update only resumes polling for the next segment. + self.chunk_transfer_adapter.segment_finished_requests.discard(session.request_id) + # Do not replace prompt/additional_information here; the next + # upstream chunk will populate them in chunk transfer adapter. + session.arrival_time = update.arrival_time + session.sampling_params = update.sampling_params + if session.status == RequestStatus.WAITING_FOR_STREAMING_REQ: + self.num_waiting_for_streaming_input -= 1 + session.status = RequestStatus.WAITING + if session in self.skipped_waiting: + self.skipped_waiting.remove_requests((session,)) + self._enqueue_waiting_request(session) - else: - super()._update_request_as_session(session, update) + if self.log_stats: + session.record_event(EngineCoreEventType.QUEUED) + return + super()._update_request_as_session(session, update) def _free_request(self, request: Request, delay_free_blocks: bool = False) -> dict[str, Any] | None: # TODO(wzliu)! for offline mode, we should not end process until all data is transferred diff --git a/vllm_omni/core/sched/omni_generation_scheduler.py b/vllm_omni/core/sched/omni_generation_scheduler.py index 8d6655742..dae3be8ae 100644 --- a/vllm_omni/core/sched/omni_generation_scheduler.py +++ b/vllm_omni/core/sched/omni_generation_scheduler.py @@ -108,9 +108,8 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler): break # async_chunk: don't schedule placeholder tokens when no new chunk is available. if required_tokens <= 0: - if ( - self.chunk_transfer_adapter is not None - and request.request_id in self.chunk_transfer_adapter.finished_requests + if self.chunk_transfer_adapter is not None and self.chunk_transfer_adapter.is_done_receiving_chunks( + request.request_id ): self._pending_finish_reqs.append(request) req_index += 1 @@ -160,7 +159,7 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler): # async_chunk: wait for the first upstream chunk (don't start with placeholders). if self.chunk_transfer_adapter is not None and len(request.prompt_token_ids) == 0: - if request.request_id in self.chunk_transfer_adapter.finished_requests: + if self.chunk_transfer_adapter.is_done_receiving_chunks(request.request_id): self.waiting.pop_request() self._pending_finish_reqs.append(request) continue @@ -489,7 +488,7 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler): or (self.chunk_transfer_adapter is None and request.num_computed_tokens >= request.num_prompt_tokens) or ( self.chunk_transfer_adapter is not None - and request.request_id in self.chunk_transfer_adapter.finished_requests + and self.chunk_transfer_adapter.is_done_receiving_chunks(request.request_id) and request.num_computed_tokens >= len(request.prompt_token_ids) ) ): @@ -507,6 +506,10 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler): routed_experts = model_runner_output.routed_experts_dict[req_id] finish_reason = request.get_finished_reason() finished = self._handle_stopped_request(request) + if not finished: + # for streaming input request only + if self.chunk_transfer_adapter: + self.chunk_transfer_adapter.segment_finished_requests.discard(req_id) if finished: kv_transfer_params = self._free_request(request) if self.chunk_transfer_adapter is not None: @@ -669,4 +672,26 @@ class OmniGenerationScheduler(OmniSchedulerMixin, VLLMScheduler): Do not expend prompt id using update. """ - self._replace_session_with_streaming_update(session, update) + if self.chunk_transfer_adapter: + self.chunk_transfer_adapter.segment_finished_requests.discard(session.request_id) + session._output_token_ids.clear() + session._all_token_ids.clear() + new_prompt = update.prompt_token_ids or () + session._all_token_ids.extend(new_prompt) + session.num_computed_tokens = 0 + session.prompt_token_ids = update.prompt_token_ids or () + session.additional_information = update.additional_information or None + # Update block hashes for the new tokens. + session.update_block_hashes() + session.num_prompt_tokens = len(session.prompt_token_ids) + session.arrival_time = update.arrival_time + session.sampling_params = update.sampling_params + if session.status == RequestStatus.WAITING_FOR_STREAMING_REQ: + self.num_waiting_for_streaming_input -= 1 + session.status = RequestStatus.WAITING + if session in self.skipped_waiting: + self.skipped_waiting.remove_requests((session,)) + self._enqueue_waiting_request(session) + + if self.log_stats: + session.record_event(EngineCoreEventType.QUEUED) diff --git a/vllm_omni/core/sched/omni_scheduler_mixin.py b/vllm_omni/core/sched/omni_scheduler_mixin.py index 570fa5545..c4b1e2429 100644 --- a/vllm_omni/core/sched/omni_scheduler_mixin.py +++ b/vllm_omni/core/sched/omni_scheduler_mixin.py @@ -5,8 +5,7 @@ from typing import Any from vllm.logger import init_logger from vllm.v1.core.sched.output import SchedulerOutput -from vllm.v1.engine import EngineCoreEventType -from vllm.v1.request import Request, RequestStatus, StreamingUpdate +from vllm.v1.request import RequestStatus from vllm_omni.core.sched.output import OmniChunkRecvHandle, OmniSchedulerOutput @@ -146,28 +145,3 @@ class OmniSchedulerMixin: finished_requests_needing_kv_transfer=finished_requests_needing_kv_transfer or {}, pending_input_registrations=pending_input_registrations, ) - - def _replace_session_with_streaming_update( - self, - session: Request, - update: StreamingUpdate, - ) -> None: - """For streaming input: Replace an existing streaming session payload with the latest update.""" - session._output_token_ids.clear() - session._all_token_ids.clear() - new_prompt = update.prompt_token_ids or () - session._all_token_ids.extend(new_prompt) - session.num_computed_tokens = 0 - session.prompt_token_ids = update.prompt_token_ids or () - session.additional_information = update.additional_information or None - # Update block hashes for the new tokens. - session.update_block_hashes() - session.num_prompt_tokens = len(session.prompt_token_ids) - session.arrival_time = update.arrival_time - session.sampling_params = update.sampling_params - if session.status == RequestStatus.WAITING_FOR_STREAMING_REQ: - self.num_waiting_for_streaming_input -= 1 - session.status = RequestStatus.WAITING - - if self.log_stats: - session.record_event(EngineCoreEventType.QUEUED) diff --git a/vllm_omni/data_entry_keys.py b/vllm_omni/data_entry_keys.py index b239ff5ed..09316b55e 100644 --- a/vllm_omni/data_entry_keys.py +++ b/vllm_omni/data_entry_keys.py @@ -59,6 +59,7 @@ class Ids(TypedDict, total=False): class OmniPayloadMeta(TypedDict, total=False): finished: torch.Tensor + is_segment_finished: torch.Tensor stream_finished: torch.Tensor req_id: list[str] left_context_size: int @@ -142,6 +143,7 @@ class IdsStruct(_StructBase): class MetaStruct(_StructBase): finished: torch.Tensor | None = None + is_segment_finished: torch.Tensor | None = None stream_finished: torch.Tensor | None = None req_id: list[str] | None = None left_context_size: int | None = None diff --git a/vllm_omni/distributed/omni_connectors/adapter.py b/vllm_omni/distributed/omni_connectors/adapter.py index 79a824aa3..db253ebad 100644 --- a/vllm_omni/distributed/omni_connectors/adapter.py +++ b/vllm_omni/distributed/omni_connectors/adapter.py @@ -214,3 +214,34 @@ def compute_talker_prompt_ids_length(prompt_ids: list[int]) -> int: pass return sum_user_len + assistant_len + + +def construct_next_stage_streaming_input_prompt(payload_data: dict[str, Any], request: Any) -> None: + """Update a downstream streaming request prompt from connector payload ids. + + Async-chunk downstream stages are prewarmed before the real Talker prompt is + known. When a Thinker payload carries ``ids.prompt``, this helper: + + * Preserves ``num_computed_tokens`` (the scheduler token watermark). + * Moves already-computed output tokens into ``prompt_token_ids``. + * Appends a new placeholder prompt slice sized from the upstream ids. + * Refreshes block hashes so the scheduler allocates KV slots for the + extended prompt without discarding prior computed state. + """ + ids = payload_data.get("ids", {}) + prompt_token_ids = ids.get("prompt", None) + if not prompt_token_ids: + return + num_computed_tokens = request.num_computed_tokens + kept_output_tokens = request._all_token_ids[request.num_prompt_tokens : num_computed_tokens] + del request._all_token_ids[num_computed_tokens:] + request._output_token_ids.clear() + assert request.prompt_token_ids is not None + # Extend prompt with kept output tokens. + request.prompt_token_ids.extend(kept_output_tokens) + next_prompt_len = max(1, compute_talker_prompt_ids_length(prompt_token_ids)) + new_prompt = [0] * next_prompt_len + request._all_token_ids.extend(new_prompt or ()) + request.prompt_token_ids.extend(new_prompt or ()) + request.update_block_hashes() + request.num_prompt_tokens = len(request.prompt_token_ids) diff --git a/vllm_omni/distributed/omni_connectors/transfer_adapter/chunk_transfer_adapter.py b/vllm_omni/distributed/omni_connectors/transfer_adapter/chunk_transfer_adapter.py index 5686c7696..0b6e0b572 100644 --- a/vllm_omni/distributed/omni_connectors/transfer_adapter/chunk_transfer_adapter.py +++ b/vllm_omni/distributed/omni_connectors/transfer_adapter/chunk_transfer_adapter.py @@ -9,8 +9,9 @@ from typing import Any import torch from vllm.v1.request import Request, RequestStatus -from vllm_omni.data_entry_keys import OmniPayloadStruct, unflatten_payload +from vllm_omni.data_entry_keys import MetaStruct, OmniPayloadStruct, unflatten_payload +from ..adapter import construct_next_stage_streaming_input_prompt from ..factory import OmniConnectorFactory from ..utils.config import ConnectorSpec from ..utils.logging import get_connector_logger @@ -54,6 +55,7 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase): self.put_req_chunk: dict[str, int] = defaultdict(int) self.get_req_chunk: dict[str, int] = defaultdict(int) self.finished_requests: set[str] = set() + self.segment_finished_requests: set[str] = set() self.request_payload = {} self.code_prompt_token_ids: dict[str, list[torch.Tensor]] = defaultdict(list) self.request_ids_mapping: dict[str, str] = {} @@ -63,6 +65,7 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase): self.requests_with_ready_chunks = set() self.requests_origin_status = {} self.requests_num_chunks_sent: dict[str, int] = defaultdict(int) + self._pending_streaming_prefills: dict[str, dict] = {} @staticmethod def _is_truthy_scalar(value: Any) -> bool: @@ -123,16 +126,26 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase): self, pooling_output: torch.Tensor | None = None, request: Request | None = None, + is_segment_finished: bool = False, ): """Build and enqueue one chunk for asynchronous sending. Payload extraction happens in ``_send_single_request`` on the background save_loop thread. + For streaming input request ``is_segment_finished`` marks the end + of the current realtime input segment. It is intentionally separate + from ``request.is_finished()``: a resumable `/v1/realtime` session + can finish one audio segment and later continue with another segment + under the same external request id. For other requests, it is the same + as ``request.is_finished()``. + Args: pooling_output: Partial pooling output dictionary request: Request object + is_segment_finished: whether the segment of request is finished """ + is_finished = request.is_finished() and not request.resumable confirmed_num_computed_tokens = self._confirmed_num_computed_tokens(request) @@ -150,7 +163,8 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase): task = { "pooling_output": pooling_output, "request": request, - "is_finished": request.is_finished(), + "is_finished": is_finished, + "is_segment_finished": is_segment_finished, } self._pending_save_reqs.append(task) with self._save_cond: @@ -185,13 +199,24 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase): meta = payload_data.get("meta", {}) payload_finished = self._is_truthy_scalar(meta.get("finished")) + payload_segment_finished = self._is_truthy_scalar(meta.get("is_segment_finished")) if self.model_mode == "ar": request.additional_information = payload_data + if chunk_id > 0 and request.resumable: + # For new streaming input segment, we should update prompt from payload + construct_next_stage_streaming_input_prompt(payload_data, request) + if payload_finished: self.finished_requests.add(req_id) + request.resumable = False + if payload_segment_finished: + self.segment_finished_requests.add(req_id) else: if payload_finished: self.finished_requests.add(req_id) + request.resumable = False + if payload_segment_finished: + self.segment_finished_requests.add(req_id) new_ids = payload_data.get("codes", {}).get("audio") has_tensor_codes = isinstance(new_ids, torch.Tensor) @@ -249,6 +274,7 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase): pooling_output = unflatten_payload(raw_po) if isinstance(raw_po, dict) else raw_po request = task["request"] is_finished = task["is_finished"] + is_segment_finished = task["is_segment_finished"] stage_id = self.connector.stage_id next_stage_id = stage_id + 1 external_req_id = request.external_req_id @@ -262,14 +288,23 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase): transfer_manager=self, pooling_output=pooling_output, request=request, - is_finished=is_finished, + # Existing processors use is_finished as a flush signal. + is_finished=is_segment_finished, ) except Exception as e: logger.error(f"Failed to use custom_process_input_func for payload extraction: {e}") if payload_data is None: - return + if not (is_segment_finished or is_finished): + return + # Segment/request finish markers must still reach downstream even when + # the processor has no tensor payload. + payload_data = OmniPayloadStruct() + if payload_data.meta is None: + payload_data.meta = MetaStruct() + payload_data.meta.finished = torch.tensor(is_finished, dtype=torch.bool) + payload_data.meta.is_segment_finished = torch.tensor(is_segment_finished, dtype=torch.bool) success, size, metadata = self.connector.put( from_stage=str(stage_id), @@ -298,13 +333,22 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase): if is_payload_finished: self.cleanup(request.request_id, external_req_id) - if is_finished: + if is_segment_finished: self.code_prompt_token_ids.pop(external_req_id, None) self.requests_num_chunks_sent.pop(external_req_id, None) cached_ic = getattr(self, "_cached_ic", None) if cached_ic is not None: cached_ic.pop(external_req_id, None) + def is_done_receiving_chunks(self, request_id: str) -> bool: + """Return True if the request should stop polling upstream chunks. + + Covers both the whole-request finish marker (``finished_requests``) and + the per-segment finish marker (``segment_finished_requests``) used while + waiting for the next streaming input slice. + """ + return request_id in self.finished_requests or request_id in self.segment_finished_requests + ######################################################################## # Cleanup ######################################################################## @@ -319,6 +363,7 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase): Idempotent: calling with an already-cleaned or unknown id is safe. """ self.finished_requests.discard(request_id) + self.segment_finished_requests.discard(request_id) self.get_req_chunk.pop(request_id, None) self.requests_with_ready_chunks.discard(request_id) self.request_ids_mapping.pop(request_id, None) @@ -339,6 +384,7 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase): self.request_payload.pop(external_req_id, None) self.code_prompt_token_ids.pop(external_req_id, None) self.requests_num_chunks_sent.pop(external_req_id, None) + self._pending_streaming_prefills.pop(external_req_id, None) cached_ic = getattr(self, "_cached_ic", None) if cached_ic is not None: @@ -459,7 +505,8 @@ class OmniChunkTransferAdapter(OmniTransferAdapterBase): # Requests that have loaded chunk from last round # of schedule, but have not scheduled continue - if request.request_id in self.finished_requests: + if self.is_done_receiving_chunks(request.request_id): + request.additional_information = None continue # Requests that waiting for chunk self.load_async(request) diff --git a/vllm_omni/engine/orchestrator.py b/vllm_omni/engine/orchestrator.py index 48026abab..791b6d5d9 100644 --- a/vllm_omni/engine/orchestrator.py +++ b/vllm_omni/engine/orchestrator.py @@ -416,7 +416,7 @@ class Orchestrator: stage_id = 0 request_id = msg.request_id request = msg.prompt - + final_stage_id = msg.final_stage_id req_state = self.request_states.get(request_id) if req_state is None: logger.warning( @@ -449,6 +449,9 @@ class Orchestrator: prompt_text=msg.output_prompt_text, ) + if self.async_chunk and stage_id == 0 and final_stage_id > 0: + await self._prewarm_async_chunk_stages(request_id, request, req_state) + async def _handle_add_companion(self, msg: AddCompanionRequestMessage) -> None: """Handle an add_companion_request message: submit companion to stage 0.""" companion_id = msg.companion_id @@ -1160,12 +1163,13 @@ class Orchestrator: base_input["prompt_token_ids"] = [0] * next_prompt_len base_input["multi_modal_data"] = None base_input["mm_processor_kwargs"] = None - + downstream_resumable = bool(getattr(stage0_request, "resumable", req_state.streaming.enabled)) request = build_engine_core_request_from_tokens( request_id=request_id, prompt=base_input, params=params, model_config=next_pool.stage_vllm_config.model_config, + resumable=downstream_resumable, ) request.external_req_id = request.request_id await next_pool.submit_initial(request_id, req_state, request, prompt_text=None) diff --git a/vllm_omni/entrypoints/openai/api_server.py b/vllm_omni/entrypoints/openai/api_server.py index d8d59285d..c13001063 100644 --- a/vllm_omni/entrypoints/openai/api_server.py +++ b/vllm_omni/entrypoints/openai/api_server.py @@ -1381,22 +1381,6 @@ async def streaming_video_chat(websocket: WebSocket): @router.websocket("/v1/realtime") async def realtime_websocket(websocket: WebSocket): """WebSocket endpoint for OpenAI-style realtime interactions.""" - engine_client = getattr(websocket.app.state, "engine_client", None) - if engine_client is not None and getattr(engine_client, "async_chunk", False): - await websocket.accept() - await websocket.send_json( - { - "type": "error", - "error": ( - "The /v1/realtime API is not supported when async_chunk is enabled on the server. " - "Use a stage configuration with async_chunk disabled and restart the server before using " - "this endpoint." - ), - "code": "unsupported", - } - ) - await websocket.close() - return serving = getattr(websocket.app.state, "openai_serving_realtime", None) if serving is None: await websocket.accept() diff --git a/vllm_omni/entrypoints/openai/realtime_connection.py b/vllm_omni/entrypoints/openai/realtime_connection.py index f2ce75df2..e04e8cdf9 100644 --- a/vllm_omni/entrypoints/openai/realtime_connection.py +++ b/vllm_omni/entrypoints/openai/realtime_connection.py @@ -120,7 +120,6 @@ class RealtimeConnection(VllmRealtimeConnection): full_text = "" prompt_token_ids_len = 0 completion_tokens_len = 0 - last_prompt_token_ids_len = 0 # detect Stage-0 segment rollover self._realtime_audio_ref = None # Coerce cumulative outputs to delta outputs; this ensures @@ -139,40 +138,26 @@ class RealtimeConnection(VllmRealtimeConnection): ) async for output in result_gen: - # Handle delta texts; this is very similar to the client from vLLM - if output.outputs and len(output.outputs) > 0: + stage_id = getattr(output, "stage_id", None) + if stage_id == 0 and output.outputs: first_output = output.outputs[0] new_token_ids = list(first_output.token_ids) - new_tokens_len = len(new_token_ids) - - cur_prompt_token_ids_len = len(output.prompt_token_ids or []) - # Stage-0 segment rollover: buffer_realtime_audio may yield - # multiple TokensPrompt segments and the second segment's - # prompt_token_ids include the first segment's decoded output. - # Clear accumulated full_text at the boundary so the - # carried-over prefix is not duplicated in the final - # TranscriptionDone.text. - stage_id = getattr(output, "stage_id", None) - if stage_id == 0 and cur_prompt_token_ids_len > last_prompt_token_ids_len > 0: - full_text = "" - last_prompt_token_ids_len = cur_prompt_token_ids_len - - if not prompt_token_ids_len and output.prompt_token_ids: - prompt_token_ids_len = len(output.prompt_token_ids) - - if new_tokens_len: + if new_token_ids: input_stream.put_nowait(new_token_ids) + if output.prompt_token_ids: + prompt_token_ids_len = max( + prompt_token_ids_len, + len(output.prompt_token_ids), + ) + delta_text = first_output.text or "" full_text += delta_text + completion_tokens_len += len(new_token_ids) - # append output to input if there was any delta text if delta_text: await self.send(TranscriptionDelta(delta=delta_text)) - completion_tokens_len += new_tokens_len - - # Handle audio chunking; this is Omni specific audio_chunks, sample_rate = self._extract_audio_chunks(output) for chunk in audio_chunks: diff --git a/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py b/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py index 1f637fcd4..6bd8bf305 100644 --- a/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py +++ b/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py @@ -183,11 +183,6 @@ class Qwen3OmniMoeForConditionalGeneration( # sync stall; build_mm_cpu handles the eventual D2H at payload time. ("codes", "audio"), } - # Keys that need to be accumulated across streaming inputs - self.streaming_accumulated_keys: set[tuple[str, str]] = { - ("embed", "prefill"), - ("hidden_states", "output"), - } elif self.model_stage == "code2wav": multimodal_config.skip_mm_profiling = True @@ -685,7 +680,8 @@ class Qwen3OmniMoeForConditionalGeneration( is_prefill = bool(payload.get("_omni_is_prefill", span_len > 1)) if is_prefill: num_computed_tokens = payload.get("_omni_num_computed_tokens") - if num_computed_tokens is not None: + request_resumable = meta.get("resumable", False) + if num_computed_tokens is not None and not request_resumable: meta["num_processed_tokens"] = int(num_computed_tokens) input_ids, input_embeds, update_dict = self.talker_preprocess_prefill(input_ids, input_embeds, payload) code_predictor_codes = torch.zeros( diff --git a/vllm_omni/model_executor/models/qwen3_tts/prompt_embeds_builder.py b/vllm_omni/model_executor/models/qwen3_tts/prompt_embeds_builder.py index ca756b794..c9a0f266d 100644 --- a/vllm_omni/model_executor/models/qwen3_tts/prompt_embeds_builder.py +++ b/vllm_omni/model_executor/models/qwen3_tts/prompt_embeds_builder.py @@ -893,12 +893,12 @@ class Qwen3TTSPromptEmbedsBuilder: * ``talker_prompt``: ``[prompt_len, hidden]`` prefill embedding. * ``trailing_text_hidden``: ``[T, hidden]`` queue of text-side - embeddings consumed one-per-decode-step (streaming mode) or a - single pad row (non-streaming). + embeddings consumed one-per-decode-step (streaming mode) or a + single pad row (non-streaming). * ``ref_code_len``: Number of ref codec frames consumed when the - Base task ran in in-context mode (``None`` otherwise). + Base task ran in in-context mode (``None`` otherwise). * ``ref_code``: The ``[T, Q]`` ref codec tensor used for ICL - (``None`` outside Base / ICL). + (``None`` outside Base / ICL). """ config = self._config talker_config = self._talker_config diff --git a/vllm_omni/model_executor/stage_input_processors/qwen3_omni.py b/vllm_omni/model_executor/stage_input_processors/qwen3_omni.py index b671951b2..aa98c2141 100644 --- a/vllm_omni/model_executor/stage_input_processors/qwen3_omni.py +++ b/vllm_omni/model_executor/stage_input_processors/qwen3_omni.py @@ -261,6 +261,89 @@ def _resolve_tts_token_embedding( # ========================= +def _construct_thinker2talker_streaming_input_async_chunk( + is_finished: bool, + request, + thinker_emb, + thinker_hid, + transfer_manager, +) -> OmniPayloadStruct | None: + """Build Thinker -> Talker payloads for realtime streaming input chunks. + + A resumable realtime request reuses the same logical request id across + audio segments. The first streaming prefill chunk is cached and returns ``None`` so the + connector does not emit an incomplete downstream chunk. The following + decode chunk flushes that cached prefill together with the current Thinker + output, keeping Talker ids and tensor rows aligned. + """ + request_id = request.external_req_id + output_token_ids = request.output_token_ids + # Convert ConstantList to regular list for OmniSerializer serialization + output_token_ids = _ensure_list(output_token_ids) + speaker = extract_speaker_from_request(request) + language = extract_language_from_request(request) + finished = torch.tensor(is_finished, dtype=torch.bool) + emb_cpu = thinker_emb.detach().cpu() + hid_cpu = thinker_hid.detach().cpu() + + if output_token_ids: + if thinker_emb.shape[0] > 1: + # if thinker_emb.shape[0] > 1, new streaming input segment is added + # and will transfer prefill embeddings and hidden states to talker. + new_prompt_len = thinker_emb.shape[0] + payload = OmniPayloadStruct( + meta=MetaStruct(finished=finished), + embed=EmbeddingsStruct(prefill=emb_cpu), + hidden_states=HiddenStatesStruct(output=hid_cpu), + ids=IdsStruct( + all=_ensure_list(request.all_token_ids[-new_prompt_len - 1 :]), + prompt=_ensure_list(request.prompt_token_ids[-new_prompt_len:]), + ), + speaker=speaker, + language=language, + ) + transfer_manager._pending_streaming_prefills[request_id] = to_dict(payload) + return None + else: + save_payload = transfer_manager._pending_streaming_prefills.pop(request_id, None) + if save_payload is not None: + saved_prefill = save_payload.get("embed", {}).get("prefill") + saved_output = save_payload.get("hidden_states", {}).get("output") + if isinstance(saved_prefill, torch.Tensor) and isinstance(saved_output, torch.Tensor): + return OmniPayloadStruct( + meta=MetaStruct(finished=finished), + embed=EmbeddingsStruct(prefill=torch.cat((saved_prefill, emb_cpu), dim=0)), + hidden_states=HiddenStatesStruct(output=torch.cat((saved_output, hid_cpu), dim=0)), + ids=IdsStruct( + all=save_payload.get("ids", {}).get("all"), + prompt=save_payload.get("ids", {}).get("prompt"), + ), + speaker=speaker, + language=language, + ) + return OmniPayloadStruct( + meta=MetaStruct( + finished=finished, + ), + embed=EmbeddingsStruct(decode=emb_cpu), + hidden_states=HiddenStatesStruct(output=hid_cpu), + ids=IdsStruct(output=output_token_ids), + speaker=speaker, + language=language, + ) + else: + if not is_finished: + # do not send async chunk mode placeholder token or embedding/hidden of the stop token + return None + return OmniPayloadStruct( + meta=MetaStruct(finished=finished), + embed=EmbeddingsStruct(decode=emb_cpu), + hidden_states=HiddenStatesStruct(output=hid_cpu), + speaker=speaker, + language=language, + ) + + @dataclass class _Thinker2TalkerStreamingState: last_prompt_len: int = 0 @@ -295,16 +378,16 @@ def _get_streaming_talker_tokens( streaming_context: Any | None = None, *, clear_state: bool = False, -) -> tuple[list[int], list[int], list[int], list[int]]: - """Return streaming token slices and merged token views for thinker->talker. - e.g. For the second streaming input request: - merged_sequences: [input_prompt 1, output_tokens 1[:-1], input_prompt 2, output_tokens 2] - thinker_input_ids: [input_prompt 1, output_tokens 1[:-1], input_prompt 2] +) -> tuple[list[int], list[int]]: + """Return prompt/output token deltas for the current streaming segment. + + In non-async-chunk streaming, Thinker's prompt may already include the + next input segment. Remove that new prompt tail before building the Talker + delta for the previous segment. + Returns: inc_prompt: prompt token delta for this segment. inc_output: output token delta for this segment. - merged_sequences: full thinker_sequences to send downstream. - thinker_input_ids: full thinker_input_ids paired with merged_sequences. """ state = _get_qwen3_streaming_state(request_id, streaming_context).thinker2talker if new_prompt_len_snapshot: @@ -314,15 +397,6 @@ def _get_streaming_talker_tokens( inc_prompt = prompt_token_ids[state.last_prompt_len :] inc_output = output_token_ids[state.last_output_len :] - delta_sequences = inc_prompt + inc_output - cached_sequences = state.merged_sequences - - merged_sequences = cached_sequences + delta_sequences - thinker_input_ids = cached_sequences + inc_prompt - - # Persist history for next segment. Drop the latest sampled token to keep - # thinker_input_ids / thinker_sequences alignment with next-step append. - cached_sequences.extend(delta_sequences[:-1]) state.last_prompt_len = cur_prompt_len state.last_output_len = cur_output_len @@ -332,7 +406,7 @@ def _get_streaming_talker_tokens( state.last_output_len = 0 state.merged_sequences.clear() - return inc_prompt, inc_output, merged_sequences, thinker_input_ids + return inc_prompt, inc_output def _get_streaming_codec_delta_len( @@ -429,6 +503,10 @@ def thinker2talker_async_chunk( transfer_manager.request_payload[request_id] = to_dict(payload) return None else: + if request.resumable: + return _construct_thinker2talker_streaming_input_async_chunk( + is_finished, request, thinker_emb, thinker_hid, transfer_manager + ) if thinker_emb.shape[0] > 1: logger.warning( "Unexpected multiple embeddings in thinker2talker_async_chunk for chunk_id %d: " @@ -567,7 +645,7 @@ def thinker2talker( output_ids = _ensure_list(output.cumulative_token_ids) is_streaming_session = bool(getattr(streaming_context, "enabled", False)) if is_streaming_session: - prompt_token_ids, output_ids, thinker_sequences, thinker_input_ids = _get_streaming_talker_tokens( + prompt_token_ids, output_ids = _get_streaming_talker_tokens( req_id, prompt_token_ids, output_ids, @@ -575,9 +653,8 @@ def thinker2talker( streaming_context, clear_state=bool(getattr(thinker_output, "finished", False)), ) - else: - thinker_sequences = prompt_token_ids + output_ids - thinker_input_ids = prompt_token_ids + thinker_sequences = prompt_token_ids + output_ids + thinker_input_ids = prompt_token_ids new_seq_length = len(prompt_token_ids + output_ids) - 1 thinker_mm_raw = getattr(output, "multimodal_output", None) if not isinstance(thinker_mm_raw, dict): @@ -683,7 +760,7 @@ def thinker2talker_token_only( output_ids = _ensure_list(output.cumulative_token_ids) is_streaming_session = bool(getattr(streaming_context, "enabled", False)) if is_streaming_session: - prompt_token_ids, output_ids, thinker_sequences, thinker_input_ids = _get_streaming_talker_tokens( + prompt_token_ids, output_ids = _get_streaming_talker_tokens( req_id, prompt_token_ids, output_ids, @@ -691,9 +768,8 @@ def thinker2talker_token_only( streaming_context, clear_state=bool(getattr(thinker_output, "finished", False)), ) - else: - thinker_sequences = prompt_token_ids + output_ids - thinker_input_ids = prompt_token_ids + thinker_sequences = prompt_token_ids + output_ids + thinker_input_ids = prompt_token_ids info_for_len = {"ids": {"all": thinker_sequences, "prompt": thinker_input_ids}} prompt_len = _compute_talker_prompt_ids_length(info_for_len, device="cpu") @@ -759,6 +835,15 @@ def talker2code2wav_async_chunk( if isinstance(code_predictor_codes, torch.Tensor): if not code_predictor_codes.any(): return None + sampling_params = getattr(request, "sampling_params", None) + stop_token_ids = set(getattr(sampling_params, "stop_token_ids", None) or []) + stop_token_id = getattr(sampling_params, "stop_token_id", None) + if stop_token_id is not None: + stop_token_ids.add(stop_token_id) + first_codebook = int(code_predictor_codes[0, 0].item()) + if first_codebook in stop_token_ids: + logger.debug("skip stop-token codec frame: first_codebook=%s", first_codebook) + return None else: code_tensor = torch.tensor(code_predictor_codes, dtype=torch.long) if not code_tensor.any(): diff --git a/vllm_omni/worker/gpu_model_runner.py b/vllm_omni/worker/gpu_model_runner.py index a97cc2b65..0663673d2 100644 --- a/vllm_omni/worker/gpu_model_runner.py +++ b/vllm_omni/worker/gpu_model_runner.py @@ -437,7 +437,7 @@ class OmniGPUModelRunner(GPUModelRunner): for new_req_data in scheduler_output.scheduled_new_reqs: req_id = new_req_data.req_id if req_id in self.requests: - self._update_streaming_input_additional_info(new_req_data, req_id) + self._update_streaming_input_additional_info(req_id) req_state = self._update_streaming_request(req_id, new_req_data) reqs_to_add.append(req_state) continue @@ -1806,34 +1806,12 @@ class OmniGPUModelRunner(GPUModelRunner): logger.warning_once("_merge_additional_information_update is deprecated, use _update_intermediate_buffer") return self._update_intermediate_buffer(req_id, upd) - def _update_streaming_input_additional_info(self, new_req_data, req_id): - # For streaming input prefill case only. Update buffer from last segment input + def _update_streaming_input_additional_info(self, req_id): + # For streaming input prefill case only. Set num processed tokens = 0 for new segment input cached_additional_info = self.model_intermediate_buffer.get(req_id, {}) if cached_additional_info: - payload_info = getattr(new_req_data, "additional_information", None) - inc_info = deserialize_additional_information(payload_info) - if isinstance(inc_info, dict) and inc_info: - accumulated_keys: set[tuple[str, str]] = set() - if hasattr(self, "model") and hasattr(self.model, "streaming_accumulated_keys"): - accumulated_keys = self.model.streaming_accumulated_keys - merged_info = dict(cached_additional_info) - for key, value in inc_info.items(): - if isinstance(value, dict): - existing_sub = merged_info.get(key) - merged_sub = dict(existing_sub) if isinstance(existing_sub, dict) else {} - for sk, sv in value.items(): - if (key, sk) in accumulated_keys and isinstance(sv, torch.Tensor): - inc_tensor = sv.detach().to("cpu").contiguous() - old_tensor = merged_sub.get(sk) - if old_tensor is None: - merged_sub[sk] = inc_tensor - else: - merged_sub[sk] = torch.cat((old_tensor, inc_tensor), dim=0) - else: - merged_sub[sk] = sv - merged_info[key] = merged_sub - else: - merged_info[key] = value - merged_info.setdefault("meta", {})["num_processed_tokens"] = 0 - self.model_intermediate_buffer[req_id] = merged_info - setattr(self.requests[req_id], "additional_information_cpu", merged_info) + merged_info = dict(cached_additional_info) + merged_info.setdefault("meta", {})["num_processed_tokens"] = 0 + merged_info.setdefault("meta", {})["resumable"] = True + self.model_intermediate_buffer[req_id] = merged_info + setattr(self.requests[req_id], "additional_information_cpu", merged_info)

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 literal 267149 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�I-NHf+@TLx8N8Y1>AJ9nwyOc7{vIH>B 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