From af11b02081fa90bde9074643bae1d74e8165f7d8 Mon Sep 17 00:00:00 2001 From: Canlin Guo Date: Tue, 24 Feb 2026 14:10:00 +0800 Subject: [PATCH 01/10] [Minor][Refactor] Pass seq_token_counts explicitly (#1425) Signed-off-by: gcanlin Co-authored-by: Hongsheng Liu --- .../models/qwen3_omni/qwen3_omni.py | 28 ++++++++++++------- .../models/qwen3_omni/qwen3_omni_code2wav.py | 18 ++++++------ .../worker/gpu_generation_model_runner.py | 7 ++--- 3 files changed, 29 insertions(+), 24 deletions(-) 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 41fd1ecf5..58eb1ff22 100644 --- a/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py +++ b/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py @@ -15,7 +15,6 @@ from transformers.models.qwen3_omni_moe.configuration_qwen3_omni_moe import ( Qwen3OmniMoeThinkerConfig, ) from vllm.config import VllmConfig -from vllm.forward_context import get_forward_context from vllm.logger import init_logger from vllm.model_executor.layers.rotary_embedding import MRotaryEmbedding from vllm.model_executor.models.interfaces import SupportsMRoPE, SupportsMultiModal, SupportsPP @@ -355,13 +354,14 @@ class Qwen3OmniMoeForConditionalGeneration( # ========== Stage 3: Code2Wav ========== elif self.model_stage == "code2wav": + seq_token_counts: list[int] | None = kwargs.get("seq_token_counts") + # Extract codec codes from input if input_ids.shape[0] % 16 == 0: - ubatch_slices = get_forward_context().ubatch_slices - if ubatch_slices is not None: - max_seq_len = max(ubatch_slices) // 16 - batch_size = len(ubatch_slices) - split_codes = torch.split(input_ids, ubatch_slices, dim=0) + if seq_token_counts is not None: + max_seq_len = max(seq_token_counts) // 16 + batch_size = len(seq_token_counts) + split_codes = torch.split(input_ids, seq_token_counts, dim=0) codes = torch.zeros((batch_size, 16, max_seq_len), device=input_ids.device, dtype=input_ids.dtype) for idx, code in enumerate(split_codes): seq_len = code.shape[0] // 16 @@ -386,7 +386,7 @@ class Qwen3OmniMoeForConditionalGeneration( codes = input_ids_flatten.reshape(1, 16, -1) # Generate audio from codec codes - audio_tensors = self.generate_audio(codes, voice_type) + audio_tensors = self.generate_audio(codes, voice_type, seq_token_counts) return audio_tensors @@ -458,16 +458,22 @@ class Qwen3OmniMoeForConditionalGeneration( # ==================== Audio Generation ==================== - def generate_audio(self, code: torch.Tensor, voice_type: str) -> list[torch.Tensor]: + def generate_audio( + self, + code: torch.Tensor, + voice_type: str, + seq_token_counts: list[int] | None = None, + ) -> list[torch.Tensor]: """ Generate audio waveform from codec codes. Args: - code: [8, T] - 8-layer RVQ codec codes + code: [batch, num_quantizers, T] - RVQ codec codes voice_type: Voice type (not used in Qwen3, kept for compatibility) + seq_token_counts: Token count for each request in batch Returns: - audio_tensor: [1, waveform_len] - Audio waveform + list of audio waveforms """ code2wav_dev = self._module_device(self.code2wav) @@ -491,6 +497,7 @@ class Qwen3OmniMoeForConditionalGeneration( talker_codes, chunk_size=25, left_context_size=25, + seq_token_counts=seq_token_counts, ) else: # Use chunked decode for memory efficiency @@ -498,6 +505,7 @@ class Qwen3OmniMoeForConditionalGeneration( talker_codes, chunk_size=300, left_context_size=25, + seq_token_counts=seq_token_counts, ) return audio_tensors 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 f1e08c435..54bc1c1c5 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 @@ -22,7 +22,6 @@ from transformers.models.qwen3_omni_moe.modeling_qwen3_omni_moe import ( SnakeBeta, ) from vllm.config import VllmConfig # type: ignore -from vllm.forward_context import get_forward_context from vllm.logger import init_logger # type: ignore from vllm.model_executor.models.utils import ( # type: ignore AutoWeightsLoader, @@ -163,6 +162,7 @@ class Qwen3OmniMoeCode2Wav(nn.Module): codes: torch.Tensor, chunk_size: int = 300, left_context_size: int = 25, + seq_token_counts: list[int] | None = None, ) -> list[torch.Tensor]: """ Decode long sequences in chunks to avoid OOM. @@ -173,6 +173,7 @@ class Qwen3OmniMoeCode2Wav(nn.Module): codes: [batch, num_quantizers, seq_len] - num_quantizers-layer RVQ codes chunk_size: Number of codec frames per chunk left_context_size: Number of overlapping frames for context + seq_token_counts: Token count for each request in batch Returns: list[torch.Tensor]: Complete waveform decoded from the input @@ -197,12 +198,10 @@ class Qwen3OmniMoeCode2Wav(nn.Module): start_index = end_index - ubatch_slices = get_forward_context().ubatch_slices - if ubatch_slices is not None: - code_seq_lens = [seq_len // self.config.num_quantizers for seq_len in ubatch_slices] + if seq_token_counts is not None: + code_seq_lens = [seq_len // self.config.num_quantizers for seq_len in seq_token_counts] else: # Fallback: assume all batch elements share the same sequence length. - # Create one entry per batch so that each element is processed. code_seq_lens = [codes.shape[-1]] * codes.shape[0] batch_wav = torch.cat(wavs, dim=-1) wavs = [] @@ -216,6 +215,7 @@ class Qwen3OmniMoeCode2Wav(nn.Module): codes: torch.Tensor, chunk_size: int = 25, left_context_size: int = 25, + seq_token_counts: list[int] | None = None, ) -> list[torch.Tensor]: """ Decode long sequences in chunks to avoid OOM. @@ -226,21 +226,19 @@ class Qwen3OmniMoeCode2Wav(nn.Module): codes: [batch, num_quantizers, seq_len] - num_quantizers-layer RVQ codes chunk_size: Number of codec frames per chunk left_context_size: Number of overlapping frames for context + seq_token_counts: Token count for each request in batch Returns: list[torch.Tensor]: Complete waveform decoded from the input codes. For ``batch_size == 1``, this is a list containing a single tensor with shape ``[1, waveform_len]``. """ - # Decode chunk wavs = [] batch_wav = self(codes) - ubatch_slices = get_forward_context().ubatch_slices - if ubatch_slices is not None: - code_seq_lens = [seq_len // self.config.num_quantizers for seq_len in ubatch_slices] + if seq_token_counts is not None: + code_seq_lens = [n // self.config.num_quantizers for n in seq_token_counts] else: # Fallback: assume all batch elements share the same sequence length. - # Create one entry per batch so that each element is processed. code_seq_lens = [codes.shape[-1]] * codes.shape[0] for idx, code_seq_len in enumerate(code_seq_lens): # TODO: need to optimize algorithms, current only support diff --git a/vllm_omni/worker/gpu_generation_model_runner.py b/vllm_omni/worker/gpu_generation_model_runner.py index aa75d201c..fc1362f88 100644 --- a/vllm_omni/worker/gpu_generation_model_runner.py +++ b/vllm_omni/worker/gpu_generation_model_runner.py @@ -250,6 +250,9 @@ class GPUGenerationModelRunner(OmniGPUModelRunner): intermediate_tensors, ) + # [Omni] Pass token counts per request for code2wav output slicing + model_kwargs["seq_token_counts"] = tokens + # Set cudagraph mode to none if calc_kv_scales is true. # KV scales calculation involves dynamic operations that are incompatible # with CUDA graph capture. @@ -258,10 +261,6 @@ class GPUGenerationModelRunner(OmniGPUModelRunner): # Mark KV scales as calculated after the first forward pass self.calculate_kv_scales = False - if ubatch_slices_padded is None: - # reuse ubatch_slices_padded for code2wav batching - ubatch_slices_padded = tokens - # Run the model. # Use persistent buffers for CUDA graphs. with ( From 06d347b47fc3ff349c4f34ed978c05981f86467c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Lucchesi?= Date: Tue, 24 Feb 2026 09:44:33 +0100 Subject: [PATCH 02/10] [Misc] Extend Diffusion Benchmark script to other backends (#875) Signed-off-by: NickLucche Co-authored-by: Hongsheng Liu --- benchmarks/diffusion/backends.py | 198 ++++++++++++++++++ .../diffusion/diffusion_benchmark_serving.py | 162 ++++---------- 2 files changed, 233 insertions(+), 127 deletions(-) create mode 100644 benchmarks/diffusion/backends.py diff --git a/benchmarks/diffusion/backends.py b/benchmarks/diffusion/backends.py new file mode 100644 index 000000000..98d3a30fe --- /dev/null +++ b/benchmarks/diffusion/backends.py @@ -0,0 +1,198 @@ +import base64 +import mimetypes +import os +import time +import uuid +from dataclasses import dataclass, field +from typing import Any + +import aiohttp +from tqdm import tqdm + + +@dataclass +class RequestFuncInput: + prompt: str + api_url: str + model: str + width: int | None = None + height: int | None = None + num_frames: int | None = None + num_inference_steps: int | None = None + seed: int | None = None + fps: int | None = None + timestamp: float | None = None + slo_ms: float | None = None + extra_body: dict[str, Any] = field(default_factory=dict) + image_paths: list[str] | None = None + request_id: str = field(default_factory=lambda: str(uuid.uuid4())) + + +@dataclass +class RequestFuncOutput: + success: bool = False + latency: float = 0.0 + error: str = "" + start_time: float = 0.0 + response_body: dict[str, Any] = field(default_factory=dict) + peak_memory_mb: float = 0.0 + slo_achieved: bool | None = None + + +def _guess_mime_type(path: str) -> str: + mime, _ = mimetypes.guess_type(path) + return mime or "application/octet-stream" + + +def _encode_image_as_data_url(path: str) -> str: + with open(path, "rb") as f: + encoded = base64.b64encode(f.read()).decode("utf-8") + mime = _guess_mime_type(path) + return f"data:{mime};base64,{encoded}" + + +async def async_request_chat_completions( + input: RequestFuncInput, + session: aiohttp.ClientSession, + pbar: tqdm | None = None, +) -> RequestFuncOutput: + output = RequestFuncOutput() + output.start_time = time.perf_counter() + + extra_body = dict(input.extra_body) + if input.width and input.height: + extra_body.setdefault("height", input.height) + extra_body.setdefault("width", input.width) + if input.num_frames: + extra_body.setdefault("num_frames", input.num_frames) + if input.num_inference_steps: + extra_body.setdefault("num_inference_steps", input.num_inference_steps) + if input.seed is not None: + extra_body.setdefault("seed", input.seed) + if input.fps: + extra_body.setdefault("fps", input.fps) + + if input.image_paths and len(input.image_paths) > 0: + content = [] + if input.prompt: + content.append({"type": "text", "text": input.prompt}) + for img_path in input.image_paths: + if not os.path.exists(img_path): + output.error = f"Image file not found: {img_path}" + output.success = False + if pbar: + pbar.update(1) + return output + content.append( + { + "type": "image_url", + "image_url": {"url": _encode_image_as_data_url(img_path)}, + } + ) + messages = [{"role": "user", "content": content}] + else: + messages = [{"role": "user", "content": input.prompt}] + + payload = { + "model": input.model, + "messages": messages, + } + if extra_body: + payload["extra_body"] = extra_body + + try: + async with session.post(input.api_url, json=payload) as response: + if response.status == 200: + resp_json = await response.json() + output.response_body = resp_json + output.success = True + if "peak_memory_mb" in resp_json: + output.peak_memory_mb = resp_json["peak_memory_mb"] + else: + output.error = f"HTTP {response.status}: {await response.text()}" + output.success = False + except Exception as e: + output.error = str(e) + output.success = False + + output.latency = time.perf_counter() - output.start_time + + if output.success and input.slo_ms is not None: + output.slo_achieved = (output.latency * 1000.0) <= float(input.slo_ms) + + if pbar: + pbar.update(1) + return output + + +async def async_request_openai_images( + input: RequestFuncInput, + session: aiohttp.ClientSession, + pbar: tqdm | None = None, +) -> RequestFuncOutput: + """ + Send request to OpenAI's /v1/images/generations endpoint. + """ + output = RequestFuncOutput() + output.start_time = time.perf_counter() + + # Build size string from width/height + width = input.width or 1024 + height = input.height or 1024 + size = f"{width}x{height}" + + payload: dict[str, Any] = { + "model": input.model, + "prompt": input.prompt, + "n": 1, + "size": size, + "response_format": "b64_json", + } + + # Add optional parameters + if input.seed is not None: + payload["seed"] = input.seed + if input.num_inference_steps is not None: + payload["num_inference_steps"] = input.num_inference_steps + + # Add any extra body parameters + if input.extra_body: + for key, value in input.extra_body.items(): + if key not in payload: + payload[key] = value + + headers = { + "Content-Type": "application/json", + "Authorization": "Bearer EMPTY", + } + + try: + async with session.post(input.api_url, json=payload, headers=headers) as response: + if response.status == 200: + resp_json = await response.json() + output.response_body = resp_json + output.success = True + # Check for usage/memory info if available + if "usage" in resp_json and "peak_memory_mb" in resp_json.get("usage", {}): + output.peak_memory_mb = resp_json["usage"]["peak_memory_mb"] + else: + output.error = f"HTTP {response.status}: {await response.text()}" + output.success = False + except Exception as e: + output.error = str(e) + output.success = False + + output.latency = time.perf_counter() - output.start_time + + if output.success and input.slo_ms is not None: + output.slo_achieved = (output.latency * 1000.0) <= float(input.slo_ms) + + if pbar: + pbar.update(1) + return output + + +backends_function_mapping = { + "vllm-omni": (async_request_chat_completions, "/v1/chat/completions"), + "openai": (async_request_openai_images, "/v1/images/generations"), +} diff --git a/benchmarks/diffusion/diffusion_benchmark_serving.py b/benchmarks/diffusion/diffusion_benchmark_serving.py index 8cb4cdcff..a00e975b0 100644 --- a/benchmarks/diffusion/diffusion_benchmark_serving.py +++ b/benchmarks/diffusion/diffusion_benchmark_serving.py @@ -6,80 +6,60 @@ Benchmark online serving for diffusion models (Image/Video Generation). If you want to use i2v, i2i dataset, you should `uv pip install gdown` first +Supports multiple backends: + - vllm-omni: Uses /v1/chat/completions endpoint (default) + - openai: Uses /v1/images/generations endpoint + Usage: - # Video + # Video (vllm-omni backend) t2v: python3 benchmarks/diffusion/diffusion_benchmark_serving.py \ - --dataset vbench --task t2v --num-prompts 10 \ + --backend vllm-omni --dataset vbench --task t2v --num-prompts 10 \ --height 480 --width 640 --fps 16 --num-frames 80 i2v: python3 benchmarks/diffusion/diffusion_benchmark_serving.py \ - --dataset vbench --task i2v --num-prompts 10 + --backend vllm-omni --dataset vbench --task i2v --num-prompts 10 - # Image + # Image (vllm-omni backend) t2i: python3 benchmarks/diffusion/diffusion_benchmark_serving.py \ - --dataset vbench --task t2i --num-prompts 10 \ + --backend vllm-omni --dataset vbench --task t2i --num-prompts 10 \ --height 1024 --width 1024 i2i: python3 benchmarks/diffusion/diffusion_benchmark_serving.py \ - --dataset vbench --task i2i --num-prompts 10 + --backend vllm-omni --dataset vbench --task i2i --num-prompts 10 + + # Image (openai backend) + t2i: + python3 benchmarks/diffusion/diffusion_benchmark_serving.py \ + --backend openai --dataset vbench --task t2i --num-prompts 10 \ + --height 1024 --width 1024 --port 3000 """ import argparse import ast import asyncio -import base64 import glob import json -import mimetypes import os import time import uuid from abc import ABC, abstractmethod from collections.abc import AsyncGenerator -from dataclasses import dataclass, field, replace +from dataclasses import replace from typing import Any import aiohttp import numpy as np import requests +from backends import RequestFuncInput, RequestFuncOutput, backends_function_mapping from tqdm.asyncio import tqdm -@dataclass -class RequestFuncInput: - prompt: str - api_url: str - model: str - width: int | None = None - height: int | None = None - num_frames: int | None = None - num_inference_steps: int | None = None - seed: int | None = None - fps: int | None = None - timestamp: float | None = None - slo_ms: float | None = None - extra_body: dict[str, Any] = field(default_factory=dict) - image_paths: list[str] | None = None - request_id: str = field(default_factory=lambda: str(uuid.uuid4())) - - -@dataclass -class RequestFuncOutput: - success: bool = False - latency: float = 0.0 - error: str = "" - start_time: float = 0.0 - response_body: dict[str, Any] = field(default_factory=dict) - peak_memory_mb: float = 0.0 - slo_achieved: bool | None = None - - class BaseDataset(ABC): def __init__(self, args, api_url: str, model: str): self.args = args @@ -684,92 +664,6 @@ async def iter_requests( yield req -def _guess_mime_type(path: str) -> str: - mime, _ = mimetypes.guess_type(path) - return mime or "application/octet-stream" - - -def _encode_image_as_data_url(path: str) -> str: - with open(path, "rb") as f: - encoded = base64.b64encode(f.read()).decode("utf-8") - mime = _guess_mime_type(path) - return f"data:{mime};base64,{encoded}" - - -async def async_request_chat_completions( - input: RequestFuncInput, - session: aiohttp.ClientSession, - pbar: tqdm | None = None, -) -> RequestFuncOutput: - output = RequestFuncOutput() - output.start_time = time.perf_counter() - - extra_body = dict(input.extra_body) - if input.width and input.height: - extra_body.setdefault("height", input.height) - extra_body.setdefault("width", input.width) - if input.num_frames: - extra_body.setdefault("num_frames", input.num_frames) - if input.num_inference_steps: - extra_body.setdefault("num_inference_steps", input.num_inference_steps) - if input.seed is not None: - extra_body.setdefault("seed", input.seed) - if input.fps: - extra_body.setdefault("fps", input.fps) - - if input.image_paths and len(input.image_paths) > 0: - content = [] - if input.prompt: - content.append({"type": "text", "text": input.prompt}) - for img_path in input.image_paths: - if not os.path.exists(img_path): - output.error = f"Image file not found: {img_path}" - output.success = False - if pbar: - pbar.update(1) - return output - content.append( - { - "type": "image_url", - "image_url": {"url": _encode_image_as_data_url(img_path)}, - } - ) - messages = [{"role": "user", "content": content}] - else: - messages = [{"role": "user", "content": input.prompt}] - - payload = { - "model": input.model, - "messages": messages, - } - if extra_body: - payload["extra_body"] = extra_body - - try: - async with session.post(input.api_url, json=payload) as response: - if response.status == 200: - resp_json = await response.json() - output.response_body = resp_json - output.success = True - if "peak_memory_mb" in resp_json: - output.peak_memory_mb = resp_json["peak_memory_mb"] - else: - output.error = f"HTTP {response.status}: {await response.text()}" - output.success = False - except Exception as e: - output.error = str(e) - output.success = False - - output.latency = time.perf_counter() - output.start_time - - if output.success and input.slo_ms is not None: - output.slo_achieved = (output.latency * 1000.0) <= float(input.slo_ms) - - if pbar: - pbar.update(1) - return output - - def calculate_metrics( outputs: list[RequestFuncOutput], total_duration: float, @@ -848,9 +742,9 @@ async def benchmark(args): if args.base_url is None: args.base_url = f"http://{args.host}:{args.port}" - # Setup dataset (vLLM-Omni supports diffusion via /v1/chat/completions) - api_url = f"{args.base_url}/v1/chat/completions" - request_func = async_request_chat_completions + # Setup API URL and request function based on backend + request_func, api_url = backends_function_mapping[args.backend] + api_url = f"{args.base_url}{api_url}" if args.dataset == "vbench": dataset = VBenchDataset(args, api_url, args.model) @@ -920,9 +814,16 @@ async def benchmark(args): # Calculate metrics metrics = calculate_metrics(outputs, total_duration, requests_list, args, args.slo) + # Add configuration info to metrics for JSON output + metrics["backend"] = args.backend + metrics["model"] = args.model + metrics["dataset"] = args.dataset + metrics["task"] = args.task + print("\n{s:{c}^{n}}".format(s=" Serving Benchmark Result ", n=60, c="=")) # Section 1: Configuration + print("{:<40} {:<15}".format("Backend:", args.backend)) print("{:<40} {:<15}".format("Model:", args.model)) print("{:<40} {:<15}".format("Dataset:", args.dataset)) print("{:<40} {:<15}".format("Task:", args.task)) @@ -978,6 +879,13 @@ if __name__ == "__main__": parser.add_argument("--host", type=str, default="localhost", help="Server host.") parser.add_argument("--port", type=int, default=8091, help="Server port.") parser.add_argument("--model", type=str, default="default", help="Model name.") + parser.add_argument( + "--backend", + type=str, + default="vllm-omni", + choices=["vllm-omni", "openai"], + help="Backend to target the benchmark to.", + ) parser.add_argument( "--dataset", type=str, From 36b8f80266b78c9341c83aae6bfb062db6b25fcb Mon Sep 17 00:00:00 2001 From: wuhang Date: Tue, 24 Feb 2026 16:55:23 +0800 Subject: [PATCH 03/10] [Feature] Support Stage Based Deployment CLI (#939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: wuhang Signed-off-by: princepride Signed-off-by: wuhang Co-authored-by: 汪志鹏 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../bagel/run_server_stage_cli.sh | 34 ++ requirements/common.txt | 1 + .../test_async_omni_diffusion_config.py | 22 +- tests/entrypoints/test_omni_diffusion.py | 369 ++++++++++++++---- tests/entrypoints/test_omni_llm.py | 303 +++++++++++--- vllm_omni/entrypoints/async_omni.py | 13 +- vllm_omni/entrypoints/cli/serve.py | 164 +++++++- vllm_omni/entrypoints/omni.py | 283 ++++++++++++-- vllm_omni/entrypoints/omni_stage.py | 104 +++-- vllm_omni/entrypoints/utils.py | 82 ++++ vllm_omni/entrypoints/zmq_utils.py | 95 +++++ 11 files changed, 1272 insertions(+), 198 deletions(-) create mode 100644 examples/online_serving/bagel/run_server_stage_cli.sh create mode 100644 vllm_omni/entrypoints/zmq_utils.py diff --git a/examples/online_serving/bagel/run_server_stage_cli.sh b/examples/online_serving/bagel/run_server_stage_cli.sh new file mode 100644 index 000000000..51639153f --- /dev/null +++ b/examples/online_serving/bagel/run_server_stage_cli.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Bagel multi-stage online serving startup script +# Starts stage 0 as master with API server, and stage 1 in headless mode + +MODEL="${MODEL:-ByteDance-Seed/BAGEL-7B-MoT}" +PORT="${PORT:-8091}" +MASTER_ADDRESS="${MASTER_ADDRESS:-127.0.0.1}" +MASTER_PORT="${MASTER_PORT:-8092}" +STAGE_CONFIGS_PATH="$(dirname "$0")/../../../vllm_omni/model_executor/stage_configs/bagel.yaml" + +echo "Starting Bagel multi-stage server..." +echo "Model: $MODEL" +echo "API Port: $PORT" +echo "Master Address: $MASTER_ADDRESS" +echo "Master Port: $MASTER_PORT" +echo "Stage Configs: $STAGE_CONFIGS_PATH" + +# Start stage 1 (DiT) in headless mode first +echo "Starting Stage 1 (DiT) in headless mode..." +vllm serve "$MODEL" --omni \ + --stage-configs-path "$STAGE_CONFIGS_PATH" \ + --stage-id 1 \ + --headless \ + -oma "$MASTER_ADDRESS" \ + -omp "$MASTER_PORT" & + +# Start stage 0 (Thinker) as master with API server +echo "Starting Stage 0 (Thinker) as master..." +vllm serve "$MODEL" --omni \ + --port "$PORT" \ + --stage-configs-path "$STAGE_CONFIGS_PATH" \ + --stage-id 0 \ + -oma "$MASTER_ADDRESS" \ + -omp "$MASTER_PORT" diff --git a/requirements/common.txt b/requirements/common.txt index 3954b4587..7e5f5f9b2 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -14,3 +14,4 @@ imageio[ffmpeg]>=2.37.2 sox>=1.5.0 prettytable>=3.8.0 aenum==3.1.16 +pyzmq>=25.0.0 diff --git a/tests/entrypoints/test_async_omni_diffusion_config.py b/tests/entrypoints/test_async_omni_diffusion_config.py index ba0390040..ce443d5a5 100644 --- a/tests/entrypoints/test_async_omni_diffusion_config.py +++ b/tests/entrypoints/test_async_omni_diffusion_config.py @@ -3,21 +3,23 @@ import pytest -from vllm_omni.entrypoints import omni as omni_module +from vllm_omni.entrypoints import utils as utils_module from vllm_omni.entrypoints.async_omni import AsyncOmni pytestmark = [pytest.mark.core_model, pytest.mark.cpu] +MODEL = "riverclouds/qwen_image_random" + def test_default_stage_config_includes_cache_backend(monkeypatch): """Ensure cache_backend/cache_config are preserved in default diffusion stage.""" - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", lambda model, base_engine_args=None: []) - monkeypatch.setattr(omni_module, "resolve_model_config_path", lambda model: None) + monkeypatch.setattr(utils_module, "load_stage_configs_from_model", lambda model, base_engine_args=None: []) + monkeypatch.setattr(utils_module, "resolve_model_config_path", lambda model: None) monkeypatch.setattr(AsyncOmni, "_start_stages", lambda self, model: None) monkeypatch.setattr(AsyncOmni, "_wait_for_stages_ready", lambda self, timeout=0: None) omni = AsyncOmni( - model="dummy-model", + model=MODEL, cache_backend="cache_dit", cache_config='{"Fn_compute_blocks": 2}', vae_use_slicing=True, @@ -41,13 +43,13 @@ def test_default_stage_config_includes_cache_backend(monkeypatch): def test_default_cache_config_used_when_missing(monkeypatch): """Ensure default cache_config is applied when cache_backend is set.""" - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", lambda model, base_engine_args=None: []) - monkeypatch.setattr(omni_module, "resolve_model_config_path", lambda model: None) + monkeypatch.setattr(utils_module, "load_stage_configs_from_model", lambda model, base_engine_args=None: []) + monkeypatch.setattr(utils_module, "resolve_model_config_path", lambda model: None) monkeypatch.setattr(AsyncOmni, "_start_stages", lambda self, model: None) monkeypatch.setattr(AsyncOmni, "_wait_for_stages_ready", lambda self, timeout=0: None) omni = AsyncOmni( - model="dummy-model", + model=MODEL, cache_backend="cache_dit", ) @@ -59,13 +61,13 @@ def test_default_cache_config_used_when_missing(monkeypatch): def test_default_stage_devices_from_sequence_parallel(monkeypatch): """Ensure devices list reflects sequence parallel size when no parallel_config is provided.""" - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", lambda model, base_engine_args=None: []) - monkeypatch.setattr(omni_module, "resolve_model_config_path", lambda model: None) + monkeypatch.setattr(utils_module, "load_stage_configs_from_model", lambda model, base_engine_args=None: []) + monkeypatch.setattr(utils_module, "resolve_model_config_path", lambda model: None) monkeypatch.setattr(AsyncOmni, "_start_stages", lambda self, model: None) monkeypatch.setattr(AsyncOmni, "_wait_for_stages_ready", lambda self, timeout=0: None) omni = AsyncOmni( - model="dummy-model", + model=MODEL, ulysses_degree=2, ring_degree=2, ) diff --git a/tests/entrypoints/test_omni_diffusion.py b/tests/entrypoints/test_omni_diffusion.py index ab1194a25..8b7977d9c 100644 --- a/tests/entrypoints/test_omni_diffusion.py +++ b/tests/entrypoints/test_omni_diffusion.py @@ -18,6 +18,37 @@ warnings.filterwarnings( category=DeprecationWarning, ) +MODEL = "riverclouds/qwen_image_random" + + +class _FakeStageRequestStats: + """Fake StageRequestStats object with necessary attributes aligned with real StageRequestStats.""" + + def __init__(self, **kwargs): + # Required fields (with defaults for testing) + self.batch_id = kwargs.get("batch_id", 0) + self.batch_size = kwargs.get("batch_size", 1) + self.num_tokens_in = kwargs.get("num_tokens_in", 0) + self.num_tokens_out = kwargs.get("num_tokens_out", 1) + self.stage_gen_time_ms = kwargs.get("stage_gen_time_ms", 10.0) + self.rx_transfer_bytes = kwargs.get("rx_transfer_bytes", 0) + self.rx_decode_time_ms = kwargs.get("rx_decode_time_ms", 0.0) + self.rx_in_flight_time_ms = kwargs.get("rx_in_flight_time_ms", 0.0) + self.stage_stats = kwargs.get("stage_stats", None) + + # Optional fields + self.stage_id = kwargs.get("stage_id", None) + self.final_output_type = kwargs.get("final_output_type", None) + self.request_id = kwargs.get("request_id", None) + self.postprocess_time_ms = kwargs.get("postprocess_time_ms", 0.0) + self.diffusion_metrics = kwargs.get("diffusion_metrics", None) + self.audio_generated_frames = kwargs.get("audio_generated_frames", 0) + + # Allow additional attributes for flexibility + for key, value in kwargs.items(): + if not hasattr(self, key): + setattr(self, key, value) + class _FakeEngineArgs(dict): """Fake engine args that can be used both as object attributes and as **kwargs.""" @@ -60,8 +91,10 @@ class _FakeQueue: def put_nowait(self, item): self._queue.put_nowait(item) - def get(self): - return self._queue.get() + def get(self, timeout=None): + if timeout is None: + return self._queue.get() + return self._queue.get(timeout=timeout) def get_nowait(self): return self._queue.get_nowait() @@ -70,6 +103,52 @@ class _FakeQueue: return self._queue.empty() +class _FakeZmqQueue: + """Fake ZmqQueue that wraps _FakeQueue and matches ZmqQueue interface.""" + + def __init__( + self, + ctx=None, + socket_type=None, + *, + bind: str | None = None, + connect: str | None = None, + recv_timeout_ms: int | None = None, + send_timeout_ms: int | None = None, + ): + """Initialize fake ZMQ queue with same signature as real ZmqQueue.""" + self._queue = _FakeQueue(maxsize=0) + # Determine endpoint from bind or connect + path = bind if bind is not None else connect + self.endpoint = path or f"fake://zmq-endpoint-{id(self)}" + self._recv_timeout_ms = recv_timeout_ms + self._send_timeout_ms = send_timeout_ms + + def put(self, obj: Any) -> None: + """Send an object to the queue.""" + self._queue.put(obj) + + def put_nowait(self, obj: Any) -> None: + """Send an object to the queue without blocking.""" + self._queue.put_nowait(obj) + + def get(self, timeout: float | None = None) -> Any: + """Receive an object from the queue with optional timeout in seconds.""" + return self._queue.get(timeout=timeout) + + def get_nowait(self) -> Any: + """Receive an object from the queue without blocking.""" + return self._queue.get_nowait() + + def empty(self) -> bool: + """Check if the queue is empty without blocking.""" + return self._queue.empty() + + def close(self) -> None: + """Close the queue.""" + pass + + class _FakeStage: """Lightweight Stage stub for multi-process pipeline version with queue support.""" @@ -272,6 +351,19 @@ def _setup_multiprocessing_mocks(monkeypatch): monkeypatch.setattr(mp, "get_context", _mock_get_context, raising=False) monkeypatch.setattr(mp, "Process", fake_process_class, raising=False) + # Mock ZmqQueue to use _FakeZmqQueue + monkeypatch.setattr( + "vllm_omni.entrypoints.zmq_utils.ZmqQueue", + _FakeZmqQueue, + raising=False, + ) + # Also mock where ZmqQueue is imported/used + monkeypatch.setattr( + "vllm_omni.entrypoints.omni_stage.ZmqQueue", + _FakeZmqQueue, + raising=False, + ) + def _setup_ipc_mocks(monkeypatch): """Helper function to set up IPC function mocks.""" @@ -324,6 +416,65 @@ def _setup_log_mocks(monkeypatch): ) +def _setup_connector_mocks(monkeypatch, omni_module=None): + """Helper function to set up connector mocks for stage-to-stage forwarding. + + If omni_module is provided, mocks directly on the module. Otherwise, uses string path. + """ + + # Mock initialize_orchestrator_connectors to return fake connectors + def _fake_initialize_orchestrator_connectors(config_path, worker_backend=None, shm_threshold_bytes=None): + # Create fake connectors for all stage-to-stage edges + # Each connector is just a mock object that will be passed to try_send_via_connector + fake_connectors = {} + # Add connectors for common edges (0->1, 1->2, etc.) + for i in range(10): # Support up to 10 stages + fake_connectors[(str(i), str(i + 1))] = MagicMock() + return None, fake_connectors + + if omni_module is not None: + # Mock directly on the omni module where it's used (after import) + monkeypatch.setattr(omni_module, "initialize_orchestrator_connectors", _fake_initialize_orchestrator_connectors) + else: + # Mock via string path (before import) + monkeypatch.setattr( + "vllm_omni.entrypoints.omni.initialize_orchestrator_connectors", + _fake_initialize_orchestrator_connectors, + raising=False, + ) + + +def _setup_connector_adapter_mock(monkeypatch, omni_module): + """Helper function to mock try_send_via_connector on the omni module. + + This must be called AFTER importing omni module, to mock the function where it's actually used. + """ + + # Mock try_send_via_connector to always succeed + def _fake_try_send_via_connector( + connector, + stage_id, + next_stage_id, + req_id, + next_inputs, + sampling_params, + original_prompt, + next_stage_queue_submit_fn, + metrics, + ): + # Simulate successful send by calling the submit function + task = { + "request_id": req_id, + "engine_inputs": next_inputs, + "sampling_params": sampling_params, + } + next_stage_queue_submit_fn(task) + return True + + # Mock directly on the omni module where it's used + monkeypatch.setattr(omni_module, "try_send_via_connector", _fake_try_send_via_connector) + + @pytest.fixture(autouse=True) def mock_get_config(monkeypatch): """Auto-mock get_config and related model loading functions to avoid model path validation.""" @@ -449,10 +600,19 @@ def mock_get_config(monkeypatch): def test_initialize_stage_configs_called_when_none(monkeypatch, fake_stage_config): """Test that stage configs are auto-loaded when stage_configs_path is None.""" - def _fake_loader(model: str, base_engine_args=None): - return [ - _FakeStageConfig(fake_stage_config), - _FakeStageConfig(fake_stage_config), + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + cfg0 = dict(fake_stage_config) + cfg0["stage_id"] = 0 + cfg1 = dict(fake_stage_config) + cfg1["stage_id"] = 1 + return None, [ + _FakeStageConfig(cfg0), + _FakeStageConfig(cfg1), ] # Remove modules from cache BEFORE setting mocks @@ -471,10 +631,11 @@ def test_initialize_stage_configs_called_when_none(monkeypatch, fake_stage_confi _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) - # Mock load_stage_configs_from_model + # Mock load_and_resolve_stage_configs monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -489,13 +650,13 @@ def test_initialize_stage_configs_called_when_none(monkeypatch, fake_stage_confi # Import the module after mocks are set import vllm_omni.entrypoints.omni as omni_module - # Patch the imported function and class in the module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) + # Patch the imported class in the module monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Verify: auto-loaded stage_configs and stage_list have consistent count assert isinstance(omni.stage_configs, list) assert len(omni.stage_configs) == 2 @@ -514,8 +675,13 @@ def test_initialize_stage_configs_called_when_none(monkeypatch, fake_stage_confi def test_generate_raises_on_length_mismatch(monkeypatch, fake_stage_config): """Test that generate raises ValueError when sampling_params_list length doesn't match.""" - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(fake_stage_config)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + return None, [_FakeStageConfig(fake_stage_config)] import sys @@ -531,14 +697,10 @@ def test_generate_raises_on_length_mismatch(monkeypatch, fake_stage_config): _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", - _fake_loader, - raising=False, - ) - monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -550,12 +712,12 @@ def test_generate_raises_on_length_mismatch(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) with pytest.raises(ValueError): omni.generate(prompts=["hi"], sampling_params_list=[]) @@ -563,11 +725,18 @@ def test_generate_raises_on_length_mismatch(monkeypatch, fake_stage_config): def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): """Test multi-stage generation pipeline with queue polling.""" stage_cfg0 = dict(fake_stage_config) + stage_cfg0["stage_id"] = 0 stage_cfg1 = dict(fake_stage_config) + stage_cfg1["stage_id"] = 1 stage_cfg1["processed_input"] = ["processed-for-stage-1"] - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + return None, [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] import sys @@ -575,6 +744,8 @@ def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): "vllm_omni.entrypoints.utils", "vllm_omni.entrypoints.omni", "vllm_omni.entrypoints.omni_stage", + "vllm_omni.distributed.omni_connectors.adapter", + "vllm_omni.distributed.omni_connectors", ]: if module_name in sys.modules: del sys.modules[module_name] @@ -585,12 +756,7 @@ def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): _setup_log_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", - _fake_loader, - raising=False, - ) - monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -602,8 +768,11 @@ def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) + # Apply connector and adapter mocks after importing omni module + _setup_connector_mocks(monkeypatch, omni_module) + _setup_connector_adapter_mock(monkeypatch, omni_module) # Mock uuid.uuid4() to return a predictable value for request ID generation test_uuid = uuid.UUID("00000000-0000-0000-0000-000000000000") @@ -612,7 +781,7 @@ def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Generate the expected request ID format: "0_" expected_request_id = f"0_{test_uuid}" @@ -625,7 +794,7 @@ def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): { "request_id": expected_request_id, "engine_outputs": [{"stage": 0, "text": "s0"}], - "metrics": {"num_tokens_out": 1, "stage_gen_time_ms": 10.0}, + "metrics": _FakeStageRequestStats(num_tokens_out=1, stage_gen_time_ms=10.0), } ) # Stage 1 output (will be collected after stage 0 forwards to it) @@ -637,7 +806,7 @@ def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): { "request_id": expected_request_id, "engine_outputs": [{"stage": 1, "text": "s1"}], - "metrics": {"num_tokens_out": 1, "stage_gen_time_ms": 10.0}, + "metrics": _FakeStageRequestStats(num_tokens_out=1, stage_gen_time_ms=10.0), } ) @@ -662,11 +831,18 @@ def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): def test_generate_pipeline_with_batch_input(monkeypatch, fake_stage_config): """Test single-stage generation pipeline with multiple inputs in one batch.""" stage_cfg0 = dict(fake_stage_config) - stage_cfg1 = dict(fake_stage_config) + stage_cfg0["stage_id"] = 0 stage_cfg0["final_output"] = False + stage_cfg1 = dict(fake_stage_config) + stage_cfg1["stage_id"] = 1 - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + return None, [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] import sys @@ -674,6 +850,8 @@ def test_generate_pipeline_with_batch_input(monkeypatch, fake_stage_config): "vllm_omni.entrypoints.utils", "vllm_omni.entrypoints.omni", "vllm_omni.entrypoints.omni_stage", + "vllm_omni.distributed.omni_connectors.adapter", + "vllm_omni.distributed.omni_connectors", ]: if module_name in sys.modules: del sys.modules[module_name] @@ -684,7 +862,7 @@ def test_generate_pipeline_with_batch_input(monkeypatch, fake_stage_config): _setup_log_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -696,8 +874,11 @@ def test_generate_pipeline_with_batch_input(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) + # Apply connector and adapter mocks after importing omni module + _setup_connector_mocks(monkeypatch, omni_module) + _setup_connector_adapter_mock(monkeypatch, omni_module) # Mock uuid.uuid4() to return a predictable value for request ID generation test_uuid = uuid.UUID("00000000-0000-0000-0000-000000000000") @@ -706,7 +887,7 @@ def test_generate_pipeline_with_batch_input(monkeypatch, fake_stage_config): from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Generate the expected request ID format: "0_" expected_request_id = f"0_{test_uuid}" @@ -718,28 +899,28 @@ def test_generate_pipeline_with_batch_input(monkeypatch, fake_stage_config): { "request_id": expected_request_id, "engine_outputs": [{"stage": 0, "text": "s0"}], - "metrics": {"num_tokens_out": 1, "stage_gen_time_ms": 10.0}, + "metrics": _FakeStageRequestStats(num_tokens_out=1, stage_gen_time_ms=10.0), } ) omni.stage_list[0]._out_q.put_nowait( { "request_id": expected_request_id, "engine_outputs": [{"stage": 0, "text": "s0"}], - "metrics": {"num_tokens_out": 1, "stage_gen_time_ms": 10.0}, + "metrics": _FakeStageRequestStats(num_tokens_out=1, stage_gen_time_ms=10.0), } ) omni.stage_list[1]._out_q.put_nowait( { "request_id": expected_request_id, "engine_outputs": [{"stage": 1}], - "metrics": {"num_tokens_out": 1, "stage_gen_time_ms": 10.0}, + "metrics": _FakeStageRequestStats(num_tokens_out=1, stage_gen_time_ms=10.0), } ) omni.stage_list[1]._out_q.put_nowait( { "request_id": expected_request_id, "engine_outputs": [{"stage": 1}], - "metrics": {"num_tokens_out": 1, "stage_gen_time_ms": 10.0}, + "metrics": _FakeStageRequestStats(num_tokens_out=1, stage_gen_time_ms=10.0), } ) @@ -768,12 +949,19 @@ def test_generate_pipeline_with_batch_input(monkeypatch, fake_stage_config): def test_generate_no_final_output_returns_empty(monkeypatch, fake_stage_config): """Test that generate returns empty list when all stages have final_output=False.""" stage_cfg0 = dict(fake_stage_config) - stage_cfg1 = dict(fake_stage_config) + stage_cfg0["stage_id"] = 0 stage_cfg0["final_output"] = False + stage_cfg1 = dict(fake_stage_config) + stage_cfg1["stage_id"] = 1 stage_cfg1["final_output"] = False - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + return None, [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] import sys @@ -781,6 +969,8 @@ def test_generate_no_final_output_returns_empty(monkeypatch, fake_stage_config): "vllm_omni.entrypoints.utils", "vllm_omni.entrypoints.omni", "vllm_omni.entrypoints.omni_stage", + "vllm_omni.distributed.omni_connectors.adapter", + "vllm_omni.distributed.omni_connectors", ]: if module_name in sys.modules: del sys.modules[module_name] @@ -791,7 +981,7 @@ def test_generate_no_final_output_returns_empty(monkeypatch, fake_stage_config): _setup_log_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -803,8 +993,11 @@ def test_generate_no_final_output_returns_empty(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) + # Apply connector and adapter mocks after importing omni module + _setup_connector_mocks(monkeypatch, omni_module) + _setup_connector_adapter_mock(monkeypatch, omni_module) # Mock uuid.uuid4() to return a predictable value for request ID generation test_uuid = uuid.UUID("00000000-0000-0000-0000-000000000000") @@ -813,7 +1006,7 @@ def test_generate_no_final_output_returns_empty(monkeypatch, fake_stage_config): from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Generate the expected request ID format: "0_" expected_request_id = f"0_{test_uuid}" @@ -847,12 +1040,19 @@ def test_generate_no_final_output_returns_empty(monkeypatch, fake_stage_config): def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_config): """Test that generate uses default sampling params when sampling_params_list is None.""" stage_cfg0 = dict(fake_stage_config) - stage_cfg1 = dict(fake_stage_config) + stage_cfg0["stage_id"] = 0 stage_cfg0["final_output"] = False + stage_cfg1 = dict(fake_stage_config) + stage_cfg1["stage_id"] = 1 stage_cfg1["final_output"] = False - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + return None, [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] import sys @@ -860,6 +1060,8 @@ def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_confi "vllm_omni.entrypoints.utils", "vllm_omni.entrypoints.omni", "vllm_omni.entrypoints.omni_stage", + "vllm_omni.distributed.omni_connectors.adapter", + "vllm_omni.distributed.omni_connectors", ]: if module_name in sys.modules: del sys.modules[module_name] @@ -870,7 +1072,7 @@ def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_confi _setup_log_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -882,8 +1084,11 @@ def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_confi import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) + # Apply connector and adapter mocks after importing omni module + _setup_connector_mocks(monkeypatch, omni_module) + _setup_connector_adapter_mock(monkeypatch, omni_module) # Mock uuid.uuid4() to return a predictable value for request ID generation test_uuid = uuid.UUID("00000000-0000-0000-0000-000000000000") @@ -892,7 +1097,7 @@ def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_confi from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Generate the expected request ID format: "0_" expected_request_id = f"0_{test_uuid}" @@ -902,14 +1107,14 @@ def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_confi { "request_id": expected_request_id, "engine_outputs": [{"stage": 0}], - "metrics": {"num_tokens_out": 1, "stage_gen_time_ms": 10.0}, + "metrics": _FakeStageRequestStats(num_tokens_out=1, stage_gen_time_ms=10.0), } ) omni.stage_list[1]._out_q.put_nowait( { "request_id": expected_request_id, "engine_outputs": [{"stage": 1}], - "metrics": {"num_tokens_out": 1, "stage_gen_time_ms": 10.0}, + "metrics": _FakeStageRequestStats(num_tokens_out=1, stage_gen_time_ms=10.0), } ) # Use the default sampling params @@ -919,8 +1124,13 @@ def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_confi def test_wait_for_stages_ready_timeout(monkeypatch, fake_stage_config): """Test that _wait_for_stages_ready handles timeout correctly.""" - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(fake_stage_config)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + return None, [_FakeStageConfig(fake_stage_config)] import sys @@ -936,9 +1146,10 @@ def test_wait_for_stages_ready_timeout(monkeypatch, fake_stage_config): _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -961,13 +1172,13 @@ def test_wait_for_stages_ready_timeout(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStageNoReady(cfg, **kwargs)) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) from vllm_omni.entrypoints.omni import Omni # Use very short timeout - omni = Omni(model="any", init_timeout=0.01) + omni = Omni(model=MODEL, init_timeout=0.01) # Verify that no stages are ready assert len(omni._stages_ready) == 0 @@ -975,8 +1186,13 @@ def test_wait_for_stages_ready_timeout(monkeypatch, fake_stage_config): def test_generate_handles_error_messages(monkeypatch, fake_stage_config): """Test that generate handles error messages from stages correctly.""" - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(fake_stage_config)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + return None, [_FakeStageConfig(fake_stage_config)] import sys @@ -992,9 +1208,10 @@ def test_generate_handles_error_messages(monkeypatch, fake_stage_config): _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -1006,8 +1223,8 @@ def test_generate_handles_error_messages(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) # Mock uuid.uuid4() to return a predictable value for request ID generation test_uuid = uuid.UUID("00000000-0000-0000-0000-000000000000") @@ -1016,7 +1233,7 @@ def test_generate_handles_error_messages(monkeypatch, fake_stage_config): from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Generate the expected request ID format: "0_" expected_request_id = f"0_{test_uuid}" @@ -1034,7 +1251,7 @@ def test_generate_handles_error_messages(monkeypatch, fake_stage_config): { "request_id": expected_request_id, "engine_outputs": [{"stage": 0, "text": "result"}], - "metrics": {"num_tokens_out": 1, "stage_gen_time_ms": 10.0}, + "metrics": _FakeStageRequestStats(num_tokens_out=1, stage_gen_time_ms=10.0), } ) @@ -1050,8 +1267,13 @@ def test_generate_handles_error_messages(monkeypatch, fake_stage_config): def test_close_sends_shutdown_signal(monkeypatch, fake_stage_config): """Test that close() sends shutdown signal to all input queues.""" - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(fake_stage_config)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + return None, [_FakeStageConfig(fake_stage_config)] import sys @@ -1067,9 +1289,10 @@ def test_close_sends_shutdown_signal(monkeypatch, fake_stage_config): _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -1081,12 +1304,12 @@ def test_close_sends_shutdown_signal(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Call close omni.close() diff --git a/tests/entrypoints/test_omni_llm.py b/tests/entrypoints/test_omni_llm.py index 4f05575ca..49971f1b4 100644 --- a/tests/entrypoints/test_omni_llm.py +++ b/tests/entrypoints/test_omni_llm.py @@ -18,6 +18,8 @@ warnings.filterwarnings( category=DeprecationWarning, ) +MODEL = "riverclouds/qwen_image_random" + class _FakeEngineArgs(dict): """Fake engine args that can be used both as object attributes and as **kwargs.""" @@ -60,8 +62,10 @@ class _FakeQueue: def put_nowait(self, item): self._queue.put_nowait(item) - def get(self): - return self._queue.get() + def get(self, timeout=None): + if timeout is None: + return self._queue.get() + return self._queue.get(timeout=timeout) def get_nowait(self): return self._queue.get_nowait() @@ -70,6 +74,52 @@ class _FakeQueue: return self._queue.empty() +class _FakeZmqQueue: + """Fake ZmqQueue that wraps _FakeQueue and matches ZmqQueue interface.""" + + def __init__( + self, + ctx=None, + socket_type=None, + *, + bind: str | None = None, + connect: str | None = None, + recv_timeout_ms: int | None = None, + send_timeout_ms: int | None = None, + ): + """Initialize fake ZMQ queue with same signature as real ZmqQueue.""" + self._queue = _FakeQueue(maxsize=0) + # Determine endpoint from bind or connect + path = bind if bind is not None else connect + self.endpoint = path or f"fake://zmq-endpoint-{id(self)}" + self._recv_timeout_ms = recv_timeout_ms + self._send_timeout_ms = send_timeout_ms + + def put(self, obj: Any) -> None: + """Send an object to the queue.""" + self._queue.put(obj) + + def put_nowait(self, obj: Any) -> None: + """Send an object to the queue without blocking.""" + self._queue.put_nowait(obj) + + def get(self, timeout: float | None = None) -> Any: + """Receive an object from the queue with optional timeout in seconds.""" + return self._queue.get(timeout=timeout) + + def get_nowait(self) -> Any: + """Receive an object from the queue without blocking.""" + return self._queue.get_nowait() + + def empty(self) -> bool: + """Check if the queue is empty without blocking.""" + return self._queue.empty() + + def close(self) -> None: + """Close the queue.""" + pass + + class _FakeStage: """Lightweight Stage stub for multi-process pipeline version with queue support.""" @@ -272,6 +322,19 @@ def _setup_multiprocessing_mocks(monkeypatch): monkeypatch.setattr(mp, "get_context", _mock_get_context, raising=False) monkeypatch.setattr(mp, "Process", fake_process_class, raising=False) + # Mock ZmqQueue to use _FakeZmqQueue + monkeypatch.setattr( + "vllm_omni.entrypoints.zmq_utils.ZmqQueue", + _FakeZmqQueue, + raising=False, + ) + # Also mock where ZmqQueue is imported/used + monkeypatch.setattr( + "vllm_omni.entrypoints.omni_stage.ZmqQueue", + _FakeZmqQueue, + raising=False, + ) + def _setup_ipc_mocks(monkeypatch): """Helper function to set up IPC function mocks.""" @@ -324,6 +387,65 @@ def _setup_log_mocks(monkeypatch): ) +def _setup_connector_mocks(monkeypatch, omni_module=None): + """Helper function to set up connector mocks for stage-to-stage forwarding. + + If omni_module is provided, mocks directly on the module. Otherwise, uses string path. + """ + + # Mock initialize_orchestrator_connectors to return fake connectors + def _fake_initialize_orchestrator_connectors(config_path, worker_backend=None, shm_threshold_bytes=None): + # Create fake connectors for all stage-to-stage edges + # Each connector is just a mock object that will be passed to try_send_via_connector + fake_connectors = {} + # Add connectors for common edges (0->1, 1->2, etc.) + for i in range(10): # Support up to 10 stages + fake_connectors[(str(i), str(i + 1))] = MagicMock() + return None, fake_connectors + + if omni_module is not None: + # Mock directly on the omni module where it's used (after import) + monkeypatch.setattr(omni_module, "initialize_orchestrator_connectors", _fake_initialize_orchestrator_connectors) + else: + # Mock via string path (before import) + monkeypatch.setattr( + "vllm_omni.entrypoints.omni.initialize_orchestrator_connectors", + _fake_initialize_orchestrator_connectors, + raising=False, + ) + + +def _setup_connector_adapter_mock(monkeypatch, omni_module): + """Helper function to mock try_send_via_connector on the omni module. + + This must be called AFTER importing omni module, to mock the function where it's actually used. + """ + + # Mock try_send_via_connector to always succeed + def _fake_try_send_via_connector( + connector, + stage_id, + next_stage_id, + req_id, + next_inputs, + sampling_params, + original_prompt, + next_stage_queue_submit_fn, + metrics, + ): + # Simulate successful send by calling the submit function + task = { + "request_id": req_id, + "engine_inputs": next_inputs, + "sampling_params": sampling_params, + } + next_stage_queue_submit_fn(task) + return True + + # Mock directly on the omni module where it's used + monkeypatch.setattr(omni_module, "try_send_via_connector", _fake_try_send_via_connector) + + @pytest.fixture(autouse=True) def mock_get_config(monkeypatch): """Auto-mock get_config and related model loading functions to avoid model path validation.""" @@ -449,10 +571,19 @@ def mock_get_config(monkeypatch): def test_initialize_stage_configs_called_when_none(monkeypatch, fake_stage_config): """Test that stage configs are auto-loaded when stage_configs_path is None.""" - def _fake_loader(model: str, base_engine_args=None): - return [ - _FakeStageConfig(fake_stage_config), - _FakeStageConfig(fake_stage_config), + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + cfg0 = dict(fake_stage_config) + cfg0["stage_id"] = 0 + cfg1 = dict(fake_stage_config) + cfg1["stage_id"] = 1 + return None, [ + _FakeStageConfig(cfg0), + _FakeStageConfig(cfg1), ] # Remove modules from cache BEFORE setting mocks @@ -471,10 +602,11 @@ def test_initialize_stage_configs_called_when_none(monkeypatch, fake_stage_confi _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) - # Mock load_stage_configs_from_model + # Mock load_and_resolve_stage_configs monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -490,12 +622,14 @@ def test_initialize_stage_configs_called_when_none(monkeypatch, fake_stage_confi import vllm_omni.entrypoints.omni as omni_module # Patch the imported function and class in the module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + # Apply connector and adapter mocks after importing omni module + _setup_connector_mocks(monkeypatch, omni_module) from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Verify: auto-loaded stage_configs and stage_list have consistent count assert isinstance(omni.stage_configs, list) assert len(omni.stage_configs) == 2 @@ -514,8 +648,15 @@ def test_initialize_stage_configs_called_when_none(monkeypatch, fake_stage_confi def test_generate_raises_on_length_mismatch(monkeypatch, fake_stage_config): """Test that generate raises ValueError when sampling_params_list length doesn't match.""" - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(fake_stage_config)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + cfg0 = dict(fake_stage_config) + cfg0["stage_id"] = 0 + return None, [_FakeStageConfig(cfg0)] import sys @@ -531,14 +672,10 @@ def test_generate_raises_on_length_mismatch(monkeypatch, fake_stage_config): _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", - _fake_loader, - raising=False, - ) - monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -550,12 +687,12 @@ def test_generate_raises_on_length_mismatch(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) with pytest.raises(ValueError): omni.generate(prompts=["hi"], sampling_params_list=[]) @@ -563,11 +700,18 @@ def test_generate_raises_on_length_mismatch(monkeypatch, fake_stage_config): def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): """Test multi-stage generation pipeline with queue polling.""" stage_cfg0 = dict(fake_stage_config) + stage_cfg0["stage_id"] = 0 stage_cfg1 = dict(fake_stage_config) + stage_cfg1["stage_id"] = 1 stage_cfg1["processed_input"] = ["processed-for-stage-1"] - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + return None, [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] import sys @@ -583,14 +727,10 @@ def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", - _fake_loader, - raising=False, - ) - monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -602,8 +742,11 @@ def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + # Apply connector and adapter mocks after importing omni module + _setup_connector_mocks(monkeypatch, omni_module) + _setup_connector_adapter_mock(monkeypatch, omni_module) # Mock uuid.uuid4() to return a predictable value for request ID generation test_uuid = uuid.UUID("00000000-0000-0000-0000-000000000000") @@ -612,7 +755,7 @@ def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Generate the expected request ID format: "0_" expected_request_id = f"0_{test_uuid}" @@ -662,12 +805,19 @@ def test_generate_pipeline_and_final_outputs(monkeypatch, fake_stage_config): def test_generate_no_final_output_returns_empty(monkeypatch, fake_stage_config): """Test that generate returns empty list when all stages have final_output=False.""" stage_cfg0 = dict(fake_stage_config) - stage_cfg1 = dict(fake_stage_config) + stage_cfg0["stage_id"] = 0 stage_cfg0["final_output"] = False + stage_cfg1 = dict(fake_stage_config) + stage_cfg1["stage_id"] = 1 stage_cfg1["final_output"] = False - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + return None, [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] import sys @@ -683,9 +833,10 @@ def test_generate_no_final_output_returns_empty(monkeypatch, fake_stage_config): _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -697,8 +848,11 @@ def test_generate_no_final_output_returns_empty(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + # Apply connector and adapter mocks after importing omni module + _setup_connector_mocks(monkeypatch, omni_module) + _setup_connector_adapter_mock(monkeypatch, omni_module) # Mock uuid.uuid4() to return a predictable value for request ID generation test_uuid = uuid.UUID("00000000-0000-0000-0000-000000000000") @@ -707,7 +861,7 @@ def test_generate_no_final_output_returns_empty(monkeypatch, fake_stage_config): from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Generate the expected request ID format: "0_" expected_request_id = f"0_{test_uuid}" @@ -741,12 +895,19 @@ def test_generate_no_final_output_returns_empty(monkeypatch, fake_stage_config): def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_config): """Test that generate uses default sampling params when sampling_params_list is None.""" stage_cfg0 = dict(fake_stage_config) - stage_cfg1 = dict(fake_stage_config) + stage_cfg0["stage_id"] = 0 stage_cfg0["final_output"] = False + stage_cfg1 = dict(fake_stage_config) + stage_cfg1["stage_id"] = 1 stage_cfg1["final_output"] = False - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + return None, [_FakeStageConfig(stage_cfg0), _FakeStageConfig(stage_cfg1)] import sys @@ -762,9 +923,10 @@ def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_confi _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -776,8 +938,11 @@ def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_confi import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) + # Apply connector and adapter mocks after importing omni module + _setup_connector_mocks(monkeypatch, omni_module) + _setup_connector_adapter_mock(monkeypatch, omni_module) # Mock uuid.uuid4() to return a predictable value for request ID generation test_uuid = uuid.UUID("00000000-0000-0000-0000-000000000000") @@ -786,7 +951,7 @@ def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_confi from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Generate the expected request ID format: "0_" expected_request_id = f"0_{test_uuid}" @@ -813,8 +978,15 @@ def test_generate_sampling_params_none_use_default(monkeypatch, fake_stage_confi def test_wait_for_stages_ready_timeout(monkeypatch, fake_stage_config): """Test that _wait_for_stages_ready handles timeout correctly.""" - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(fake_stage_config)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + cfg0 = dict(fake_stage_config) + cfg0["stage_id"] = 0 + return None, [_FakeStageConfig(cfg0)] import sys @@ -830,9 +1002,10 @@ def test_wait_for_stages_ready_timeout(monkeypatch, fake_stage_config): _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -855,13 +1028,13 @@ def test_wait_for_stages_ready_timeout(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStageNoReady(cfg, **kwargs)) from vllm_omni.entrypoints.omni import Omni # Use very short timeout - omni = Omni(model="any", init_timeout=0.01) + omni = Omni(model=MODEL, init_timeout=0.01) # Verify that no stages are ready assert len(omni._stages_ready) == 0 @@ -869,8 +1042,15 @@ def test_wait_for_stages_ready_timeout(monkeypatch, fake_stage_config): def test_generate_handles_error_messages(monkeypatch, fake_stage_config): """Test that generate handles error messages from stages correctly.""" - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(fake_stage_config)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + cfg0 = dict(fake_stage_config) + cfg0["stage_id"] = 0 + return None, [_FakeStageConfig(cfg0)] import sys @@ -886,9 +1066,10 @@ def test_generate_handles_error_messages(monkeypatch, fake_stage_config): _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -900,7 +1081,7 @@ def test_generate_handles_error_messages(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) # Mock uuid.uuid4() to return a predictable value for request ID generation @@ -910,7 +1091,7 @@ def test_generate_handles_error_messages(monkeypatch, fake_stage_config): from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Generate the expected request ID format: "0_" expected_request_id = f"0_{test_uuid}" @@ -944,8 +1125,15 @@ def test_generate_handles_error_messages(monkeypatch, fake_stage_config): def test_close_sends_shutdown_signal(monkeypatch, fake_stage_config): """Test that close() sends shutdown signal to all input queues.""" - def _fake_loader(model: str, base_engine_args=None): - return [_FakeStageConfig(fake_stage_config)] + def _fake_loader( + model: str, + stage_configs_path: str | None = None, + base_engine_args: dict | None = None, + default_stage_cfg_factory=None, + ): + cfg0 = dict(fake_stage_config) + cfg0["stage_id"] = 0 + return None, [_FakeStageConfig(cfg0)] import sys @@ -961,9 +1149,10 @@ def test_close_sends_shutdown_signal(monkeypatch, fake_stage_config): _setup_multiprocessing_mocks(monkeypatch) _setup_ipc_mocks(monkeypatch) _setup_log_mocks(monkeypatch) + _setup_connector_mocks(monkeypatch) monkeypatch.setattr( - "vllm_omni.entrypoints.utils.load_stage_configs_from_model", + "vllm_omni.entrypoints.utils.load_and_resolve_stage_configs", _fake_loader, raising=False, ) @@ -975,12 +1164,12 @@ def test_close_sends_shutdown_signal(monkeypatch, fake_stage_config): import vllm_omni.entrypoints.omni as omni_module - monkeypatch.setattr(omni_module, "load_stage_configs_from_model", _fake_loader) + monkeypatch.setattr(omni_module, "load_and_resolve_stage_configs", _fake_loader) monkeypatch.setattr(omni_module, "OmniStage", lambda cfg, **kwargs: _FakeStage(cfg, **kwargs)) from vllm_omni.entrypoints.omni import Omni - omni = Omni(model="any", init_timeout=1) + omni = Omni(model=MODEL, init_timeout=1) # Call close omni.close() diff --git a/vllm_omni/entrypoints/async_omni.py b/vllm_omni/entrypoints/async_omni.py index d00652a65..08c4ce903 100644 --- a/vllm_omni/entrypoints/async_omni.py +++ b/vllm_omni/entrypoints/async_omni.py @@ -38,7 +38,7 @@ from vllm_omni.outputs import OmniRequestOutput logger = init_logger(__name__) -def _weak_close_cleanup_async(stage_list, stage_in_queues, ray_pg, output_handler): +def _weak_close_cleanup_async(stage_list, stage_in_queues, stage_out_queues, ray_pg, output_handler, zmq_ctx=None): """Weak reference cleanup function for AsyncOmni instances.""" if stage_list: for q in stage_in_queues: @@ -46,6 +46,13 @@ def _weak_close_cleanup_async(stage_list, stage_in_queues, ray_pg, output_handle q.put_nowait(SHUTDOWN_TASK) except Exception as e: logger.warning(f"Failed to send shutdown signal to stage input queue: {e}") + close_fn = getattr(q, "close", None) + if callable(close_fn): + close_fn() + for q in stage_out_queues: + close_fn = getattr(q, "close", None) + if callable(close_fn): + close_fn() for stage in stage_list: try: stage.stop_stage_worker() @@ -55,6 +62,8 @@ def _weak_close_cleanup_async(stage_list, stage_in_queues, ray_pg, output_handle # Cancel output handler if output_handler is not None: output_handler.cancel() + if zmq_ctx is not None: + zmq_ctx.term() class AsyncOmni(OmniBase): @@ -108,8 +117,10 @@ class AsyncOmni(OmniBase): _weak_close_cleanup_async, self.stage_list, self._stage_in_queues, + self._stage_out_queues, self._ray_pg, self.output_handler, + self._zmq_ctx, ) def _create_default_diffusion_stage_cfg(self, kwargs: dict[str, Any]) -> dict[str, Any]: diff --git a/vllm_omni/entrypoints/cli/serve.py b/vllm_omni/entrypoints/cli/serve.py index e28637f6a..73bb03805 100644 --- a/vllm_omni/entrypoints/cli/serve.py +++ b/vllm_omni/entrypoints/cli/serve.py @@ -7,19 +7,37 @@ diffusion models (e.g., Qwen-Image) through the same CLI interface. import argparse import os +import signal +from typing import Any +import msgspec.msgpack import uvloop +import zmq from vllm.entrypoints.cli.types import CLISubcommand from vllm.entrypoints.openai.cli_args import make_arg_parser, validate_parsed_serve_args from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG from vllm.logger import init_logger from vllm.utils.argparse_utils import FlexibleArgumentParser +from vllm.utils.network_utils import make_zmq_socket +from vllm.v1.utils import get_engine_client_zmq_addr +from vllm_omni.distributed.omni_connectors import ( + get_connectors_config_for_stage, + load_omni_transfer_config, +) +from vllm_omni.distributed.omni_connectors.utils.initialization import ( + resolve_omni_kv_config_for_stage, +) from vllm_omni.entrypoints.cli.logo import log_logo +from vllm_omni.entrypoints.omni import OmniBase, omni_snapshot_download +from vllm_omni.entrypoints.omni_stage import OmniStage from vllm_omni.entrypoints.openai.api_server import omni_run_server +from vllm_omni.entrypoints.utils import inject_omni_kv_config logger = init_logger(__name__) +HANDSHAKE_TIMEOUT_MINS = 5 + DESCRIPTION = """Launch a local OpenAI-compatible API server to serve Omni models via HTTP. Supports both multi-stage LLM models and diffusion models. @@ -55,9 +73,15 @@ class OmniServeCommand(CLISubcommand): if hasattr(args, "model_tag") and args.model_tag is not None: args.model = args.model_tag - uvloop.run(omni_run_server(args)) + if args.headless: + run_headless(args) + else: + uvloop.run(omni_run_server(args)) def validate(self, args: argparse.Namespace) -> None: + if args.stage_id is not None and (args.omni_master_address is None or args.omni_master_port is None): + raise ValueError("--stage-id requires both --omni-master-address and --omni-master-port to be set") + # Skip validation for diffusion models as they have different requirements from vllm_omni.diffusion.utils.hf_utils import is_diffusion_model @@ -94,6 +118,12 @@ class OmniServeCommand(CLISubcommand): default=None, help="Path to the stage configs file. If not specified, the stage configs will be loaded from the model.", ) + omni_config_group.add_argument( + "--stage-id", + type=int, + default=None, + help="Select and launch a single stage by stage_id.", + ) omni_config_group.add_argument( "--stage-init-timeout", type=int, @@ -142,6 +172,18 @@ class OmniServeCommand(CLISubcommand): default=None, help="The address of the Ray cluster to connect to.", ) + omni_config_group.add_argument( + "--omni-master-address", + "-oma", + type=str, + help="Hostname or IP address of the Omni orchestrator (master).", + ) + omni_config_group.add_argument( + "--omni-master-port", + "-omp", + type=int, + help="Port of the Omni orchestrator (master).", + ) # Diffusion model specific arguments omni_config_group.add_argument( @@ -251,5 +293,125 @@ class OmniServeCommand(CLISubcommand): return serve_parser +def _create_default_diffusion_stage_cfg(args: argparse.Namespace) -> list[dict[str, Any]]: + omni_base = OmniBase.__new__(OmniBase) + return omni_base._create_default_diffusion_stage_cfg(vars(args)) + + +def run_headless(args: argparse.Namespace) -> None: + if args.api_server_count is not None and args.api_server_count > 1: + raise ValueError("api_server_count can't be set in headless mode") + if args.worker_backend != "multi_process": + raise ValueError("headless mode requires worker_backend=multi_process") + + model = omni_snapshot_download(args.model) + + omni_base = OmniBase.__new__(OmniBase) + args_dict = vars(args).copy() + args_dict["model"] = model + config_path, stage_configs = omni_base._resolve_stage_configs(model, args_dict) + + single_stage_id = args.stage_id + if single_stage_id is None: + if len(stage_configs) != 1: + raise ValueError("--stage-id is required in headless mode for multi-stage configs") + single_stage_id = getattr(stage_configs[0], "stage_id", 0) + + stage_config = None + for cfg in stage_configs: + if getattr(cfg, "stage_id", None) == single_stage_id: + stage_config = cfg + break + if stage_config is None: + raise ValueError(f"No stage matches stage_id={single_stage_id}.") + + # TODO(wuhang): Support connectors config by cli + transfer_config = load_omni_transfer_config(config_path, default_shm_threshold=args.shm_threshold_bytes) + connectors_config = get_connectors_config_for_stage(transfer_config, single_stage_id) + + omni_master_address = args.omni_master_address + omni_master_port = args.omni_master_port + + # Perform handshake with orchestrator to get dynamically allocated endpoints + with zmq.Context() as zmq_ctx: + handshake_endpoint = get_engine_client_zmq_addr( + local_only=False, host=omni_master_address, port=omni_master_port + ) + + with make_zmq_socket(zmq_ctx, handshake_endpoint, zmq.REQ, bind=False, linger=5000) as handshake_socket: + # TODO(wuhang): Define protocol in python dataclass. + handshake_msg = {"type": "handshake", "stage_id": single_stage_id} + handshake_socket.send(msgspec.msgpack.encode(handshake_msg)) + + # Wait for response with timeout + if not handshake_socket.poll(timeout=HANDSHAKE_TIMEOUT_MINS * 60_000): + raise RuntimeError( + f"Handshake timeout ({HANDSHAKE_TIMEOUT_MINS} minutes) for stage-{single_stage_id} " + f"at {handshake_endpoint}" + ) + + try: + response = msgspec.msgpack.decode(handshake_socket.recv()) + except msgspec.DecodeError as exc: + raise RuntimeError( + f"Handshake decode failed for stage-{single_stage_id} at {handshake_endpoint}: {exc}" + ) from exc + except Exception as exc: # pragma: no cover - unexpected decode errors + raise RuntimeError( + f"Unexpected error decoding handshake for stage-{single_stage_id} at {handshake_endpoint}: {exc}" + ) from exc + + if not response["ok"]: + error_msg = response["error"] + raise RuntimeError(f"Handshake failed for stage-{single_stage_id}: {error_msg}") + + in_endpoint, out_endpoint = response["in_endpoint"], response["out_endpoint"] + + logger.info( + f"[Headless] Stage-{single_stage_id} received endpoints via handshake: " + f"in={in_endpoint}, out={out_endpoint}" + ) + + shutdown_requested = False + + def signal_handler(signum, frame): + nonlocal shutdown_requested + if shutdown_requested: + return + shutdown_requested = True + raise SystemExit + + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + stage = OmniStage(stage_config, stage_init_timeout=args.stage_init_timeout) + stage.attach_queues(in_endpoint, out_endpoint) + + # Inject YAML-resolved connector config into omni_kv_config for in-engine usage. + try: + omni_conn_cfg, omni_from, omni_to = resolve_omni_kv_config_for_stage(transfer_config, single_stage_id) + if omni_conn_cfg: + inject_omni_kv_config(stage, omni_conn_cfg, omni_from, omni_to) # type: ignore + except Exception as e: + logger.debug("[Headless] Failed to inject omni connector config into stage-%s: %s", single_stage_id, e) + + old_env = os.environ.get("VLLM_LOGGING_PREFIX") + os.environ["VLLM_LOGGING_PREFIX"] = f"[Stage-{single_stage_id}] {'' if old_env is None else old_env}" + try: + stage.init_stage_worker( + model, + is_async=True, + shm_threshold_bytes=int(args.shm_threshold_bytes), + batch_timeout=int(args.batch_timeout), + connectors_config=connectors_config, + worker_backend="multi_process", + ignore_runtime_config=True, + ) + if stage._proc is not None: + stage._proc.join() + finally: + stage.stop_stage_worker() + + def cmd_init() -> list[CLISubcommand]: return [OmniServeCommand()] diff --git a/vllm_omni/entrypoints/omni.py b/vllm_omni/entrypoints/omni.py index f30cd7d36..051b2acf0 100644 --- a/vllm_omni/entrypoints/omni.py +++ b/vllm_omni/entrypoints/omni.py @@ -3,6 +3,7 @@ import json import multiprocessing as mp import os +import threading import time import uuid import weakref @@ -11,10 +12,14 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Any, Literal, overload import huggingface_hub +import msgspec.msgpack +import zmq from omegaconf import OmegaConf from tqdm.auto import tqdm from vllm import SamplingParams from vllm.logger import init_logger +from vllm.utils.network_utils import make_zmq_socket +from vllm.v1.utils import get_engine_client_zmq_addr from vllm_omni.distributed.omni_connectors import ( get_stage_connector_config, @@ -33,12 +38,12 @@ from vllm_omni.entrypoints.omni_stage import OmniStage from vllm_omni.entrypoints.stage_utils import SHUTDOWN_TASK, OmniStageTaskType from vllm_omni.entrypoints.stage_utils import maybe_load_from_ipc as _load from vllm_omni.entrypoints.utils import ( + build_base_engine_args, get_final_stage_id_for_e2e, inject_omni_kv_config, - load_stage_configs_from_model, - load_stage_configs_from_yaml, - resolve_model_config_path, + load_and_resolve_stage_configs, ) +from vllm_omni.entrypoints.zmq_utils import ZmqQueue from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniPromptType, OmniSamplingParams from vllm_omni.metrics import OrchestratorAggregator, StageRequestStats from vllm_omni.model_executor.model_loader.weight_utils import ( @@ -49,7 +54,16 @@ from vllm_omni.outputs import OmniRequestOutput logger = init_logger(__name__) -def _weak_close_cleanup(stage_list, stage_in_queues, ray_pg): +def _weak_close_cleanup( + stage_list, + stage_in_queues, + stage_out_queues, + ray_pg, + zmq_ctx=None, + handshake_stop: threading.Event | None = None, + zmq_handshake_socket: zmq.Socket | None = None, + handshake_thread: threading.Thread | None = None, +): """Weak reference cleanup function for OmniBase instances.""" if stage_list: for q in stage_in_queues: @@ -57,6 +71,13 @@ def _weak_close_cleanup(stage_list, stage_in_queues, ray_pg): q.put_nowait(SHUTDOWN_TASK) except Exception as e: logger.warning(f"Failed to send shutdown signal to stage input queue: {e}") + close_fn = getattr(q, "close", None) + if callable(close_fn): + close_fn() + for q in stage_out_queues: + close_fn = getattr(q, "close", None) + if callable(close_fn): + close_fn() for stage in stage_list: try: stage.stop_stage_worker() @@ -64,6 +85,20 @@ def _weak_close_cleanup(stage_list, stage_in_queues, ray_pg): logger.warning(f"Failed to stop stage worker: {e}") try_close_ray(ray_pg) + # Gracefully shutdown handshake server thread + if handshake_stop is not None: + handshake_stop.set() + if handshake_thread is not None: + handshake_thread.join(timeout=2.0) + if handshake_thread.is_alive(): + logger.warning("Handshake server thread did not terminate gracefully within timeout") + + # Close ZMQ resources after thread has exited + if zmq_handshake_socket is not None: + zmq_handshake_socket.close(0) + if zmq_ctx is not None: + zmq_ctx.term() + def _dummy_snapshot_download(model_id): return model_id @@ -125,12 +160,21 @@ class OmniBase: # Stage management attributes self.stage_list: list[OmniStage] = [] - self._stage_in_queues: list[mp.Queue] = [] - self._stage_out_queues: list[mp.Queue] = [] + self._stage_in_queues: list[Any] = [] + self._stage_out_queues: list[Any] = [] self._stages_ready: set[int] = set() self._ray_pg = None self._queue_cls = None self._ctx = None + self._zmq_ctx: zmq.Context | None = None + self._zmq_master_address: str | None = None + self._zmq_master_port: int | None = None + self._zmq_handshake_socket: zmq.Socket | None = None + self._handshake_thread: threading.Thread | None = None + self._handshake_stop: threading.Event | None = None + self._handshake_endpoints: dict[int, tuple[str, str]] = {} + self._handshake_seen: set[int] = set() # Track which stage IDs have completed ZMQ handshake + self._single_stage_id: int | None = None # Optional: deploy only a specific stage ID # Initialize stages - each stage will create appropriate instance based on stage_type # Stage workers will automatically create OmniLLM or OmniDiffusion instances @@ -206,35 +250,25 @@ class OmniBase: default_stage_cfg[0]["engine_args"]["model_stage"] = "diffusion" return default_stage_cfg - def _initialize_stages(self, model: str, kwargs: dict[str, Any]) -> None: - """Initialize stage list management.""" - stage_init_timeout = kwargs.get("stage_init_timeout", 20) - shm_threshold_bytes = kwargs.get("shm_threshold_bytes", 65536) - init_timeout = kwargs.get("init_timeout", 300) - worker_backend = kwargs.get("worker_backend", "multi_process") - ray_address = kwargs.get("ray_address", None) - batch_timeout = kwargs.get("batch_timeout", 10) + def _resolve_stage_configs(self, model: str, kwargs: dict[str, Any]) -> tuple[str, list[Any]]: + """Resolve stage configs and inject defaults shared by orchestrator/headless.""" + # TODO(wuhang): + # Remove kwargs as parameters in the future. + # Use dataclass directly for engine args. + base_engine_args = build_base_engine_args(kwargs) + stage_configs_path = kwargs.get("stage_configs_path", None) - log_stats = kwargs.get("log_stats", False) - - ### base engine args - tokenizer = kwargs.get("tokenizer", None) - - base_engine_args = {"tokenizer": tokenizer} if tokenizer is not None else None # Load stage configurations from YAML - if stage_configs_path is None: - self.config_path = resolve_model_config_path(model) - self.stage_configs = load_stage_configs_from_model(model, base_engine_args=base_engine_args) - if not self.stage_configs: - default_stage_cfg = self._create_default_diffusion_stage_cfg(kwargs) - self.stage_configs = OmegaConf.create(default_stage_cfg) - else: - self.config_path = stage_configs_path - self.stage_configs = load_stage_configs_from_yaml(stage_configs_path, base_engine_args=base_engine_args) + config_path, stage_configs = load_and_resolve_stage_configs( + model, + stage_configs_path, + base_engine_args, + default_stage_cfg_factory=lambda: self._create_default_diffusion_stage_cfg(kwargs), + ) # Inject diffusion LoRA-related knobs from kwargs if not present in the stage config. - for cfg in self.stage_configs: + for cfg in stage_configs: try: if getattr(cfg, "stage_type", None) != "diffusion": continue @@ -253,6 +287,27 @@ class OmniBase: except Exception as e: logger.warning("Failed to inject LoRA config for stage: %s", e) + return config_path, stage_configs + + def _initialize_stages(self, model: str, kwargs: dict[str, Any]) -> None: + """Initialize stage list management.""" + stage_init_timeout = kwargs.get("stage_init_timeout", 20) + shm_threshold_bytes = kwargs.get("shm_threshold_bytes", 65536) + init_timeout = kwargs.get("init_timeout", 300) + worker_backend = kwargs.get("worker_backend", "multi_process") + ray_address = kwargs.get("ray_address", None) + batch_timeout = kwargs.get("batch_timeout", 10) + log_stats = kwargs.get("log_stats", False) + self._single_stage_id = kwargs.get("stage_id", None) + self._zmq_master_address = kwargs.get("omni_master_address", None) + if self._zmq_master_address is None: + self._zmq_master_address = "127.0.0.1" + logger.info("No omni_master_address provided, defaulting to localhost (127.0.0.1)") + self._zmq_master_port = kwargs.get("omni_master_port", None) + + # Resolve stage configs shared by orchestrator/headless paths. + self.config_path, self.stage_configs = self._resolve_stage_configs(model, kwargs) + # Initialize connectors self.omni_transfer_config, self.connectors = initialize_orchestrator_connectors( self.config_path, worker_backend=worker_backend, shm_threshold_bytes=shm_threshold_bytes @@ -281,7 +336,7 @@ class OmniBase: self.stage_list = [st for _, st in results] self.default_sampling_params_list = [st.default_sampling_params for st in self.stage_list] self.output_modalities = [st.final_output_type for st in self.stage_list] - logger.debug(f"[{self._name}] Loaded {len(self.stage_list)} stages") + logger.info(f"[{self._name}] Loaded {len(self.stage_list)} stages") if self.worker_backend == "ray": self._queue_cls = get_ray_queue_class() @@ -307,10 +362,34 @@ class OmniBase: self._ray_pg = create_placement_group( number_of_stages=len(self.stage_list), address=self.ray_address, strategy="PACK" ) + else: + # Initialize ZMQ context + if self._zmq_ctx is None: + self._zmq_ctx = zmq.Context() + + # Allocate endpoints for each stage + total_stages = len(self.stage_configs) + self._handshake_endpoints = {} + for sid in range(total_stages): + in_endpoint = get_engine_client_zmq_addr(local_only=False, host=self._zmq_master_address) + out_endpoint = get_engine_client_zmq_addr(local_only=False, host=self._zmq_master_address) + self._handshake_endpoints[sid] = (in_endpoint, out_endpoint) + logger.debug( + f"[{self._name}] Allocated endpoints for stage-{sid}: in={in_endpoint}, out={out_endpoint}" + ) + + # Start handshake server + self.start_handshake_server() for stage_id, stage in enumerate[OmniStage](self.stage_list): - in_q = self._queue_cls() - out_q = self._queue_cls() + if self.worker_backend == "ray": + in_q = self._queue_cls() + out_q = self._queue_cls() + else: + in_endpoint, out_endpoint = self._handshake_endpoints[stage_id] + in_q = ZmqQueue(self._zmq_ctx, zmq.PUSH, bind=in_endpoint) + out_q = ZmqQueue(self._zmq_ctx, zmq.PULL, bind=out_endpoint) + self._stage_in_queues.append(in_q) self._stage_out_queues.append(out_q) stage.attach_queues(in_q, out_q) @@ -332,6 +411,12 @@ class OmniBase: except Exception as e: logger.debug("[Omni] Failed to inject omni connector config into stage-%s: %s", stage_id, e) + if self._single_stage_id is not None and stage_id != int(self._single_stage_id): + logger.info( + f"[{self._name}] Skipping initialization of stage-{stage_id} worker due to single_stage_id setting" + ) + continue + stage.init_stage_worker( model, is_async=self.is_async, @@ -341,6 +426,7 @@ class OmniBase: connectors_config=stage_connectors_config, worker_backend=self.worker_backend, ray_placement_group=self._ray_pg, + ignore_runtime_config=True if self._single_stage_id is not None else False, ) logger.debug(f"[{self._name}] Stage-{stage_id} process started") @@ -351,6 +437,9 @@ class OmniBase: def _wait_for_stages_ready(self, timeout: int = 120) -> None: """Wait for all stages to report readiness with optimized polling.""" + if self._single_stage_id is not None and self.worker_backend != "ray": + timeout = self._wait_for_handshakes(timeout) + num_stages = len(self.stage_list) deadline = time.time() + max(0, int(timeout)) @@ -501,6 +590,129 @@ class OmniBase: if hasattr(self, "_weak_finalizer"): self._weak_finalizer() + def _process_handshake_message(self, msg: Any) -> dict[str, Any]: + """Process incoming handshake message and generate response. + + Args: + msg: Decoded message from client + + Returns: + Response dictionary with ok status and either endpoints or error + """ + if not isinstance(msg, dict) or msg.get("type") != "handshake": + return {"ok": False, "error": "invalid handshake payload"} + + try: + stage_id = int(msg.get("stage_id")) + except (TypeError, ValueError) as e: + return {"ok": False, "error": f"invalid stage_id: {e}"} + + endpoints = self._handshake_endpoints.get(stage_id) + if endpoints is None: + return {"ok": False, "error": f"unknown stage_id: {stage_id}"} + + # Mark stage as seen and prepare success response + self._handshake_seen.add(stage_id) + in_endpoint, out_endpoint = endpoints + + logger.info( + "[%s] Handshake received from stage-%s", + self._name, + stage_id, + ) + + return { + "ok": True, + "in_endpoint": in_endpoint, + "out_endpoint": out_endpoint, + } + + def _run_handshake_server_loop(self) -> None: + """Main loop for handshake server - polls for messages and responds.""" + poller = zmq.Poller() + poller.register(self._zmq_handshake_socket, zmq.POLLIN) + + try: + while not self._handshake_stop.is_set(): + events = poller.poll(1000) + has_message = any(sock == self._zmq_handshake_socket and event == zmq.POLLIN for sock, event in events) + if not has_message: + continue + + msg = msgspec.msgpack.decode(self._zmq_handshake_socket.recv()) + response = msgspec.msgpack.encode(self._process_handshake_message(msg)) + self._zmq_handshake_socket.send(response) + finally: + poller.unregister(self._zmq_handshake_socket) + + def start_handshake_server(self) -> None: + """Start the ZMQ handshake server. + + The handshake server allows distributed stages to discover their + queue endpoints by querying the orchestrator with their stage_id. + Skips starting if the server is already running or ZMQ is not initialized. + """ + # Skip if already running or ZMQ not initialized + if self._handshake_thread is not None or self._zmq_ctx is None: + return + + # Skip if master address/port not configured + if not self._zmq_master_address or self._zmq_master_port is None: + return + + # Create server endpoint and socket + endpoint = get_engine_client_zmq_addr( + local_only=False, host=self._zmq_master_address, port=int(self._zmq_master_port) + ) + + self._handshake_stop = threading.Event() + self._zmq_handshake_socket = make_zmq_socket(self._zmq_ctx, endpoint, zmq.REP, bind=True, linger=5000) + + # Start server thread + self._handshake_thread = threading.Thread( + target=self._run_handshake_server_loop, daemon=True, name="zmq-handshake-server" + ) + self._handshake_thread.start() + + def _wait_for_handshakes(self, timeout: int = 120) -> int: + """Wait for handshakes from all expected stages. + + Args: + timeout: Timeout in seconds for waiting for handshakes. Default is 120s. + + Returns: + Remaining timeout in seconds after waiting for handshakes. + """ + total_stages = len(self.stage_configs) + expected = set(range(total_stages)) - {int(self._single_stage_id)} + if not expected: + return timeout + + deadline = time.time() + max(0, int(timeout)) + logger.info(f"[{self._name}] Waiting for handshakes from stages: {expected} (timeout: {timeout}s)") + + # NOTE: _handshake_seen may be updated from the handshake server thread. + # It is intentionally used here without additional locking because: + # - _handshake_seen only ever grows (stages are added but never removed), and + # - we only check membership and set inclusion relative to `expected`. + # Under these monotonic semantics and the CPython GIL, concurrent reads/writes + # are safe for this usage and cannot violate correctness: we may observe a + # slightly stale view, but the loop condition remains valid and eventually + # becomes true once all expected stages have handshaked or the timeout elapses. + while not expected.issubset(self._handshake_seen) and time.time() < deadline: + time.sleep(1.0) + + remaining_timeout = max(0, int(deadline - time.time())) + + if not expected.issubset(self._handshake_seen): + missing = sorted(expected - self._handshake_seen) + logger.warning( + f"[{self._name}] Handshake timeout: {len(self._handshake_seen)}/{len(expected)} " + f"stages completed handshake. Missing stages: {missing}" + ) + + return remaining_timeout + @property def _name(self) -> str: return "OmniBase" @@ -546,7 +758,12 @@ class Omni(OmniBase): _weak_close_cleanup, self.stage_list, self._stage_in_queues, + self._stage_out_queues, self._ray_pg, + self._zmq_ctx, + self._handshake_stop, + self._zmq_handshake_socket, + self._handshake_thread, ) @overload diff --git a/vllm_omni/entrypoints/omni_stage.py b/vllm_omni/entrypoints/omni_stage.py index 6c9723b6b..d90a805bd 100644 --- a/vllm_omni/entrypoints/omni_stage.py +++ b/vllm_omni/entrypoints/omni_stage.py @@ -50,6 +50,10 @@ from vllm_omni.entrypoints.stage_utils import ( set_stage_devices, ) from vllm_omni.entrypoints.utils import detect_pid_host +from vllm_omni.entrypoints.zmq_utils import ( + ZmqQueue, + create_zmq_queue, +) from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniPromptType, OmniSamplingParams, OmniTokensPrompt from vllm_omni.metrics import count_tokens_from_outputs from vllm_omni.outputs import OmniRequestOutput @@ -286,8 +290,8 @@ class OmniStage: except TypeError as error: raise TypeError(f"Invalid default_sampling_params for stage {self.stage_id}: {error}") from error # Runtime orchestration state (added) - self._in_q: mp.Queue | None = None - self._out_q: mp.Queue | None = None + self._in_q: mp.queues.Queue | ZmqQueue | str | None = None + self._out_q: mp.queues.Queue | ZmqQueue | str | None = None self._proc: mp.Process | None = None self._shm_threshold_bytes: int = 65536 self._stage_init_timeout: int = stage_init_timeout @@ -349,12 +353,16 @@ class OmniStage: self.engine_outputs = engine_outputs # ----------------- New Orchestration APIs ----------------- - def attach_queues(self, in_q: mp.Queue, out_q: mp.Queue) -> None: + def attach_queues( + self, + in_q: mp.queues.Queue | ZmqQueue | str | None, + out_q: mp.queues.Queue | ZmqQueue | str | None, + ) -> None: """Attach input and output queues for IPC communication. Args: - in_q: Input queue for receiving tasks from orchestrator - out_q: Output queue for sending results to orchestrator + in_q: Input queue for receiving tasks from orchestrator (queue object or endpoint string) + out_q: Output queue for sending results to orchestrator (queue object or endpoint string) """ self._in_q = in_q self._out_q = out_q @@ -401,6 +409,7 @@ class OmniStage: batch_timeout: int = 10, connectors_config: dict | None = None, worker_backend: str = "multi_process", + ignore_runtime_config: bool = False, **kwargs: Any, ) -> None: """Initialize and start the stage worker process. @@ -416,6 +425,7 @@ class OmniStage: batch_timeout: Timeout in seconds for batching requests connectors_config: Configuration for stage connectors worker_backend: Backend type ("multi_process" or "ray") + ignore_runtime_config: Whether to ignore runtime configuration (default: False) **kwargs: Additional arguments (e.g. ray_placement_group) Raises: @@ -433,7 +443,10 @@ class OmniStage: ctx = ctx or mp.get_context("spawn") # Prepare lightweight dict config for worker engine_args = _to_dict(self.engine_args) - runtime_cfg = _to_dict(getattr(self.stage_config, "runtime", {})) + if ignore_runtime_config: + runtime_cfg = {} + else: + runtime_cfg = _to_dict(getattr(self.stage_config, "runtime", {})) stage_payload: dict[str, Any] = { "stage_id": self.stage_id, "engine_args": engine_args, @@ -442,6 +455,8 @@ class OmniStage: "connectors_config": connectors_config or {}, "stage_type": self.stage_type, "engine_input_source": self.engine_input_source, + "final_output": self.final_output, + "final_output_type": self.final_output_type, } try: old_env = os.environ.get("VLLM_LOGGING_PREFIX") @@ -453,9 +468,10 @@ class OmniStage: _stage_worker_async_entry, ray_placement_group, self.stage_id, - self, model=model, stage_payload=stage_payload, + in_q=self._in_q, + out_q=self._out_q, batch_timeout=batch_timeout, stage_init_timeout=self._stage_init_timeout, ) @@ -476,9 +492,10 @@ class OmniStage: self._proc = ctx.Process( target=_stage_worker_async_entry, args=( - self, model, stage_payload, + self._in_q.endpoint if isinstance(self._in_q, ZmqQueue) else self._in_q, + self._out_q.endpoint if isinstance(self._out_q, ZmqQueue) else self._out_q, batch_timeout, self._stage_init_timeout, ), @@ -489,8 +506,8 @@ class OmniStage: args=( model, stage_payload, - self._in_q, - self._out_q, + self._in_q.endpoint if isinstance(self._in_q, ZmqQueue) else self._in_q, + self._out_q.endpoint if isinstance(self._out_q, ZmqQueue) else self._out_q, batch_timeout, self._stage_init_timeout, ), @@ -514,6 +531,13 @@ class OmniStage: self._in_q.put_nowait(SHUTDOWN_TASK) except Exception as e: logger.warning("Failed to send shutdown to in_q: %s", e) + close_fn = getattr(self._in_q, "close", None) + if callable(close_fn): + close_fn() + if self._out_q is not None: + close_fn = getattr(self._out_q, "close", None) + if callable(close_fn): + close_fn() if hasattr(self, "_ray_actor") and self._ray_actor: kill_ray_actor(self._ray_actor) @@ -636,8 +660,8 @@ class OmniStage: def _stage_worker( model: str, stage_payload: dict[str, Any], - in_q: mp.Queue, - out_q: mp.Queue, + in_q: mp.queues.Queue | ZmqQueue | str, + out_q: mp.queues.Queue | ZmqQueue | str, batch_timeout: int = 10, stage_init_timeout: int = 300, ) -> None: @@ -648,6 +672,8 @@ def _stage_worker( import os as _os import time as _time + import zmq + from vllm_omni.plugins import load_omni_general_plugins load_omni_general_plugins() @@ -674,6 +700,21 @@ def _stage_worker( if stage_type != "diffusion": _resolve_worker_cls(engine_args) + # Resolve ZMQ queue endpoints if needed + zmq_ctx = None + if isinstance(in_q, str) or isinstance(out_q, str): + zmq_ctx = zmq.Context() + if isinstance(in_q, str): + in_q = create_zmq_queue(zmq_ctx, in_q, zmq.PULL) + if isinstance(out_q, str): + out_q = create_zmq_queue(zmq_ctx, out_q, zmq.PUSH) + # When using ZMQ (cross-node IPC), disable SHM so data is sent inline. + shm_threshold_bytes = sys.maxsize + logger.info( + "[Stage-%s] ZMQ transport detected; disabling SHM IPC (shm_threshold_bytes set to maxsize)", + stage_id, + ) + # Aggregates for running average _agg_total_tokens = 0 _agg_total_gen_time_ms = 0.0 @@ -1008,19 +1049,21 @@ def _stage_worker( def _stage_worker_async_entry( - omni_stage: OmniStage, model: str, stage_payload: dict[str, Any], + in_q: mp.queues.Queue | ZmqQueue | str, + out_q: mp.queues.Queue | ZmqQueue | str, batch_timeout: int = 10, stage_init_timeout: int = 300, ) -> None: - asyncio.run(_stage_worker_async(omni_stage, model, stage_payload, batch_timeout, stage_init_timeout)) + asyncio.run(_stage_worker_async(model, stage_payload, in_q, out_q, batch_timeout, stage_init_timeout)) async def _stage_worker_async( - omni_stage: OmniStage, model: str, stage_payload: dict[str, Any], + in_q: mp.queues.Queue | ZmqQueue | str, + out_q: mp.queues.Queue | ZmqQueue | str, batch_timeout: int = 10, stage_init_timeout: int = 300, ) -> None: @@ -1030,6 +1073,8 @@ async def _stage_worker_async( import os as _os import time as _time + import zmq + from vllm_omni.plugins import load_omni_general_plugins load_omni_general_plugins() @@ -1049,12 +1094,26 @@ async def _stage_worker_async( shm_threshold_bytes = int(stage_payload.get("shm_threshold_bytes", 65536)) connectors_config = stage_payload.get("connectors_config", {}) stage_type = stage_payload.get("stage_type", "llm") + final_output = stage_payload.get("final_output", False) + final_output_type = stage_payload.get("final_output_type", None) if stage_type != "diffusion": _resolve_worker_cls(engine_args) - in_q = omni_stage._in_q - out_q = omni_stage._out_q + # Resolve ZMQ queue endpoints if needed + zmq_ctx = None + if isinstance(in_q, str) or isinstance(out_q, str): + zmq_ctx = zmq.Context() + if isinstance(in_q, str): + in_q = create_zmq_queue(zmq_ctx, in_q, zmq.PULL) + if isinstance(out_q, str): + out_q = create_zmq_queue(zmq_ctx, out_q, zmq.PUSH) + # When using ZMQ (cross-node IPC), disable SHM so data is sent inline. + shm_threshold_bytes = sys.maxsize + logger.info( + "[Stage-%s] ZMQ transport detected; disabling SHM IPC (shm_threshold_bytes set to maxsize)", + stage_id, + ) # Aggregates for running average _agg_total_tokens = 0 @@ -1130,14 +1189,13 @@ async def _stage_worker_async( engine_args.get("disable_log_stats", True) or getattr(omni_engine_args, "disable_log_stats", True) ), ) - omni_stage.set_async_engine(stage_engine) - if hasattr(omni_stage.async_engine, "log_stats") and omni_stage.async_engine.log_stats: + if hasattr(stage_engine, "log_stats") and stage_engine.log_stats: async def _force_log(): try: while True: await asyncio.sleep(10.0) - await omni_stage.async_engine.do_log_stats() + await stage_engine.do_log_stats() except asyncio.CancelledError: pass @@ -1334,7 +1392,7 @@ async def _stage_worker_async( batch_request_ids, batch_request_outputs, _gen_ms_list, batch_metrics ): try: - r_outputs = [output_strip(output, omni_stage)] + r_outputs = [output_strip(output, final_output, final_output_type)] use_shm, payload = maybe_dump_to_shm(r_outputs, shm_threshold_bytes) if use_shm: out_q.put( @@ -1420,7 +1478,7 @@ def make_stage_stats(_agg_total_tokens: int, _agg_total_gen_time_ms: float): return StageStats(total_token=_agg_total_tokens, total_gen_time_ms=_agg_total_gen_time_ms) -def output_strip(r_output: RequestOutput | OmniRequestOutput, omni_stage: OmniStage): +def output_strip(r_output: RequestOutput | OmniRequestOutput, final_output: bool, final_output_type: str | None): """ Strip unnecessary multimodal outputs from stages results, in order to: @@ -1429,7 +1487,7 @@ def output_strip(r_output: RequestOutput | OmniRequestOutput, omni_stage: OmniSt """ # check multimodal data is required by stage output config. - if omni_stage.final_output and omni_stage.final_output_type != "text": + if final_output and final_output_type != "text": return r_output # If the request has already finished, should not be altered. diff --git a/vllm_omni/entrypoints/utils.py b/vllm_omni/entrypoints/utils.py index 0abe029d6..6ee42a231 100644 --- a/vllm_omni/entrypoints/utils.py +++ b/vllm_omni/entrypoints/utils.py @@ -236,6 +236,40 @@ def load_stage_configs_from_yaml(config_path: str, base_engine_args: dict | None return stage_args +def load_and_resolve_stage_configs( + model: str, + stage_configs_path: str | None, + base_engine_args: dict | None, + default_stage_cfg_factory: Any = None, +) -> tuple[str, list]: + """Load stage configurations from model or YAML file with fallback to defaults. + + Args: + model: Model name or path + stage_configs_path: Optional path to YAML file containing stage configurations + base_engine_args: Base engine arguments to merge with stage configs + default_stage_cfg_factory: Optional callable that takes no args and returns + default stage config list when no configs are found + + Returns: + Tuple of (config_path, stage_configs) + """ + if stage_configs_path is None: + config_path = resolve_model_config_path(model) + stage_configs = load_stage_configs_from_model(model, base_engine_args=base_engine_args) + if not stage_configs: + if default_stage_cfg_factory is not None: + default_stage_cfg = default_stage_cfg_factory() + stage_configs = OmegaConf.create(default_stage_cfg) + else: + stage_configs = [] + else: + config_path = stage_configs_path + stage_configs = load_stage_configs_from_yaml(stage_configs_path, base_engine_args=base_engine_args) + + return config_path, stage_configs + + def get_final_stage_id_for_e2e( output_modalities: list[str] | None, default_modalities: list[str], stage_list: list ) -> int: @@ -282,6 +316,54 @@ def get_final_stage_id_for_e2e( return final_stage_id_for_e2e +# TODO(wuhang): Remove after PR #1115. +def build_base_engine_args(source: Any) -> dict[str, Any] | None: + """Build base engine args with tokenizer and parallel configuration. + + Automatically detects whether source is a dict-like object or namespace object. + + Args: + source: Source object (args namespace or kwargs dict) containing configuration. + + Returns: + Dictionary containing tokenizer and parallel configuration overrides, + or None if no configuration is present. + """ + # Auto-detect source type: dict-like objects have 'get' method + is_dict_like = hasattr(source, "get") and callable(getattr(source, "get")) + + # Extract tokenizer + if is_dict_like: + tokenizer = source.get("tokenizer", None) + else: + tokenizer = getattr(source, "tokenizer", None) + + base_engine_args = {"tokenizer": tokenizer} if tokenizer is not None else None + + # Extract parallel configuration + parallel_keys = [ + "tensor_parallel_size", + "pipeline_parallel_size", + "data_parallel_size", + "data_parallel_size_local", + "data_parallel_backend", + "distributed_executor_backend", + ] + + if is_dict_like: + parallel_overrides = {k: source[k] for k in parallel_keys if k in source and source[k] is not None} + else: + parallel_overrides = { + k: getattr(source, k) for k in parallel_keys if hasattr(source, k) and getattr(source, k) is not None + } + + if parallel_overrides: + base_engine_args = base_engine_args or {} + base_engine_args.update(parallel_overrides) + + return base_engine_args + + # The following code detects if the process is running in a container and if # PID host is available. If so, we can use process-scoped memory tracking; # otherwise we need sequential init locks. diff --git a/vllm_omni/entrypoints/zmq_utils.py b/vllm_omni/entrypoints/zmq_utils.py new file mode 100644 index 000000000..2ef5685cd --- /dev/null +++ b/vllm_omni/entrypoints/zmq_utils.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +"""ZMQ-based queue utilities for Omni IPC.""" + +from __future__ import annotations + +import queue +from typing import Any + +import zmq +from vllm.utils.network_utils import make_zmq_socket + + +class ZmqQueue: + """Queue-like wrapper on a ZMQ socket.""" + + def __init__( + self, + ctx: zmq.Context, + socket_type: int, + *, + bind: str | None = None, + connect: str | None = None, + recv_timeout_ms: int | None = None, + send_timeout_ms: int | None = None, + ) -> None: + # Determine path and bind mode + path = bind if bind is not None else connect + if path is None: + raise ValueError("Either bind or connect must be specified") + bind_mode = bind is not None + + self._socket = make_zmq_socket(ctx, path, socket_type, bind=bind_mode, linger=5000) + + # Reusable poller for efficient polling operations + self._poller = zmq.Poller() + self._poller.register(self._socket, zmq.POLLIN) + + # Store default timeout settings + self._default_recv_timeout = recv_timeout_ms + self._default_send_timeout = send_timeout_ms + + # Apply timeout settings if specified + if recv_timeout_ms is not None: + self._socket.rcvtimeo = recv_timeout_ms + if send_timeout_ms is not None: + self._socket.sndtimeo = send_timeout_ms + + self.endpoint = path + + def put(self, obj: Any) -> None: + """Send an object to the queue. Blocks until sent or timeout.""" + try: + self._socket.send_pyobj(obj) + except zmq.Again as e: + raise queue.Full() from e + + def put_nowait(self, obj: Any) -> None: + """Send an object to the queue without blocking.""" + try: + self._socket.send_pyobj(obj, flags=zmq.NOBLOCK) + except zmq.Again as e: + raise queue.Full() from e + + def get(self, timeout: float | None = None) -> Any: + """Receive an object from the queue with optional timeout in seconds.""" + if timeout is None: + return self._socket.recv_pyobj() + + # Use the reusable poller for timeout handling + events = dict(self._poller.poll(int(timeout * 1000))) + if events.get(self._socket) == zmq.POLLIN: + return self._socket.recv_pyobj() + raise queue.Empty() + + def get_nowait(self) -> Any: + """Receive an object from the queue without blocking.""" + try: + return self._socket.recv_pyobj(flags=zmq.NOBLOCK) + except zmq.Again as e: + raise queue.Empty() from e + + def empty(self) -> bool: + """Check if the queue is empty without blocking.""" + events = dict(self._poller.poll(0)) + return events.get(self._socket) != zmq.POLLIN + + def close(self) -> None: + self._socket.close(0) + + +def create_zmq_queue(ctx: zmq.Context, endpoint: str, socket_type: int) -> ZmqQueue: + """Create a ZmqQueue from an endpoint string and socket type.""" + return ZmqQueue(ctx, socket_type, connect=endpoint) From a8cec43ce6fbe91cbfc4eab4fe0e688358bcbad1 Mon Sep 17 00:00:00 2001 From: Junhong Liu Date: Tue, 24 Feb 2026 20:27:29 +0800 Subject: [PATCH 04/10] [Doc] Optimize vLLM-Omni metrics documentation (#1311) Signed-off-by: Junhong Liu <98734602+LJH-LBJ@users.noreply.github.com> Signed-off-by: Junhong Liu Co-authored-by: Hongsheng Liu --- docs/contributing/metrics.md | 44 +++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/docs/contributing/metrics.md b/docs/contributing/metrics.md index 7a46a2861..92dcd92cc 100644 --- a/docs/contributing/metrics.md +++ b/docs/contributing/metrics.md @@ -1,25 +1,27 @@ -# Metrics vLLM-Omni: +# Metrics You can use these metrics in production to monitor the health and performance of the vLLM-omni system. Typical scenarios include: + - **Performance Monitoring**: Track throughput (e.g., `e2e_avg_tokens_per_s`), latency (e.g., `e2e_total_ms`), and resource utilization to verify that the system meets expected standards. + - **Debugging and Troubleshooting**: Use detailed per-request metrics to diagnose issues, such as high transfer times or unexpected token counts. ## How to Enable and View Metrics -### 1. Start the Service with Metrics Logging +### Start the Service with Metrics Logging ```bash vllm serve /workspace/models/Qwen3-Omni-30B-A3B-Instruct --omni --port 8014 --log-stats ``` -### 2. Send a Request +### Send a Request ```bash python openai_chat_completion_client_for_multimodal_generation.py --query-type use_image ``` -### 3. What You Will See +### What You Will See With `--log-stats` enabled, the server will output detailed metrics logs after each request. Example output: @@ -69,9 +71,13 @@ With `--log-stats` enabled, the server will output detailed metrics logs after e These logs include: + - **Overall summary**: total requests, wall time, average tokens/sec, etc. + - **E2E table**: per-request latency and token counts. + - **Stage table**: per-stage batch and timing details. + - **Transfer table**: data transfer and timing for each edge. You can use these logs to monitor system health, debug performance, and analyze request-level metrics as described above. @@ -87,6 +93,8 @@ For **online inference** (serving mode), the summary is always per-request. `e2e ## Parameter Details +### Summary Metrics + | Field | Meaning | |---------------------------|----------------------------------------------------------------------------------------------| | `e2e_requests` | Number of completed requests. | @@ -98,7 +106,7 @@ For **online inference** (serving mode), the summary is always per-request. `e2e --- -## E2E Table (per request) +### E2E Table (per request) | Field | Meaning | |---------------------------|-----------------------------------------------------------------------| @@ -110,7 +118,7 @@ For **online inference** (serving mode), the summary is always per-request. `e2e --- -## Stage Table (per stage event / request) +### Stage Table (per stage event / request) | Field | Meaning | |---------------------------|-------------------------------------------------------------------------------------------------| @@ -125,7 +133,7 @@ For **online inference** (serving mode), the summary is always per-request. `e2e --- -## Transfer Table (per edge / request) +### Transfer Table (per edge / request) | Field | Meaning | |----------------------|---------------------------------------------------------------------------| @@ -135,31 +143,31 @@ For **online inference** (serving mode), the summary is always per-request. `e2e | `in_flight_time_ms` | In-flight time in ms. | -## Expectation of the Numbers (Verification) +### Expectation of the Numbers (Verification) **Formulas:** + - `e2e_total_tokens = Stage0's num_tokens_in + sum(all stages' num_tokens_out)` + - `transfers_total_time_ms = sum(tx_time_ms + rx_decode_time_ms + in_flight_time_ms)` for every edge **Using the example above:** -### e2e_total_tokens +**e2e_total_tokens** + - Stage0's `num_tokens_in`: **4,860** - Stage0's `num_tokens_out`: **67** - Stage1's `num_tokens_out`: **275** - Stage2's `num_tokens_out`: **0** -So, -``` -e2e_total_tokens = 4,860 + 67 + 275 + 0 = 5,202 -``` -This matches the table value: `e2e_total_tokens = 5,202`. +so `e2e_total_tokens = 4,860 + 67 + 275 + 0 = 5,202`, which matches the table value `e2e_total_tokens`. + +**transfers_total_time_ms** -### transfers_total_time_ms For each edge: + - 0->1: tx_time_ms (**78.701**) + rx_decode_time_ms (**111.865**) + in_flight_time_ms (**2.015**) = **192.581** + - 1->2: tx_time_ms (**18.790**) + rx_decode_time_ms (**31.706**) + in_flight_time_ms (**2.819**) = **53.315** -Sum: 192.581 + 53.315 = **245.896** - -The table shows `transfers_total_time_ms = 245.895`, which matches the calculation (difference is due to rounding). +192.581 + 53.315 = **245.896** = transfers_total_time_ms, which matches the calculation (difference is due to rounding) From c0908dd3fa6824cc3829a24dac9c29a282ea32e8 Mon Sep 17 00:00:00 2001 From: Junhong Liu Date: Tue, 24 Feb 2026 22:20:01 +0800 Subject: [PATCH 05/10] [Bugfix] Forward all vllm-omni serve command parameters to model (#985) Signed-off-by: Junhong Liu <98734602+LJH-LBJ@users.noreply.github.com> Signed-off-by: Junhong Liu Co-authored-by: Hongsheng Liu --- docs/cli/serve.md | 9 + docs/mkdocs/hooks/generate_argparse.py | 257 +++++++++++++++++++++++ mkdocs.yml | 1 + tests/entrypoints/test_utils.py | 271 +++++++++++++++++++++++++ vllm_omni/entrypoints/omni.py | 4 +- vllm_omni/entrypoints/omni_llm.py | 3 +- vllm_omni/entrypoints/omni_stage.py | 19 +- vllm_omni/entrypoints/utils.py | 135 +++++++++++- 8 files changed, 682 insertions(+), 17 deletions(-) create mode 100644 docs/cli/serve.md create mode 100644 docs/mkdocs/hooks/generate_argparse.py create mode 100644 tests/entrypoints/test_utils.py diff --git a/docs/cli/serve.md b/docs/cli/serve.md new file mode 100644 index 000000000..47a873b72 --- /dev/null +++ b/docs/cli/serve.md @@ -0,0 +1,9 @@ +# vllm-omni serve + +## JSON CLI Arguments + +--8<-- "docs/cli/json_tip.inc.md" + +## Arguments + +--8<-- "docs/generated/argparse_omni/omni_serve.inc.md" diff --git a/docs/mkdocs/hooks/generate_argparse.py b/docs/mkdocs/hooks/generate_argparse.py new file mode 100644 index 000000000..b4eb44129 --- /dev/null +++ b/docs/mkdocs/hooks/generate_argparse.py @@ -0,0 +1,257 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +import ast +import logging +import re +import sys +from argparse import SUPPRESS, Action, ArgumentParser, HelpFormatter, _ArgumentGroup +from collections.abc import Iterable +from importlib.machinery import ModuleSpec +from pathlib import Path +from typing import Literal +from unittest.mock import MagicMock + +from pydantic_core import core_schema + + +class _FlexibleArgumentParser(ArgumentParser): + """Fallback parser for docs when vllm is unavailable. + + Accepts the 'deprecated' kwarg used by vllm CLI and emits warnings + if a deprecated argument is actually provided. + """ + + _deprecated: set[Action] = set() + _deprecated_warned: set[str] = set() + + if sys.version_info < (3, 13): + + def parse_known_args(self, args=None, namespace=None): + namespace, args = super().parse_known_args(args, namespace) + for action in _FlexibleArgumentParser._deprecated: + if ( + hasattr(namespace, dest := action.dest) + and getattr(namespace, dest) != action.default + and dest not in _FlexibleArgumentParser._deprecated_warned + ): + _FlexibleArgumentParser._deprecated_warned.add(dest) + logger.warning("argument '%s' is deprecated", dest) + return namespace, args + + def add_argument(self, *args, **kwargs): + deprecated = kwargs.pop("deprecated", False) + action = super().add_argument(*args, **kwargs) + if deprecated: + _FlexibleArgumentParser._deprecated.add(action) + return action + + class _FlexibleArgumentGroup(_ArgumentGroup): + def add_argument(self, *args, **kwargs): + deprecated = kwargs.pop("deprecated", False) + action = super().add_argument(*args, **kwargs) + if deprecated: + _FlexibleArgumentParser._deprecated.add(action) + return action + + def add_argument_group(self, *args, **kwargs): + group = self._FlexibleArgumentGroup(self, *args, **kwargs) + self._action_groups.append(group) + return group + + +logger = logging.getLogger("mkdocs") + +# Define root and doc output directories +ROOT_DIR = Path(__file__).parent.parent.parent.parent +ARGPARSE_DOC_DIR = ROOT_DIR / "docs/generated/argparse_omni" + +# Ensure the repo root is in sys.path for dynamic imports +sys.path.insert(0, str(ROOT_DIR)) + + +class PydanticMagicMock(MagicMock): + """`MagicMock` that's able to generate pydantic-core schemas and avoids _mock_name=None errors.""" + + def __init__(self, *args, **kwargs): + name = kwargs.pop("name", None) + super().__init__(*args, **kwargs) + self.__spec__ = ModuleSpec(name, None) + self._mock_name = name or "mock" + + def __get_pydantic_core_schema__(self, source_type, handler): + return core_schema.any_schema() + + +# --- Static extraction for CLI argument docs --- + + +def extract_omni_serve_subparser_init(): + """ + Statically parse vllm_omni/entrypoints/cli/serve.py to extract the subparser_init method + and return a callable that adds arguments to a parser. This avoids import and mock issues. + """ + serve_path = ROOT_DIR / "vllm_omni" / "entrypoints" / "cli" / "serve.py" + with open(serve_path, encoding="utf-8") as f: + source = f.read() + tree = ast.parse(source, filename=str(serve_path)) + # Find class OmniServeCommand + for node in tree.body: + if isinstance(node, ast.ClassDef) and node.name == "OmniServeCommand": + for item in node.body: + if isinstance(item, ast.FunctionDef) and item.name == "subparser_init": + # We'll exec this function body in a dummy context + func_code = ast.Module(body=[item], type_ignores=[]) + code = compile(func_code, filename=str(serve_path), mode="exec") + # Prepare dummy context + local_vars = {} + + # Provide a dummy subparsers with add_parser returning our parser + class DummySubparsers: + def add_parser(self, name, **kwargs): + return _FlexibleArgumentParser(prog=name) + + dummy_subparsers = DummySubparsers() + # Provide globals for exec + # Extract DESCRIPTION from the source file (assume it's a triple-quoted string at the top) + m = re.search(r'DESCRIPTION\s*=\s*([ru]?""".*?""")', source, re.DOTALL) + DESCRIPTION = m.group(1)[3:-3] if m else "" + exec_globals = { + "_FlexibleArgumentParser": _FlexibleArgumentParser, + "FlexibleArgumentParser": _FlexibleArgumentParser, + "make_arg_parser": lambda parser: parser, # no-op for doc + "VLLM_SUBCMD_PARSER_EPILOG": "", + "logger": logger, + "DummySubparsers": DummySubparsers, + "argparse": __import__("argparse"), + "DESCRIPTION": DESCRIPTION, + } + exec(code, exec_globals, local_vars) + # Get the function + subparser_init = local_vars["subparser_init"] + + # Return a callable that mimics OmniServeCommand().subparser_init + def parser_factory(): + class DummySelf: + name = "serve" + + return subparser_init(DummySelf(), dummy_subparsers) + + return parser_factory + raise RuntimeError("Could not statically extract OmniServeCommand.subparser_init") + + +OmniServeCommand = type("OmniServeCommand", (), {"subparser_init": staticmethod(extract_omni_serve_subparser_init())}) + + +class MarkdownFormatter(HelpFormatter): + """Custom formatter that generates markdown for argument groups.""" + + def __init__(self, prog: str, starting_heading_level: int = 3): + super().__init__(prog, max_help_position=sys.maxsize, width=sys.maxsize) + + self._section_heading_prefix = "#" * starting_heading_level + self._argument_heading_prefix = "#" * (starting_heading_level + 1) + self._markdown_output = [] + + def start_section(self, heading: str): + if heading not in {"positional arguments", "options"}: + heading_md = f"\n{self._section_heading_prefix} {heading}\n\n" + self._markdown_output.append(heading_md) + + def end_section(self): + pass + + def add_text(self, text: str): + if text: + self._markdown_output.append(f"{text.strip()}\n\n") + + def add_usage(self, usage, actions, groups, prefix=None): + pass + + def add_arguments(self, actions: Iterable[Action]): + for action in actions: + if len(action.option_strings) == 0 or "--help" in action.option_strings: + continue + + option_strings = f"`{'`, `'.join(action.option_strings)}`" + heading_md = f"{self._argument_heading_prefix} {option_strings}\n\n" + self._markdown_output.append(heading_md) + + if choices := action.choices: + choices = f"`{'`, `'.join(str(c) for c in choices)}`" + self._markdown_output.append(f"Possible choices: {choices}\n\n") + elif (metavar := action.metavar) and isinstance(metavar, (list, tuple)): + metavar = f"`{'`, `'.join(str(m) for m in metavar)}`" + self._markdown_output.append(f"Possible choices: {metavar}\n\n") + + if action.help: + self._markdown_output.append(f"{action.help}\n\n") + + if (default := action.default) != SUPPRESS: + # Make empty string defaults visible + if default == "": + default = '""' + self._markdown_output.append(f"Default: `{default}`\n\n") + + def format_help(self): + """Return the formatted help as markdown.""" + return "".join(self._markdown_output) + + +# Function to create parser using subparser_init style CLI class + + +def create_parser_subparser_init(subcmd_class): + """ + Create an argparse parser using subparser_init style CLI class, with MarkdownFormatter. + Compatible with both AST-extracted static parser_factory and real class instance method. + """ + + class DummySubparsers: + def add_parser(self, name, **kwargs): + return _FlexibleArgumentParser(prog=name) + + dummy_subparsers = DummySubparsers() + # If subparser_init is a zero-arg static function (AST-extracted), call it directly + subparser_init = getattr(subcmd_class, "subparser_init", None) + if subparser_init is not None: + import inspect + + sig = inspect.signature(subparser_init) + if len(sig.parameters) == 0: + parser = subparser_init() + else: + parser = subcmd_class().subparser_init(dummy_subparsers) + else: + parser = subcmd_class().subparser_init(dummy_subparsers) + parser.formatter_class = MarkdownFormatter + return parser + + +def on_startup(command: Literal["build", "gh-deploy", "serve"], dirty: bool): + """ + Entry point for doc generation. Builds doc directory and outputs markdown for each CLI command. + """ + logger.info("Generating vllm-omni argparse documentation") + logger.debug("Root directory: %s", ROOT_DIR.resolve()) + logger.debug("Output directory: %s", ARGPARSE_DOC_DIR.resolve()) + if not ARGPARSE_DOC_DIR.exists(): + ARGPARSE_DOC_DIR.mkdir(parents=True) + + # Register all CLI parsers; you can easily add more commands here + parsers = { + "omni_serve": create_parser_subparser_init(OmniServeCommand), + # "another_cmd": create_parser_subparser_init(AnotherCommandClass), + } + + for stem, parser in parsers.items(): + doc_path = ARGPARSE_DOC_DIR / f"{stem}.inc.md" + with open(doc_path, "w", encoding="utf-8") as f: + f.write(super(type(parser), parser).format_help()) + logger.info("Argparse generated: %s", doc_path.relative_to(ROOT_DIR)) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + on_startup("build", False) diff --git a/mkdocs.yml b/mkdocs.yml index 4f554003a..edecec07a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -59,6 +59,7 @@ hooks: - docs/mkdocs/hooks/generate_api_readme.py - docs/mkdocs/hooks/url_schemes.py - docs/mkdocs/hooks/generate_examples.py + - docs/mkdocs/hooks/generate_argparse.py # Exclude include files from navigation warnings exclude_docs: | diff --git a/tests/entrypoints/test_utils.py b/tests/entrypoints/test_utils.py new file mode 100644 index 000000000..e18db40c1 --- /dev/null +++ b/tests/entrypoints/test_utils.py @@ -0,0 +1,271 @@ +"""Unit tests for vllm_omni.entrypoints.utils module.""" + +from collections import Counter +from dataclasses import dataclass +from unittest.mock import patch + +import pytest + +from vllm_omni.diffusion.data import OmniDiffusionConfig +from vllm_omni.engine.arg_utils import OmniEngineArgs +from vllm_omni.entrypoints.utils import ( + _convert_dataclasses_to_dict, + _filter_dict_like_object, + filter_dataclass_kwargs, +) + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + + +class TestFilterDictLikeObject: + """Test suite for _filter_dict_like_object function.""" + + def test_simple_dict(self): + """Test filtering a simple dictionary with no callables.""" + input_dict = {"key1": "value1", "key2": 42, "key3": [1, 2, 3]} + result = _filter_dict_like_object(input_dict) + + assert result == input_dict + assert isinstance(result, dict) + + def test_dict_with_nested_values(self): + """Test filtering dict with nested dict and list values.""" + input_dict = { + "level1": { + "level2": {"key": "value"}, + "list": [1, 2, 3], + }, + "simple": "string", + } + + result = _filter_dict_like_object(input_dict) + + # Nested dicts and lists should be recursively processed + assert result["simple"] == "string" + assert isinstance(result["level1"], dict) + + def test_dict_with_dataclass_values(self): + """Test filtering dict containing dataclass values.""" + + @dataclass + class TestDataclass: + field1: str + field2: int + + obj = TestDataclass(field1="test", field2=42) + input_dict = {"data": obj, "normal": "value"} + + result = _filter_dict_like_object(input_dict) + + # Dataclass should be converted to dict by recursive _convert_dataclasses_to_dict + assert "data" in result + assert "normal" in result + assert result["normal"] == "value" + + def test_dict_with_counter_values(self): + """Test filtering dict containing Counter objects.""" + counter_obj = Counter({"a": 1, "b": 2}) + input_dict = {"counter": counter_obj, "normal": "value"} + + result = _filter_dict_like_object(input_dict) + + # Counter should be converted to regular dict + assert "counter" in result + assert "normal" in result + assert result["normal"] == "value" + + def test_empty_dict(self): + """Test filtering an empty dictionary.""" + result = _filter_dict_like_object({}) + assert result == {} + assert isinstance(result, dict) + + def test_dict_with_set_values(self): + """Test filtering dict with set values.""" + input_dict = {"set_key": {1, 2, 3}, "normal": "value"} + + result = _filter_dict_like_object(input_dict) + + assert "set_key" in result + assert "normal" in result + # Set should be converted to list by _convert_dataclasses_to_dict + assert result["normal"] == "value" + + def test_dict_with_none_values(self): + """Test filtering dict with None values.""" + input_dict = {"key1": None, "key2": "value", "key3": 0} + + result = _filter_dict_like_object(input_dict) + + assert result == input_dict + + def test_dict_with_mixed_types(self): + """Test filtering dict with mixed value types.""" + input_dict = { + "string": "hello", + "int": 42, + "float": 3.14, + "bool": True, + "none": None, + "list": [1, 2, 3], + "tuple": (1, 2, 3), + "set": {1, 2, 3}, + "dict": {"nested": "value"}, + } + + result = _filter_dict_like_object(input_dict) + + assert "string" in result + assert "int" in result + assert "float" in result + assert "bool" in result + assert "none" in result + assert "list" in result + assert "tuple" in result + assert "set" in result + assert "dict" in result + + def test_dict_preserves_key_types(self): + """Test that dict key types are preserved.""" + input_dict = { + "string_key": "value1", + 42: "value2", + (1, 2): "value3", # tuple as key + } + + result = _filter_dict_like_object(input_dict) + + # Keys should remain the same + assert "string_key" in result + assert 42 in result + assert (1, 2) in result + + def test_dict_with_recursive_structure(self): + """Test filtering dict with recursive/complex nested structure.""" + input_dict = { + "level1": { + "level2": { + "level3": {"key": "value"}, + "callable": lambda x: x, + } + }, + "normal": "value", + } + + with patch("vllm_omni.entrypoints.utils.logger"): + result = _filter_dict_like_object(input_dict) + + # Normal key should exist + assert "normal" in result + # Level1 should exist + assert "level1" in result + + def test_integration_with_convert_dataclasses(self): + """Test that _filter_dict_like_object integrates properly with _convert_dataclasses_to_dict.""" + + @dataclass + class Config: + name: str + count: int + + input_dict = { + "config": Config(name="test", count=5), + "func": lambda x: x, + "normal": "value", + } + + with patch("vllm_omni.entrypoints.utils.logger"): + result = _filter_dict_like_object(input_dict) + + # Callable should be filtered + assert "func" not in result + # Config should be converted to dict + assert "config" in result + assert "normal" in result + + +class TestConvertDataclassesToDict: + """Test suite for _convert_dataclasses_to_dict function.""" + + def test_uses_filter_dict_like_object(self): + """Test that _convert_dataclasses_to_dict uses _filter_dict_like_object for dicts.""" + input_dict = { + "normal": "value", + "callable": lambda x: x, + } + + with patch("vllm_omni.entrypoints.utils.logger"): + result = _convert_dataclasses_to_dict(input_dict) + + # Callable should be filtered out by _filter_dict_like_object + assert "normal" in result + assert "callable" not in result + + +class TestFilterDataclassKwargs: + """Test basic functionality of filter_dataclass_kwargs.""" + + def test_simple_filtering(self): + """Test basic dataclass kwargs filtering.""" + + @dataclass + class SimpleConfig: + name: str + count: int + + kwargs = {"name": "test", "count": 42, "invalid": "should_be_removed"} + result = filter_dataclass_kwargs(SimpleConfig, kwargs) + + assert "name" in result + assert "count" in result + assert "invalid" not in result + + def test_invalid_dataclass_raises_error(self): + """Test that non-dataclass raises ValueError.""" + with pytest.raises(ValueError, match="is not a dataclass"): + filter_dataclass_kwargs(dict, {}) + + def test_invalid_kwargs_type_raises_error(self): + """Test that non-dict kwargs raises ValueError.""" + + @dataclass + class SimpleConfig: + name: str + + with pytest.raises(ValueError, match="kwargs must be a dictionary"): + filter_dataclass_kwargs(SimpleConfig, "invalid") + + def test_filters_omni_engine_args_unknown_fields(self): + """Test that OmniEngineArgs kwargs are filtered to valid fields only.""" + kwargs = { + "model": "dummy", + "stage_id": 1, + "engine_output_type": "image", + "unknown_field": "drop_me", + } + + result = filter_dataclass_kwargs(OmniEngineArgs, kwargs) + + assert "model" in result + assert "stage_id" in result + assert "engine_output_type" in result + assert "unknown_field" not in result + + def test_filters_omni_diffusion_config_union_dataclass(self): + """Test that OmniDiffusionConfig filters nested dataclass in Union fields.""" + kwargs = { + "model": "dummy", + "cache_config": { + "rel_l1_thresh": 0.3, + "extra_param": "should_drop", + }, + "unknown_top": "drop_me", + } + + result = filter_dataclass_kwargs(OmniDiffusionConfig, kwargs) + + assert "model" in result + assert "cache_config" in result + assert "unknown_top" not in result + assert result["cache_config"]["rel_l1_thresh"] == 0.3 + assert "extra_param" not in result["cache_config"] diff --git a/vllm_omni/entrypoints/omni.py b/vllm_omni/entrypoints/omni.py index 051b2acf0..94409b3e6 100644 --- a/vllm_omni/entrypoints/omni.py +++ b/vllm_omni/entrypoints/omni.py @@ -38,7 +38,6 @@ from vllm_omni.entrypoints.omni_stage import OmniStage from vllm_omni.entrypoints.stage_utils import SHUTDOWN_TASK, OmniStageTaskType from vllm_omni.entrypoints.stage_utils import maybe_load_from_ipc as _load from vllm_omni.entrypoints.utils import ( - build_base_engine_args, get_final_stage_id_for_e2e, inject_omni_kv_config, load_and_resolve_stage_configs, @@ -255,7 +254,6 @@ class OmniBase: # TODO(wuhang): # Remove kwargs as parameters in the future. # Use dataclass directly for engine args. - base_engine_args = build_base_engine_args(kwargs) stage_configs_path = kwargs.get("stage_configs_path", None) @@ -263,7 +261,7 @@ class OmniBase: config_path, stage_configs = load_and_resolve_stage_configs( model, stage_configs_path, - base_engine_args, + kwargs, default_stage_cfg_factory=lambda: self._create_default_diffusion_stage_cfg(kwargs), ) diff --git a/vllm_omni/entrypoints/omni_llm.py b/vllm_omni/entrypoints/omni_llm.py index 118a0ba7f..ec9248e30 100644 --- a/vllm_omni/entrypoints/omni_llm.py +++ b/vllm_omni/entrypoints/omni_llm.py @@ -22,6 +22,7 @@ from vllm_omni.engine.arg_utils import OmniEngineArgs from vllm_omni.engine.input_processor import OmniInputProcessor from vllm_omni.engine.output_processor import MultimodalOutputProcessor from vllm_omni.entrypoints.utils import ( + filter_dataclass_kwargs, load_stage_configs_from_model, load_stage_configs_from_yaml, resolve_model_config_path, @@ -151,7 +152,7 @@ class OmniLLM(LLM): compilation_config=compilation_config_instance, structured_outputs_config=structured_outputs_instance, omni_kv_config=omni_kv_config, - **kwargs, + **filter_dataclass_kwargs(OmniEngineArgs, kwargs), ) # Create the Engine (autoselects V0 vs V1) diff --git a/vllm_omni/entrypoints/omni_stage.py b/vllm_omni/entrypoints/omni_stage.py index d90a805bd..b26e4c0bc 100644 --- a/vllm_omni/entrypoints/omni_stage.py +++ b/vllm_omni/entrypoints/omni_stage.py @@ -36,7 +36,7 @@ from vllm_omni.distributed.omni_connectors import build_stage_connectors from vllm_omni.distributed.omni_connectors.adapter import try_recv_via_connector from vllm_omni.distributed.omni_connectors.connectors.base import OmniConnectorBase from vllm_omni.distributed.ray_utils.utils import kill_ray_actor, start_ray_actor -from vllm_omni.engine.arg_utils import AsyncOmniEngineArgs +from vllm_omni.engine.arg_utils import AsyncOmniEngineArgs, OmniEngineArgs from vllm_omni.entrypoints.async_omni_diffusion import AsyncOmniDiffusion from vllm_omni.entrypoints.async_omni_llm import AsyncOmniLLM from vllm_omni.entrypoints.omni_diffusion import OmniDiffusion @@ -49,7 +49,7 @@ from vllm_omni.entrypoints.stage_utils import ( maybe_dump_to_shm, set_stage_devices, ) -from vllm_omni.entrypoints.utils import detect_pid_host +from vllm_omni.entrypoints.utils import detect_pid_host, filter_dataclass_kwargs from vllm_omni.entrypoints.zmq_utils import ( ZmqQueue, create_zmq_queue, @@ -212,7 +212,8 @@ def _resolve_worker_cls(engine_args: dict[str, Any]) -> None: worker_type = engine_args.get("worker_type", None) if not worker_type: return - if engine_args.get("worker_cls"): + worker_cls = engine_args.get("worker_cls") + if worker_cls is not None and worker_cls != "auto": return from vllm_omni.platforms import current_omni_platform @@ -268,6 +269,12 @@ class OmniStage: self.is_comprehension = getattr(stage_config, "is_comprehension", False) # Support for different stage types: "llm" (default) or "diffusion" self.stage_type: Literal["llm", "diffusion"] = getattr(stage_config, "stage_type", "llm") + if ( + "stage_id" in stage_config.engine_args + and stage_config.engine_args.stage_id != self.stage_id + and self.stage_id is not None + ): + stage_config.engine_args.stage_id = self.stage_id if hasattr(stage_config, "custom_process_input_func"): # Import the module specified in the config (already a full module path) module_path, func_name = stage_config.custom_process_input_func.rsplit(".", 1) @@ -746,6 +753,7 @@ def _stage_worker( engine_args["stage_connector_spec"] = stage_connector_spec engine_args["stage_id"] = stage_id if stage_type == "diffusion": + engine_args = filter_dataclass_kwargs(OmniDiffusionConfig, engine_args) engine_args.pop("model_stage", None) engine_args.pop("model", None) stage_engine = OmniDiffusion( @@ -755,6 +763,8 @@ def _stage_worker( **engine_args, ) else: + engine_args = filter_dataclass_kwargs(OmniEngineArgs, engine_args) + engine_args.pop("model", None) # Default to LLM engine stage_engine = OmniLLM(model=model, **engine_args) @@ -1162,6 +1172,7 @@ async def _stage_worker_async( engine_args["stage_id"] = stage_id if stage_type == "diffusion": # For diffusion, we need to extract diffusion-specific config + engine_args = filter_dataclass_kwargs(OmniDiffusionConfig, engine_args) od_config = _build_od_config(engine_args, model) # Inject omni config for worker to access stage info @@ -1178,6 +1189,8 @@ async def _stage_worker_async( ) vllm_config = None # Diffusion doesn't use vllm_config else: + engine_args = filter_dataclass_kwargs(AsyncOmniEngineArgs, engine_args) + engine_args.pop("model", None) omni_engine_args = AsyncOmniEngineArgs(model=model, **engine_args) usage_context = UsageContext.OPENAI_API_SERVER vllm_config = omni_engine_args.create_engine_config(usage_context=usage_context) diff --git a/vllm_omni/entrypoints/utils.py b/vllm_omni/entrypoints/utils.py index 6ee42a231..69ce73fc4 100644 --- a/vllm_omni/entrypoints/utils.py +++ b/vllm_omni/entrypoints/utils.py @@ -1,8 +1,9 @@ import os +import types from collections import Counter -from dataclasses import asdict, is_dataclass +from dataclasses import asdict, fields, is_dataclass from pathlib import Path -from typing import Any +from typing import Any, get_args, get_origin from omegaconf import OmegaConf from vllm.logger import init_logger @@ -68,6 +69,52 @@ def _try_get_class_name_from_diffusers_config(model: str) -> str | None: return None +def _filter_dict_like_object(obj: dict | Any) -> dict: + """Filter dict-like object by removing callables and recursively converting values. + + Converts dict-like objects to regular dicts while filtering out callable values + that are incompatible with OmegaConf. Recursively processes values through + _convert_dataclasses_to_dict for nested object conversion. + + Args: + obj: Dict or dict-like object to filter + + Returns: + Regular dict with callables filtered out and values recursively converted + + Raises: + TypeError: If obj doesn't support .items() method + ValueError: If dict conversion fails unexpectedly + """ + + def _is_callable_value(value: Any) -> bool: + if callable(value): + return True + return isinstance( + value, + ( + types.FunctionType, + types.MethodType, + types.BuiltinFunctionType, + types.BuiltinMethodType, + ), + ) + + result = {} + filtered_keys = [] + for k, v in obj.items(): + if _is_callable_value(v): + filtered_keys.append(str(k)) + else: + result[k] = _convert_dataclasses_to_dict(v) + if filtered_keys: + logger.warning( + f"Filtered out {len(filtered_keys)} callable object(s) from base_engine_args " + f"that are not compatible with OmegaConf: {filtered_keys}. " + ) + return result + + def _convert_dataclasses_to_dict(obj: Any) -> Any: """Recursively convert non-serializable objects to OmegaConf-compatible types. @@ -75,6 +122,7 @@ def _convert_dataclasses_to_dict(obj: Any) -> Any: - Dataclass objects with Literal type annotations (e.g., StructuredOutputsConfig) - Counter objects (from collections or vllm.utils) - Set objects + - Callable objects (functions, methods, etc.) - Other non-primitive types """ # IMPORTANT: Check Counter BEFORE dict, since Counter is a subclass of dict @@ -99,17 +147,21 @@ def _convert_dataclasses_to_dict(obj: Any) -> Any: result = asdict(obj) # Recursively process the result to convert any Counter objects return _convert_dataclasses_to_dict(result) - # Handle dictionaries (recurse into values) + # Handle dictionaries (recurse into values) and filter out callables(cause error in OmegaConf.create) # Note: This must come AFTER Counter check since Counter is a dict subclass if isinstance(obj, dict): - return {k: _convert_dataclasses_to_dict(v) for k, v in obj.items()} + return _filter_dict_like_object(obj) + # Handle callable objects (functions, methods, etc.) - skip them + # Note: This comes after dict/list checks to avoid misclassifying dict-like objects + if callable(obj): + return None # Handle lists and tuples (recurse into items) if isinstance(obj, (list, tuple)): - return type(obj)(_convert_dataclasses_to_dict(item) for item in obj) + return type(obj)(_convert_dataclasses_to_dict(item) for item in obj if not callable(item)) # Try to convert any dict-like object (has keys/values methods) to dict if hasattr(obj, "keys") and hasattr(obj, "values") and not isinstance(obj, (str, bytes)): try: - return {k: _convert_dataclasses_to_dict(v) for k, v in obj.items()} + return _filter_dict_like_object(obj) except (TypeError, ValueError, AttributeError): # If conversion fails, return as-is return obj @@ -239,7 +291,7 @@ def load_stage_configs_from_yaml(config_path: str, base_engine_args: dict | None def load_and_resolve_stage_configs( model: str, stage_configs_path: str | None, - base_engine_args: dict | None, + kwargs: dict | None, default_stage_cfg_factory: Any = None, ) -> tuple[str, list]: """Load stage configurations from model or YAML file with fallback to defaults. @@ -247,7 +299,7 @@ def load_and_resolve_stage_configs( Args: model: Model name or path stage_configs_path: Optional path to YAML file containing stage configurations - base_engine_args: Base engine arguments to merge with stage configs + kwargs: Engine arguments to merge with stage configs default_stage_cfg_factory: Optional callable that takes no args and returns default stage config list when no configs are found @@ -256,7 +308,7 @@ def load_and_resolve_stage_configs( """ if stage_configs_path is None: config_path = resolve_model_config_path(model) - stage_configs = load_stage_configs_from_model(model, base_engine_args=base_engine_args) + stage_configs = load_stage_configs_from_model(model, base_engine_args=kwargs) if not stage_configs: if default_stage_cfg_factory is not None: default_stage_cfg = default_stage_cfg_factory() @@ -265,7 +317,7 @@ def load_and_resolve_stage_configs( stage_configs = [] else: config_path = stage_configs_path - stage_configs = load_stage_configs_from_yaml(stage_configs_path, base_engine_args=base_engine_args) + stage_configs = load_stage_configs_from_yaml(stage_configs_path, base_engine_args=kwargs) return config_path, stage_configs @@ -316,6 +368,69 @@ def get_final_stage_id_for_e2e( return final_stage_id_for_e2e +def filter_dataclass_kwargs(cls: Any, kwargs: dict) -> dict: + """Filter kwargs to only include fields defined in the dataclass. + + Args: + cls: Dataclass type + kwargs: Keyword arguments to filter + + Returns: + Filtered keyword arguments containing only valid dataclass fields + """ + if not is_dataclass(cls): + raise ValueError(f"{cls} is not a dataclass") + if not isinstance(kwargs, dict): + raise ValueError("kwargs must be a dictionary") + + def _filter_value(value: Any, annotation: Any) -> Any: + """Recursively filter nested dict/list values based on dataclass annotations.""" + if annotation is None: + return value + + origin = get_origin(annotation) + if origin is None: + if isinstance(annotation, type) and is_dataclass(annotation) and isinstance(value, dict): + return filter_dataclass_kwargs(annotation, value) + return value + + if origin in (list, tuple, set): + args = get_args(annotation) + inner = args[0] if args else None + if isinstance(value, (list, tuple, set)): + return type(value)(_filter_value(v, inner) for v in value) + return value + + if origin is dict: + args = get_args(annotation) + val_type = args[1] if len(args) > 1 else None + if isinstance(value, dict): + return {k: _filter_value(v, val_type) for k, v in value.items()} + return value + + if origin is types.UnionType or origin is getattr(types, "UnionType", None): + for arg in get_args(annotation): + if isinstance(arg, type) and is_dataclass(arg) and isinstance(value, dict): + return filter_dataclass_kwargs(arg, value) + # Try container-style filtering for union members + filtered = _filter_value(value, arg) + if filtered is not value: + return filtered + return value + + return value + + valid_fields = {f.name: f for f in fields(cls) if f.init} + filtered_kwargs = {} + for k, v in kwargs.items(): + if k not in valid_fields: + continue + field = valid_fields[k] + filtered_kwargs[k] = _filter_value(v, field.type) + + return filtered_kwargs + + # TODO(wuhang): Remove after PR #1115. def build_base_engine_args(source: Any) -> dict[str, Any] | None: """Build base engine args with tokenizer and parallel configuration. From 82e1bf2804784f1dfa6977e106df19937344675e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=AA=E5=BF=97=E9=B9=8F?= Date: Tue, 24 Feb 2026 23:54:57 +0800 Subject: [PATCH 06/10] [Doc]: Add bagel single/multi node usage with mooncake document (#1450) --- examples/offline_inference/bagel/README.md | 68 ++++++++++++ examples/online_serving/bagel/README.md | 123 +++++++++++++++++++++ 2 files changed, 191 insertions(+) diff --git a/examples/offline_inference/bagel/README.md b/examples/offline_inference/bagel/README.md index c1850c27a..9649a54c3 100644 --- a/examples/offline_inference/bagel/README.md +++ b/examples/offline_inference/bagel/README.md @@ -177,6 +177,74 @@ Example configuration for TP=2 on GPUs 0 and 1: | `max_inflight` | `1` | Maximum inflight requests | | `shm_threshold_bytes` | `65536` | Shared memory threshold (64KB) | +## Using Mooncake Connector + +[Mooncake](https://github.com/kvcache-ai/Mooncake) is a high-performance distributed KV cache transfer engine that enables efficient cross-node data movement via TCP or RDMA, making it ideal for multi-node disaggregated inference. + +By default, BAGEL uses `SharedMemoryConnector` for inter-stage communication. You can switch to the Mooncake connector for better performance on multi-GPU setups and to enable multi-node deployment. + +### Prerequisites + +Install the Mooncake transfer engine: + +```bash +# For CUDA-enabled systems (recommended) +pip install mooncake-transfer-engine + +# For non-CUDA systems +pip install mooncake-transfer-engine-non-cuda +``` + +### Step 1: Start the Mooncake Master + +On the **primary node**, start the Mooncake master service (run in a separate terminal or background with `&`): + +```bash +# Optional: enable disk-backed storage by creating a directory and passing --root_fs_dir. +# Without it, Mooncake runs in memory-only mode, which is sufficient for KV cache transfer. +mkdir -p ./mc_storage + +mooncake_master \ + --rpc_port=50051 \ + --enable_http_metadata_server=true \ + --http_metadata_server_host=0.0.0.0 \ + --http_metadata_server_port=8080 \ + --metrics_port=9003 \ + --root_fs_dir=./mc_storage/ \ + --cluster_id=mc-local-1 & +``` + +### Step 2: Run Offline Inference with Mooncake + +Use the provided Mooncake stage config [`bagel_multiconnector.yaml`](../../../vllm_omni/model_executor/stage_configs/bagel_multiconnector.yaml). Before launching, update the `metadata_server` and `master` addresses in the YAML to match your Mooncake master node's IP (use `127.0.0.1` for single-node testing). + +```bash +cd examples/offline_inference/bagel + +# Text to Image with Mooncake +python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \ + --modality text2img \ + --prompts "A cute cat" \ + --stage-configs-path ../../../vllm_omni/model_executor/stage_configs/bagel_multiconnector.yaml + +# Image to Text with Mooncake +python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \ + --modality img2text \ + --image-path /path/to/image.jpg \ + --prompts "Describe this image" \ + --stage-configs-path ../../../vllm_omni/model_executor/stage_configs/bagel_multiconnector.yaml + +# Text to Text with Mooncake +python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \ + --modality text2text \ + --prompts "What is the capital of France?" \ + --stage-configs-path ../../../vllm_omni/model_executor/stage_configs/bagel_multiconnector.yaml +``` + +For more details on the Mooncake connector and multi-node setup, see the [Mooncake Store Connector documentation](../../../docs/design/feature/omni_connectors/mooncake_store_connector.md). + +------ + ## FAQ - If you encounter an error about the backend of librosa, try to install ffmpeg with the command below. diff --git a/examples/online_serving/bagel/README.md b/examples/online_serving/bagel/README.md index 9308acb77..9b74acae1 100644 --- a/examples/online_serving/bagel/README.md +++ b/examples/online_serving/bagel/README.md @@ -45,6 +45,129 @@ For larger models or multi-GPU environments, you can enable Tensor Parallelism ( vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni --port 8091 --stage-configs-path /path/to/your/custom_bagel.yaml ``` +#### Using Mooncake Connector + +By default, BAGEL uses `SharedMemoryConnector` for inter-stage communication. You can use the [Mooncake](https://github.com/kvcache-ai/Mooncake) connector to transfer KV cache between stages, which also enables multi-node deployment. + +**1. Install Mooncake** + +```bash +# For CUDA-enabled systems (recommended) +pip install mooncake-transfer-engine + +# For non-CUDA systems +pip install mooncake-transfer-engine-non-cuda +``` + +**2. Start Mooncake Master** on the primary node: + +```bash +# Optional: enable disk-backed storage by creating a directory and passing --root_fs_dir. +# Without it, Mooncake runs in memory-only mode, which is sufficient for KV cache transfer. +mkdir -p ./mc_storage + +mooncake_master \ + --rpc_port=50051 \ + --enable_http_metadata_server=true \ + --http_metadata_server_host=0.0.0.0 \ + --http_metadata_server_port=8080 \ + --metrics_port=9003 \ + --root_fs_dir=./mc_storage/ \ + --cluster_id=mc-local-1 & +``` + +**3. Launch the server** with the Mooncake stage config: + +```bash +vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni --port 8091 \ + --stage-configs-path vllm_omni/model_executor/stage_configs/bagel_multiconnector.yaml +``` + +> **Note**: Before launching, edit [`bagel_multiconnector.yaml`](../../../vllm_omni/model_executor/stage_configs/bagel_multiconnector.yaml) and replace the `metadata_server` and `master` addresses with your Mooncake master node's actual IP. For single-node testing, `127.0.0.1` works. + +The client-side usage is identical to the default setup -- the Mooncake connector is transparent to the API. See the requests section below. + +For more details on the Mooncake connector configuration, see the [Mooncake Store Connector documentation](../../../docs/design/feature/omni_connectors/mooncake_store_connector.md). + +#### Multi-Node Deployment + +You can deploy each stage on a **separate node** for better resource utilization. In this example, the orchestrator (Stage 0 / Thinker) and Stage 1 (DiT) run on different machines, connected via Mooncake. + +Replace `` below with the actual IP address of your orchestrator node (e.g., `10.244.227.244`). + +> [!WARNING] +> **Before launching**, edit [`bagel_multiconnector.yaml`](../../../vllm_omni/model_executor/stage_configs/bagel_multiconnector.yaml) and replace the `metadata_server` and `master` addresses with your Mooncake master node's actual IP. Mismatched addresses will cause silent connection failures. + +**1. Start Mooncake Master** (on the orchestrator node): + +```bash +mooncake_master \ + --rpc_port=50051 \ + --enable_http_metadata_server=true \ + --http_metadata_server_host= \ + --http_metadata_server_port=8080 \ + --metrics_port=9003 +``` + +**2. Launch Stage 0 (Thinker / Orchestrator)** on the orchestrator node: + +```bash +vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni \ + --port 8000 \ # API server port for client requests + --stage-configs-path vllm_omni/model_executor/stage_configs/bagel_multiconnector.yaml \ + --stage-id 0 \ + -oma \ + -omp 8091 +``` + +**3. Launch Stage 1 (DiT)** on the remote node in headless mode: + +```bash +vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni \ + --stage-configs-path vllm_omni/model_executor/stage_configs/bagel_multiconnector.yaml \ + --stage-id 1 \ + --headless \ + -oma \ + -omp 8091 +``` + +**Mooncake Master arguments:** + +| Argument | Description | +| :------- | :---------- | +| `--rpc_port` | Mooncake RPC port for control-plane coordination between stages | +| `--enable_http_metadata_server` | Enable the HTTP metadata server for service discovery | +| `--http_metadata_server_host` | IP address to bind the metadata server (use the orchestrator node's IP) | +| `--http_metadata_server_port` | Port for the HTTP metadata server | +| `--metrics_port` | Port for Prometheus-compatible metrics endpoint | + +**vllm serve arguments:** + +| Argument | Description | +| :------- | :---------- | +| `--stage-id` | Which stage this process runs (0 = Thinker, 1 = DiT) | +| `--headless` | Run without the API server (worker-only mode) | +| `-oma` | Orchestrator master address | +| `-omp` | Orchestrator master port for Stage 1 to connect to Stage 0 for task coordination | + +> [!IMPORTANT] +> **Startup Order**: Stage 0 (orchestrator) must be launched **before** Stage 1 (headless). +> Stage 0 will appear to hang on startup until Stage 1 (worker) connects — this is expected behavior. + +**Network Requirements** + +All nodes must have network connectivity to each other. Ensure the following ports are open **between all participating nodes**: + +| Port | Protocol | Service | Direction | +| :--- | :------- | :------ | :-------- | +| 50051 | TCP | Mooncake Master RPC | Worker → Orchestrator | +| 8080 | TCP | Mooncake HTTP Metadata Server | Worker → Orchestrator | +| 8091 | TCP | Orchestrator Master (`-omp`) | Worker → Orchestrator | +| 8000 | TCP | API Server (`--port`) | Client → Orchestrator | +| 9003 | TCP | Metrics (optional) | Monitoring → Orchestrator | + +> **Tip**: If nodes are behind a firewall or in different VPCs/security groups, make sure the above ports are allowed in ingress/egress rules. All nodes should be reachable via their IP addresses (no NAT). Using nodes on the same subnet or VPC is recommended to minimize latency for Mooncake KV cache transfers. + ### Send Multi-modal Request Get into the bagel folder: From 4de077ea62464796cb07d1f15f5ec2201fab6857 Mon Sep 17 00:00:00 2001 From: Juan Pablo Zuluaga <46724788+JuanPZuluaga@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:16:46 +0100 Subject: [PATCH 07/10] [Qwen3TTS][Feat] Code2Wav batched decoding (#1426) Signed-off-by: pablo Co-authored-by: pablo --- .../offline_inference/qwen3_tts/README.md | 15 +- .../qwen3_tts/benchmark_prompts.txt | 12 ++ .../offline_inference/qwen3_tts/end2end.py | 91 ++++++--- .../models/qwen3_tts/qwen3_tts_code2wav.py | 192 +++++++++++------- .../stage_configs/qwen3_tts_batch.yaml | 105 ++++++++++ 5 files changed, 321 insertions(+), 94 deletions(-) create mode 100644 examples/offline_inference/qwen3_tts/benchmark_prompts.txt create mode 100644 vllm_omni/model_executor/stage_configs/qwen3_tts_batch.yaml diff --git a/examples/offline_inference/qwen3_tts/README.md b/examples/offline_inference/qwen3_tts/README.md index ddbd3fcbc..2b04cad75 100644 --- a/examples/offline_inference/qwen3_tts/README.md +++ b/examples/offline_inference/qwen3_tts/README.md @@ -87,7 +87,20 @@ Examples: python end2end.py --query-type Base --mode-tag icl ``` +## Batched Decoding + +The Code2Wav stage (stage 1) supports batched decoding, where multiple requests are decoded in a single forward pass through the SpeechTokenizer. To use it, provide a stage config with `max_batch_size > 1` and pass multiple prompts via `--txt-prompts` with a matching `--batch-size`. + +``` +python end2end.py --query-type CustomVoice \ + --txt-prompts benchmark_prompts.txt \ + --batch-size 4 \ + --stage-configs-path vllm_omni/model_executor/stage_configs/qwen3_tts_batch.yaml +``` + +**Important:** `--batch-size` must match a CUDA graph capture size (1, 2, 4, 8, 16...) because the Talker's code predictor KV cache is sized to `max_num_seqs`, and CUDA graphs pad the batch to the next capture size. Both stages need `max_batch_size >= batch_size` in the stage config for batching to take effect. If only stage 1 has a higher `max_batch_size`, it won't help — stage 1 can only batch chunks from requests that are in-flight simultaneously, which requires stage 0 to also process multiple requests concurrently. + ## Notes - The script uses the model paths embedded in `end2end.py`. Update them if your local cache path differs. -- Use `--output-dir` (preferred) or `--output-wav` to change the output folder. +- Use `--output-dir` to change the output folder. diff --git a/examples/offline_inference/qwen3_tts/benchmark_prompts.txt b/examples/offline_inference/qwen3_tts/benchmark_prompts.txt new file mode 100644 index 000000000..92577a418 --- /dev/null +++ b/examples/offline_inference/qwen3_tts/benchmark_prompts.txt @@ -0,0 +1,12 @@ +Hello, welcome to the voice synthesis benchmark test. +She said she would be here by noon, but nobody showed up. +The quick brown fox jumps over the lazy dog near the riverbank. +I can't believe how beautiful the sunset looks from up here on the mountain. +Please remember to bring your identification documents to the appointment tomorrow morning. +Have you ever wondered what it would be like to travel through time and visit ancient civilizations? +The restaurant on the corner serves the best pasta I have ever tasted in my entire life. +After the meeting, we should discuss the quarterly results and plan for the next phase. +Learning a new language takes patience, practice, and a genuine curiosity about other cultures. +The train leaves at half past seven, so we need to arrive at the station before then. +Could you please turn down the music a little bit, I'm trying to concentrate on my work. +It was a dark and stormy night when the old lighthouse keeper heard a knock at the door. diff --git a/examples/offline_inference/qwen3_tts/end2end.py b/examples/offline_inference/qwen3_tts/end2end.py index 12e5e1935..fec515d30 100644 --- a/examples/offline_inference/qwen3_tts/end2end.py +++ b/examples/offline_inference/qwen3_tts/end2end.py @@ -248,6 +248,13 @@ def main(args): Args: args: Parsed CLI args from parse_args(). """ + if args.batch_size < 1 or (args.batch_size & (args.batch_size - 1)) != 0: + raise ValueError( + f"--batch-size must be a power of two (got {args.batch_size}); " + "non-power-of-two values do not align with CUDA graph capture sizes " + "of Code2Wav." + ) + query_func = query_map[args.query_type] if args.query_type in {"CustomVoice", "VoiceDesign"}: query_result = query_func(use_batch_sample=args.use_batch_sample) @@ -260,6 +267,33 @@ def main(args): query_result = query_func() model_name = query_result.model_name + + # Load prompts from text file if provided. + # Use the default query as a template so task-specific fields + # (e.g. ref_audio for Base) are preserved; only override text. + if args.txt_prompts: + with open(args.txt_prompts) as f: + lines = [line.strip() for line in f if line.strip()] + if not lines: + raise ValueError(f"No valid prompts found in {args.txt_prompts}") + template = query_result.inputs + if isinstance(template, list): + template = template[0] + template_info = template["additional_information"] + inputs = [] + for text in lines: + additional_information = {**template_info, "text": [text]} + inputs.append( + { + "prompt_token_ids": [0] * _estimate_prompt_len(additional_information, model_name), + "additional_information": additional_information, + } + ) + else: + inputs = query_result.inputs + if not isinstance(inputs, list): + inputs = [inputs] + omni = Omni( model=model_name, stage_configs_path=args.stage_configs_path, @@ -267,32 +301,35 @@ def main(args): stage_init_timeout=args.stage_init_timeout, ) - output_dir = args.output_dir if getattr(args, "output_dir", None) else args.output_wav + output_dir = args.output_dir os.makedirs(output_dir, exist_ok=True) - omni_generator = omni.generate(query_result.inputs, sampling_params_list=None) - for stage_outputs in omni_generator: - for output in stage_outputs.request_output: - request_id = output.request_id - audio_data = output.outputs[0].multimodal_output["audio"] - # async_chunk mode returns a list of chunks; concatenate them. - if isinstance(audio_data, list): - audio_tensor = torch.cat(audio_data, dim=-1) - else: - audio_tensor = audio_data - output_wav = os.path.join(output_dir, f"output_{request_id}.wav") - sr_val = output.outputs[0].multimodal_output["sr"] - audio_samplerate = sr_val.item() if hasattr(sr_val, "item") else int(sr_val[-1]) - # Convert to numpy array and ensure correct format - audio_numpy = audio_tensor.float().detach().cpu().numpy() + batch_size = args.batch_size + for batch_start in range(0, len(inputs), batch_size): + batch = inputs[batch_start : batch_start + batch_size] + omni_generator = omni.generate(batch, sampling_params_list=None) + for stage_outputs in omni_generator: + for output in stage_outputs.request_output: + request_id = output.request_id + audio_data = output.outputs[0].multimodal_output["audio"] + # async_chunk mode returns a list of chunks; concatenate them. + if isinstance(audio_data, list): + audio_tensor = torch.cat(audio_data, dim=-1) + else: + audio_tensor = audio_data + output_wav = os.path.join(output_dir, f"output_{request_id}.wav") + sr_val = output.outputs[0].multimodal_output["sr"] + audio_samplerate = sr_val.item() if hasattr(sr_val, "item") else int(sr_val[-1]) + # Convert to numpy array and ensure correct format + audio_numpy = audio_tensor.float().detach().cpu().numpy() - # Ensure audio is 1D (flatten if needed) - if audio_numpy.ndim > 1: - audio_numpy = audio_numpy.flatten() + # Ensure audio is 1D (flatten if needed) + if audio_numpy.ndim > 1: + audio_numpy = audio_numpy.flatten() - # Save audio file with explicit WAV format - sf.write(output_wav, audio_numpy, samplerate=audio_samplerate, format="WAV") - print(f"Request ID: {request_id}, Saved audio to {output_wav}") + # Save audio file with explicit WAV format + sf.write(output_wav, audio_numpy, samplerate=audio_samplerate, format="WAV") + print(f"Request ID: {request_id}, Saved audio to {output_wav}") def parse_args(): @@ -341,9 +378,9 @@ def parse_args(): help="Threshold for using shared memory in bytes (default: 65536)", ) parser.add_argument( - "--output-wav", + "--output-dir", default="output_audio", - help="[Deprecated] Output wav directory (use --output-dir).", + help="Output directory for generated wav files (default: output_audio).", ) parser.add_argument( "--num-prompts", @@ -401,6 +438,12 @@ def parse_args(): choices=["icl", "xvec_only"], help="Mode tag for Base query x_vector_only_mode (default: icl).", ) + parser.add_argument( + "--batch-size", + type=int, + default=1, + help="Number of prompts per batch (default: 1, sequential).", + ) return parser.parse_args() 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 339268f34..a87027de1 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 @@ -9,6 +9,7 @@ import torch import torch.nn as nn from transformers.utils.hub import cached_file from vllm.config import VllmConfig +from vllm.forward_context import get_forward_context, is_forward_context_available from vllm.logger import init_logger from vllm_omni.model_executor.models.output_templates import OmniOutput @@ -116,6 +117,28 @@ class Qwen3TTSCode2Wav(nn.Module): def compute_logits(self, hidden_states: torch.Tensor | OmniOutput, sampling_metadata: Any = None) -> None: return None + def _split_request_ids(self, ids: torch.Tensor, seq_token_counts: list[int] | None = None) -> list[torch.Tensor]: + """Split concatenated input_ids into per-request segments. + + Uses seq_token_counts (injected by the runner via model_kwargs) when + available, falling back to forward-context ubatch_slices when + micro-batching is active. Returns [ids] for single-request batches. + """ + 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] + count) + n = ids.numel() + return [ids[boundaries[i] : min(boundaries[i + 1], n)] for i in range(len(seq_token_counts))] + 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] + s) + return [ids[boundaries[i] : boundaries[i + 1]] for i in range(len(boundaries) - 1)] + return [ids] + @torch.no_grad() def forward( self, @@ -124,98 +147,129 @@ class Qwen3TTSCode2Wav(nn.Module): intermediate_tensors: Any = None, inputs_embeds: torch.Tensor | None = None, **kwargs: Any, - ) -> tuple[torch.Tensor, torch.Tensor]: + ) -> OmniOutput: """Decode codec codes into audio waveform. - input_ids layout: [codec_context_frames, *flat_codes] + input_ids layout per request: [codec_context_frames, *flat_codes] where flat_codes is codebook-major [q*F]. + + When batched, uses forward context ubatch_slices to split the + concatenated input_ids and decode via a single batched forward pass. """ - tok = self._ensure_speech_tokenizer_loaded() + self._ensure_speech_tokenizer_loaded() assert self._num_quantizers is not None + assert self._decode_upsample_rate is not None assert self._output_sample_rate is not None - sr_val = self._output_sample_rate - empty_ret = ( - torch.zeros((0,), dtype=torch.float32), - torch.tensor(sr_val, dtype=torch.int32), - ) - - if input_ids is None: - return empty_ret - + tok = self._speech_tokenizer q = int(self._num_quantizers) - ids = input_ids.reshape(-1).to(dtype=torch.long) - n_tokens = ids.numel() + upsample = int(self._decode_upsample_rate) + sr_val = int(self._output_sample_rate) + sr_tensor = torch.tensor(sr_val, dtype=torch.int32) + empty = torch.zeros((0,), dtype=torch.float32) - if n_tokens == 0: - return empty_ret - - # input_ids[0] = codec_context_frames (prepended by stage_input_processor). - ctx_frames = int(ids[0].item()) - ids = ids[1:] - n_tokens = ids.numel() - - if n_tokens == 0: - return empty_ret - - # Warmup / dummy_run: not divisible by num_quantizers. - if n_tokens % q != 0: - logger.warning( - "Code2Wav input_ids length %d not divisible by num_quantizers %d, " - "likely a warmup run; returning empty audio.", - n_tokens, - q, + if input_ids is None or input_ids.numel() == 0: + return OmniOutput( + text_hidden_states=None, + multimodal_outputs={"model_outputs": [empty], "sr": [sr_tensor]}, ) - return empty_ret - total_frames = n_tokens // q + ids = input_ids.reshape(-1).to(dtype=torch.long) + request_ids_list = self._split_request_ids(ids, kwargs.get("seq_token_counts")) - # Reshape codebook-major flat [q*F] -> [q, F] -> [F, q] for SpeechTokenizer. - codes_fq = ids.reshape(q, total_frames).transpose(0, 1).contiguous() + # Parse each request: extract ctx_frames, validate, reshape codes. + # input_ids layout per request: [codec_context_frames, *flat_codes] + # where flat_codes is codebook-major [q*F]. + parsed = [] # (ctx_frames, actual_frames) + valid_codes = [] + valid_indices = [] + for i, req_ids in enumerate(request_ids_list): + if req_ids.numel() < 2: + parsed.append((0, 0)) + continue + ctx_frames = int(req_ids[0].item()) + flat = req_ids[1:] + n = flat.numel() + # Warmup / dummy_run: not divisible by num_quantizers. + if n == 0 or n % q != 0: + if n > 0: + logger.warning( + "Code2Wav input_ids length %d not divisible by num_quantizers %d, " + "likely a warmup run; returning empty audio.", + n, + q, + ) + parsed.append((0, 0)) + continue + frames = n // q + # Reshape codebook-major flat [q*F] -> [q, F] -> [F, q] for SpeechTokenizer. + codes_fq = flat.reshape(q, frames).transpose(0, 1).contiguous() + parsed.append((ctx_frames, frames)) + valid_codes.append({"audio_codes": codes_fq}) + valid_indices.append(i) - if not self._logged_codec_stats and total_frames > 1: + num_req = len(request_ids_list) + if not valid_codes: + return OmniOutput( + text_hidden_states=None, + multimodal_outputs={ + "model_outputs": [empty] * num_req, + "sr": [sr_tensor] * num_req, + }, + ) + + if not self._logged_codec_stats: self._logged_codec_stats = True try: - uniq = int(torch.unique(codes_fq).numel()) - cmin = int(codes_fq.min().item()) - cmax = int(codes_fq.max().item()) - head = codes_fq[: min(2, total_frames), : min(8, q)].cpu().tolist() + c = valid_codes[0]["audio_codes"] logger.info( - "Code2Wav codec: frames=%d q=%d uniq=%d range=[%d,%d] head=%s", - total_frames, + "Code2Wav codec: frames=%d q=%d uniq=%d range=[%d,%d] head=%s batch=%d", + c.shape[0], q, - uniq, - cmin, - cmax, - head, + int(torch.unique(c).numel()), + int(c.min().item()), + int(c.max().item()), + c[: min(2, c.shape[0]), : min(8, q)].cpu().tolist(), + len(valid_codes), ) except Exception: pass - wavs, sr = tok.decode({"audio_codes": codes_fq}) - if not wavs: - raise ValueError("SpeechTokenizer code2wav produced empty waveform list.") - audio_np = wavs[0].astype(np.float32, copy=False) + # Batched decode: single forward pass through SpeechTokenizer. + wavs, _ = tok.decode(valid_codes) + if len(wavs) != len(valid_codes): + raise RuntimeError(f"Code2Wav returned {len(wavs)} waveforms for {len(valid_codes)} requests") - # Trim left-context waveform samples (streaming sliding window). - if ctx_frames > 0: - upsample = self._decode_upsample_rate - if upsample is None or upsample <= 0: - raise ValueError(f"Invalid decode upsample rate: {upsample}") - cut = ctx_frames * upsample - if cut < audio_np.shape[0]: - audio_np = audio_np[cut:] - else: - logger.warning( - "Context trim %d >= decoded length %d; returning empty audio.", - cut, - audio_np.shape[0], - ) - return empty_ret + # Build per-request outputs, trimming padding and left-context. + audios = [empty] * num_req + srs = [sr_tensor] * num_req - audio_tensor = torch.from_numpy(audio_np).to(dtype=torch.float32).reshape(-1) - sr_tensor = torch.tensor(int(sr), dtype=torch.int32) - return audio_tensor, sr_tensor + for j, idx in enumerate(valid_indices): + ctx_frames, actual_frames = parsed[idx] + audio_np = wavs[j].astype(np.float32, copy=False) + # Trim decoder padding (output may be longer due to batch padding). + expected_len = actual_frames * upsample + if audio_np.shape[0] > expected_len: + audio_np = audio_np[:expected_len] + # Trim left-context waveform samples (streaming sliding window). + if ctx_frames > 0: + cut = ctx_frames * upsample + if cut < audio_np.shape[0]: + audio_np = audio_np[cut:] + else: + logger.warning( + "Context trim %d >= decoded length %d; returning empty audio.", + cut, + audio_np.shape[0], + ) + continue + if audio_np.shape[0] > 0: + audios[idx] = torch.from_numpy(audio_np).to(dtype=torch.float32).reshape(-1) + + return OmniOutput( + text_hidden_states=None, + multimodal_outputs={"model_outputs": audios, "sr": srs}, + ) def make_omni_output(self, model_outputs: torch.Tensor | OmniOutput, **kwargs: Any) -> OmniOutput: if isinstance(model_outputs, OmniOutput): diff --git a/vllm_omni/model_executor/stage_configs/qwen3_tts_batch.yaml b/vllm_omni/model_executor/stage_configs/qwen3_tts_batch.yaml new file mode 100644 index 000000000..d73739109 --- /dev/null +++ b/vllm_omni/model_executor/stage_configs/qwen3_tts_batch.yaml @@ -0,0 +1,105 @@ +# Same as qwen3_tts.yaml with batched talker and code2wav. +# Stage 0: max_batch_size 4, stage 1: max_batch_size 4. +# max_batch_size must be a power of two to align with CUDA graph capture sizes +# (stage 0) and must match --batch-size in end2end.py / benchmark scripts. +async_chunk: true +stage_args: + - stage_id: 0 + stage_type: llm + runtime: + devices: "0" + max_batch_size: 4 + engine_args: + model_stage: qwen3_tts + model_arch: Qwen3TTSTalkerForConditionalGeneration + # Force stage-specific registered architecture. + hf_overrides: + architectures: [Qwen3TTSTalkerForConditionalGeneration] + worker_type: ar + scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler + enforce_eager: false + trust_remote_code: true + async_scheduling: false + enable_prefix_caching: false + engine_output_type: latent + gpu_memory_utilization: 0.3 + distributed_executor_backend: "mp" + max_num_batched_tokens: 512 + max_model_len: 4096 + custom_process_next_stage_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_tts.talker2code2wav_async_chunk + # Use named connector to apply runtime.connectors.extra. + output_connectors: + to_stage_1: connector_of_shared_memory + default_sampling_params: + temperature: 0.9 + top_k: 50 + max_tokens: 4096 + seed: 42 + detokenize: false + repetition_penalty: 1.05 + stop_token_ids: [2150] + + - stage_id: 1 + stage_type: llm + runtime: + devices: "0" + max_batch_size: 4 + engine_args: + model_stage: code2wav + model_arch: Qwen3TTSCode2Wav + # Force stage-specific registered architecture. + hf_overrides: + architectures: [Qwen3TTSCode2Wav] + worker_type: generation + scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler + enforce_eager: true + trust_remote_code: true + async_scheduling: false + enable_prefix_caching: false + engine_output_type: audio + gpu_memory_utilization: 0.2 + distributed_executor_backend: "mp" + # Must be divisible by num_code_groups and cover (left_context + chunk). + max_num_batched_tokens: 8192 + # async_chunk appends windows per step; max_model_len must cover accumulated stream. + max_model_len: 32768 + engine_input_source: [0] + final_output: true + final_output_type: audio + # Distributed connector configuration + 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 + detokenize: true + repetition_penalty: 1.0 + +runtime: + enabled: true + defaults: + window_size: -1 + max_inflight: 1 + + connectors: + connector_of_shared_memory: + name: SharedMemoryConnector + extra: + shm_threshold_bytes: 65536 + # Frame-aligned codec streaming transport. + codec_streaming: true + # Connector polling / timeout (unit: loop count, sleep interval in seconds). + connector_get_sleep_s: 0.01 + connector_get_max_wait_first_chunk: 3000 + connector_get_max_wait: 300 + # Align with Omni: small chunks with sufficient context overlap. + codec_chunk_frames: 25 + codec_left_context_frames: 25 + + edges: + - from: 0 + to: 1 + window_size: -1 From 296d995ec263f3dadde511de4a89f132c9ccf944 Mon Sep 17 00:00:00 2001 From: Zhou Taichang Date: Wed, 25 Feb 2026 12:06:01 +0800 Subject: [PATCH 08/10] [CI] Remove overwhelming debug log (#1463) Signed-off-by: tzhouam --- vllm_omni/worker/gpu_ar_model_runner.py | 24 ------------------------ vllm_omni/worker/gpu_model_runner.py | 6 ------ 2 files changed, 30 deletions(-) diff --git a/vllm_omni/worker/gpu_ar_model_runner.py b/vllm_omni/worker/gpu_ar_model_runner.py index 7b23b3b91..5704b590a 100644 --- a/vllm_omni/worker/gpu_ar_model_runner.py +++ b/vllm_omni/worker/gpu_ar_model_runner.py @@ -192,15 +192,6 @@ class GPUARModelRunner(OmniGPUModelRunner): num_encoder_reqs=len(scheduler_output.scheduled_encoder_inputs), ) - logger.debug( - "Running batch with cudagraph_mode: %s, batch_descriptor: %s, " - "should_ubatch: %s, num_tokens_across_dp: %s", - cudagraph_mode, - batch_desc, - should_ubatch, - num_tokens_across_dp, - ) - num_tokens_padded = batch_desc.num_tokens num_reqs_padded = batch_desc.num_reqs if batch_desc.num_reqs is not None else num_reqs ubatch_slices, ubatch_slices_padded = maybe_create_ubatch_slices( @@ -211,12 +202,6 @@ class GPUARModelRunner(OmniGPUModelRunner): self.parallel_config.num_ubatches, ) - logger.debug( - "ubatch_slices: %s, ubatch_slices_padded: %s", - ubatch_slices, - ubatch_slices_padded, - ) - pad_attn = cudagraph_mode == CUDAGraphMode.FULL use_spec_decode = len(scheduler_output.scheduled_spec_decode_tokens) > 0 @@ -308,15 +293,6 @@ class GPUARModelRunner(OmniGPUModelRunner): aux_hidden_states = None hidden_states, multimodal_outputs = self.extract_multimodal_outputs(model_output) - if multimodal_outputs is not None: - keys_or_type = ( - list(multimodal_outputs.keys()) - if isinstance(multimodal_outputs, dict) - else type(multimodal_outputs) - ) - logger.debug(f"[AR] execute_model: multimodal_outputs keys = {keys_or_type}") - else: - logger.debug("[AR] execute_model: multimodal_outputs is None") if not self.broadcast_pp_output: # Common case. diff --git a/vllm_omni/worker/gpu_model_runner.py b/vllm_omni/worker/gpu_model_runner.py index a01ad113a..ca072fea9 100644 --- a/vllm_omni/worker/gpu_model_runner.py +++ b/vllm_omni/worker/gpu_model_runner.py @@ -1151,12 +1151,6 @@ class OmniGPUModelRunner(GPUModelRunner): """Inject omni-specific kwargs into forward and cache model output""" model_kwargs_extra = self._build_model_kwargs_extra() - runtime_info = model_kwargs_extra.get("runtime_additional_information", []) - if runtime_info: - for i, info in enumerate(runtime_info): - if info: - logger.debug(f"[OMNI] req[{i}] runtime_additional_information keys: {list(info.keys())}") - model_output = super()._model_forward( input_ids=input_ids, positions=positions, From bf9754a8b14b097f777ab7ec8a6ec2d3f80a5334 Mon Sep 17 00:00:00 2001 From: WeiQing Chen <40507679+david6666666@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:06:27 +0800 Subject: [PATCH 09/10] [Misc] update wechat image (#1464) Signed-off-by: David Chen <530634352@qq.com> --- docs/assets/WeChat.jpg | Bin 126134 -> 129767 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/WeChat.jpg b/docs/assets/WeChat.jpg index 577ca90de609a145cc0c8ef9d6879b0ba6eb07d7..3d049b5467f1232bf21a864a9d69217396140801 100644 GIT binary patch literal 129767 zcmdqJ2UJsCyEVEYKoF3C^ctjyQd9&)YCx2#f`uY2Do7C!L1|KgAiW6)2r5BQ0Rbbu zh=g85dI?Q>uL(6k%D;WfIp=%N9rwTGzvF)64`a(9EZKYQ{j53XGoNRLIz*iW*bH>^ zbpRR~0H6WC0O}~94KUErGtkp9FwiqVp$v>nY)ni?jxceu!dTdNIC*(_IJvp`g(Tqo z0%C&P+#)g}Vkaa|NuA<@%bt}zc~;`&sgu9|2n`epWnyIFU}EAp$v@c!O(bdy8xOvOO)Xe<0g{|Fvdk06S2alh4dU^Z!J`H&u8WtWA8I_py zGC3vnRa*M{tPdYQW#{C6E-Ef5Ei136tg3HlY-(<4ZTr#N*FP|b8yX&&nx2`Rn_pO5 zS|)66ZSU;v5%&*%&x;0t{5dV~-#;hzk9o0!^P;7rgU~^L&x?lE8$2NFbo9sN7>;RO zhu(GP5Iy&dk@I50yTZC7V)8feT=zVBn7G9irX&czr}oFp{;y3e`2W$&{$pbQIj<3b z1wsQpJP13001iGyoy?^D_dhxS&g<#&n)ICZG2Jwm`LYy%>pG7A`DPSw^8J4v+4;*U z7N^r5=PakPkKVR?mw4Om0u#M|<&4qDmqnXb)qL+Q1JZcoOCI>W`k|hsV~vf5XJ+jV z5=`os`6WZ|j;D<4?#9dtOUV3;KuoF%Qi0m=kN%~aa7|Q$iO&S(S)c&;mM;|`?!fT_ zHcC_=6B$MY@WLpHIugcegz zz^ICEU0AI3!n{8TypQ^q1Cl}DiLI8O`c_hkEB%Eqs_+~y9J|6EbudYm%6LtXtb6)) zo7(~vx&1o2a@5XU*W=^c?*Jwo;ixZc6jO6*O9<^c*i*3poU9cdSSb9~B-$o$PUXdq zACI(z7!_y^K3+EMZF4>MGg1HSJ8>#dCDj3l6}^{Nr8nUde*;KIp`s|XI#T1ZJt2@_ zjoxY##0Zma_#`eWL7S=Aa5L;z_$elW{ZOg2^42cGSM=?wOR!#E&_bzSJxAew3-aAa2Obis0DC zCHr4b(XWy4DVX>2rVLbB6^c(9)WT|klaOtg4l#V{sN40Z27SVbRA0Z-{uOsCOzmsC zXsgmmSbTnw_O$B>J?pH5httPjPR76S#`H4*QmU^bTwUiXDw9QnB~EWD*ME-Fm;9P5 z-HLtCCX5J9uZtaKHl$6==3_8vJurq|r2@s&h5peL}Wo3-eUG0yTIpmsl0rqyu!D{V=5ZQX&1$OL+Aip?Hm z7%gZDAA+~mwida4ei6iuW2FO2>B%X%*$t>qzqCE?pWBG@d2{aEL9q1Zc+Cf*&|?)! zJBsc0iI)mzd_<3Tt+xzkA}&U0ZfwfUlhch4<@+uin9VC=oXIcdo)DWJMy;~1E{tVPp63(}aD=Q!{q>{- z#6DavNj&%dq|DD2fTlmm%8Vq72B<*yulH17^2Q)yO;P3riIt#wX*K2sy~H(E7DBMb z^J5xeSdARW_F+SYM34VfXW5qU9mA_SjOcdnvKJRjn;*HaI$PA7BS+4<6L>5lR2fGN z?C*EcMZT{S>$juW*L3_zJ@0D1m47WxU*C0{EDL3BlZh?8u6yI^Rhv_XHtpg_=qL7X zCk}^9%HBMLD?vwsRE>z1!ZcLi-8UT&hLgX%*1{=Z*@l>Z!HmnWB{DZV%3r9=dh!Xa zY;M-%ki#Wjbj&bD7sklu(RTq!CnbfeVXel{ftS}W19yo@P2L$WfERsXGyw&m+fdY)|&3%n@bKBjZSWtyFgoWk(@u5Kv8(0FIFt>wL3+l%*68G zk-D-tMRDc{%|Y&E4=TVk=Uc<)qjURhhHkIS@Y_g<$Y)2yGd@-QNl(&^foB7|Ep(q@ z=6e;Qjn)F}&t7`G1vD{7QIl#^ zfb_Hm^*%@==syN3{%<+(TUOw}xI`RTz@qz!^Xts9pMKDxlkiMBuvNWf)TO)iadZLe%2^%EAGV z@uPS8U|qoSf{kC=D{=SCc6s3-rd?*laQXDnjt}zW*GAz#H7Qo*nko*3yvwIhc7Eok z1LZ6O-t*3#9l0vw*Dn+U(1gF6-9ma9LmS}upA&4Tg_Kk(@C-$MfS5|F5upNIvS+9O zEfqKz*i+}E!MBj)l^Wsub05J~i=+Y;@kD#K2*g&qAvg&vr8J=HGT<*q1z0)=P>Qb; zipWC+2D2!9ucu+8&0bK$fPbt2`54qNET{%~e=RDIVS%L3Q-PfvFnH{VAyh!E1WRJ3 z0xM=?+yDKdJ{AOaPOvEyoSxoaEjVq;HFmG80w%_?68WHcY=dn}vJ}Z=PI2(AUn~@Z$nsV|> zHQNH8@>*O&BafS(Vkj;_sPRL|mr!m0)j$H@%3zRs6)3b}F9Q_^ zX53XFzCVlG=M>yxxE4JwtXPF59m@HPxEhS8U)^4@Jn`s6n&cKAPbG8QRmn9P0-~AS z|Hj~xHG~beFTZ48ql@Ldd^M9MuqxXzsJuC@hFy2rVcPSW=kD^DP=Pa%qwf zHjfB3gGzm9)o+mm9QdH~NaF1p6%cw`8WBV}Fc70MVcm#`3{Ve0$JVLT1fSOUiRhfn}_QL7}9%+S#MQrSJv3I!? zs~1~nyFH}bk@}f$Ue0?MWV>Q@rV%~d()2Vnb|=DowSpl%?SR(^H_D#QG!DYKPvi3?(4>?x2yJ3Vt#(3X(ysC+XnL)J{A71`+&@t zNn6nsBg~rAk^%IW2>6ATWw<6@T0QrzCj zr})`MD9v@QuK@5gsXtL)vB#g6m_4IMk@R^wveBp#m*s@=<$Afhw+B$tn>MS@$)^#0&|`4^3|B%UCdjAJv>7AI8Kdz zy0~CmNXM1Sf7EH+P%XAn{N9)0cHm~*|#fh?)7eva~+hl-@5ZpDI`9EAIZq zPbwrxJroFx#kErb;Tnifd=r}OboJ$_%C!?wWVQ~@O<#MSOSgC@XW25zkJqfsR;LK^ zO?xP=C*LJ+;@nE5kLvp0XbO-1P zCtYGe=X7+&HMuUKykCLJ{^B)+)Szu4dF^j86OIvujY*_Dm9<;Si26sBq?M7pdW18Q z-#LR5UzXrDb%-l;)$zXl<+y}B(=$EBT2iHCqVf!1vj@!-2;U$>VE=<54q#K^^@bQ$ z65pVj_x_(ehxwD|`q0HKQJx4M zl5C%+tcOX)zKtILor-gGE(~tFh5fjX#?YX^4iv)R(K1&IPg%R z{Y;_k*7rBDoJD%QdaoRys+!k;7V!nOJCNn{$v`EdC@Ml2_|}vEg`2rhbnK#)fBwpw zQ`=B==PC4GnBLF`C!YO{>6q%kqr|YoCL~MAQ3o@*QgT1E)3ML--k0`oLXo3Map$f9 zg2vD`)W7PHy$ukEOtbOREoNxzif5Qo?$|45-?sDXeuH{@c%ys2e|jy=89C$$qywH@ z`yB=TR)q1q&%cm}C`D(}x9$7fsUF%awngtDK>#U0Us-X)pMm&p<^(b8z4?Um-a%|K zX?oDnE!!98>}}mLcXueh-(6Gq^`VwOGXWMudgQMKE>j81R|zM=rj2A8Ti@xxl<8m# zHqOx1rWss@;;W_WmwrMJBi^ojvonG-z2#Kk0Wwlpx-U???Y-l{zkY^IxaUVi5Od{$0*t9_Q+hBHL&h&B6 z@0m&dbXB8Q;BrD3b40g@xL4=c;79)*Li@C2#FLF@He7>GzJ-46FI_B%{W(~hr&|M) zt1fjH8t_t|k z#083q{QPfUSB*MCx-*=|av(nt58U>T2Z|kxqL!|h;JE@04{xH{mFi0!)E|-*zSX4?exX$IKib zSbRElnpvr|>Y{sw>E%wPQ;S;pDhGSJW2Im;Ja=yJ*0_Rtz!T4#>iFyHCxeAA=V+Sz zLp6oW>f`sNY*?wl*A6r#-heEH=|G0UH*Zx7QGvU!5oF=y0~64hy#B`l=c0^YH%%0< zhqO&pAZr1GURi{Vno$fsflKR-Id;FYM~q#SVm9ss5@HKBoKpv3!UZ>Vd z6A&2DbD5K7f9CKK6=-`vFeCe8DA5*M>Dz82k`zO;DD27{YTE+$rW-nd_}Vp2!1RLA z?Tj1n+{|8;->Y`sE}{Y>J3H0@7rtxF_5JdBERUPwk=1!i0pdMpr`CD0x6fH}4K1sS+h8Zz2JSIoFtLD7FUiegYXjXrE_qg>~`>bWt+s~GCi7j6N zet?AEybs2y5f$)HKh)k;=qWv_RLh?{K>kpMM~)wHP1lN7q@|aETG2?D zcVRXdjM`Kn)DKsOh-4!f;;YjmiqCs{_3rbqg;YXpMRM2AUIw0Wfsp}6V@txKbDbu5 zc9t+M^HI{nH9ZgKJc^n%1)d2{#y{BVkksw>jJ-B$)F-o0`MuIz@Ve-1F?AJ?D?JeqG&;8;ehnMQH7|`S4YvbG3ZB3>4V#%A%I~SvX z{nRPk^Bo*HskB4+NgHJ%hwpTOYb(uM%&XQr>h;HK%rM;4qEs@dR{i2VjWtzz2%i$S*P(l)1>KL!#4WCY0fL@Xhus|Xzn?YilyY;->idtgmu-I3ChAWD zG~(&xv+t|@X0Xloi4N}B=C_xEslfGb+H%J#^?KEkJ9Whq5eC36zw|7E=qzhX2@ODi zNiLEky@U;?NR<%EL$)W_mI#JNzc0(Nx9=q+N1fHExUaUC$r|=M!}^KfaD4hH95dlm zRT{HfKG<7i$bk{rp%Z@2rbTDRrp3ol75}VCI6KeZ9ACe*d$ViTlK5~zs)AixFsgIa z2%yP5CsoX}0OXWYNiSBoxHU64gU#eTUM*ilnu3B3&;9Pu|eA zIwqHq){^tZw0*Y-tMrw5f@MVZo%{T&FT533w#vo$n*88R6VWL(5d6dBe2Zbl)q}65 zC6S#?BPQml>bfi!b~0f>)oIv;m*~AVOhFCQ$I=2d{JJRxIlLu1Sh$<(a?fA-S}d*T z*S1OSn=vzD(02@#U4KSM>|YTwN`d>|?jv2tV~q&`O&`i~Kgqn`rQ847lll1T9^Kg{ zz@J9_<4+EKmffw(GA`G9y7QzV!HnvRitEV2-PoKig66XO?Vm0(YxngR#F%1cn9e;- z>VkCRw3XKG=kE9V3iA|gmJ7XJxAanVfD}BHtfd0+aQxS5!}H;}PDk!~_3+(s6h9rq zlX5g8yf;XFFlztXwA-4rWGKlg$1zjzF*@1$a z7Wi2&_3lVdDRVe}yny~gk<;#y49B$)QKfqywbtcBx(xP_ zKmK0ws=cY4G;{D`hWH#uETclJqUCK_hZKDW^I2ZkxZxYRE^&{1F=k+qOnU3)^tJHZ z#c>s!tfJwmf^`j?%(L51^W^T|(WPV4YQ^+corG*TPnkyMI>Xu35N@A_uewXwG^vK# zaIrVYAM*id#=nQ}4)l0(%(%jDb9t~QCU=usC za=E{tM!GBas>8V366n#w~hBGLjmyin(by_5V!!Q7Sp;5h4@QP;V`3z8yobz)-JYtFNn6Gr>< zQ~+FV{X)#Q$_KZvB20=Vb}d(Y*58kx9Tn0(t!o=BMF#x$4tW<3!ro2>SZp5jn#*o{ zNnXnUS<&Q?Icr51-v;vHZ$m(L4XhrCg9ePH8~IdJ4J`(X`a_wD|E){`u7k)-R4X$$ z4C6xuw%L)Xp8t49lb`b#=dngWH629%QTe9y&psIq4aU}ntSK^s%X%{&?;Y$t_Y3=h zH;@(unw=IjjIrqPTB;)55CD;jvKDhs`=Q z2M>LY?CVyrTGTP5Pw|vaNZtd?JW?;7v&j3gV9e|IN~R-wiWA1rj)}4b zQj`XmLSq)FK#wP3X$5&Va1_0HlK7Z{1;yQETuXKV;E7;D%;x&2QX0;q4%DT$KVuaG zIZEp2TsXavcYZtWDOT6b_+0+I12zI>L?Ho zT6QC;K;pnO=3pNQ8i)&uixVYAq!bJn3ipEwES6%FGhv@m|HYkMnKK%4*zmzEfx@bE zWx48)0=@JCeDpEp5*kJYhCubWScb{Uiz61gO$o#^SjWRlj=VobpK(!J+9CFJpzFl? zmL~jl>8|9eE)@`Uh(N81U=L)>e3|x$f#KWHa*JHIxNZCKtbkRgOvRcPAp?&2Wa#a_9kM9yR?nB|>aSH=;x~1C?vm<+K+P1)deqUF z<}#--;5_3t>Oj$5gHr0yhbAkl>6=s_E(ObaqEBI8<3t#+b*;kYXUcIg zkhoc6=)xM9r-AFYjR}tT2ig+;H5HO`Q#8jm?vZk>Brijd>W|-XA+6e| zGX8q1gMY*d3#HOJJn8D({f|t#1LS}8*^>DRsv_SGy%4fzt3>*@b zO$eInKWViy=DuR{i{RB)cf&^(Hw_z}>Jfdy*z(BEf9jGS!|F^?T-qwQOww!6L^u^V zQAmK354*?`QAw+)Vly(`I;aa@Z%{;QLW#Z(liPUu{6O|Y@eJO?6UW1)CdpeT)*s_k zD|zRG5K7V`C_AHlnL+fVR-<$5-Q;9V4E?z=$+8V_E2arYupLX+RV{Z5-BgQ972N8r1WEQm$$ylu=F zijt;q-sPJ;JF2n{SIYXIe0#N?|HW9Jv*%4-?uOa#Ru8aX$>DE@=Lz$S-2U#+$ld_< zhMCWlr@Myp)!AK?Uwab;Y%@uBkn#m4y zRi9GaJh9V^16lN@M$4fhs{7goWxob=oTbMoOcq2Y_|Yq%)DRD|R-YGjVxR(ND^?F{ z-CHzuuf3$%pR%5)Mej>fj)CA$qylgKa51YWhnIwFpQ@TB!*6gXjU4N?<{dPehP2;! z`*d=r;|YQxpTY#=3BwEZjyXqjC^$b}2!`^VS~y8x^JYf9SW}qe@HVQ=WdOAjilszZ z5S9;%XWpXg9R?Kk1I4=p`J*rSXTjdiSKrxjXOraXWcHhRhq=&~kFcM~v4|)=!mlm0 z#sha@PfCcLdvOwJ^+IkiNaHyXbXvqQbrZV`#bA^lv9{KADb#i2xBM(4l_{c08`{F# z;&SWdAF0k9yi|$w0(&gL6}$aOipW$$Cg);jLy1W%u2kTooLTKHz-;*hXkCcNDp9`A zfCB7tpf&Cv?*NWwji@pQQu1r$Aw8HNXGvhnrKb_2N)&?*3_186coB)+&=Al?__ick^kM_#itmrXlOHq7@BTMuTlO(jP7=9~l($)r zy}6F)*QYE|O!n_;9sxPxH&4*~lU?UmJ`lkhE5TEb-Q_9;?@PNT{L^m*5a>hcL2O!7 zn@|CyG!=M1SUvw;;2#tWiXWqYF;x6>4_U|?{A3yI?)Lz*KOHImnM3ddtw~cL05GD- zoCDjQVA%#4)c4x-w^RxV0Nhynd#UENys5e{g%*!nyVCR`HxlTd|J|t=2wg zuzH=~td9ffTzoWQQTZKe0)!a?C(ZIiVHYX!HfmKui4vT=UfSRQ!uNd{w_6!*G%Hn-0J?yEAh^=y!YIWq^x-Sp(xLhXH2Fl})#T%6&yog%R4hPJ zMaa0ZJ_H3~0QRjXoDnsf_ge~bqYspwN7b|srJRil#x=sfEVW$mW!Bp5n7)(!=6t`% z3y*UuGZ*-3hlsX^3J)^igqqZRVu(dyBJNt+!$eitx!GjibF{ZLnG=%w332i-Xm);k zO~N#Ps7IMzP(8}?P$DvFq%b{hu~=B@v{5ip*Rt5jlcn9;_4u6HfT^*dt*V}vpa(1L zLfw8lh0pN6K4f4Sb+>CD>Vu_JjZlG8&dXAp*dPScEYbEIvdJ$nH9hW~Xf;KzQIF%1 zW+jjmCZ4nsbfIm>08BB#;!y@Kac&y5R{)CJbTEOyys0@K*i{uC>+kA5Ed(=ZBtPfRshLqS9hTd66Y+o|D*0Ot@z+t0#rgTk0 zmN{ObVP5`um#UWWk(Mh7HhqNDDS*%w{KOT0rT=KbcpR6k&CJULug`3UI*&u4BA#nq z032f0kAPrj2Pk4FtREHlP-qH1$@nPjqU!rVkKamb9m}e5tZphklph_kv2#Q+z}x^= z+z`b6A?7&y>=UM}r2bjL8vqt0J%Y8)zO~>b_M9+>kIel~MXyB2_Jh;OI9MJx@suB?oIY0u$ zFu@MI!DQj0!G2WD7CEzqt%Gp>=((9|{kFyZ`QD|0Y3)`R%UOdvT z%YXzgdeaW-a*W^Ubio^w-0>{QP;#;1s`~ajn`X_g@0% zlr_-V15W_6$Uu-@H+4V$CP~EZgWZv7${C`N#&I9f6>TL~C;4+7ESdK&MZaNy|59E6 z#f=m|n!teG>OGf*X41^wdF-J@5q9dOKZu1|zhGObR3HN5N~Fg}8HJ_W=aI8=B*wFH zxo-OhUDAvE_HZmne}7c`@TpypKfCopEp}f<_}Bt`iGV9`h8KmEl5-!E<1tPx*T%2F zAB;;J89F~8rZIylH(1i(4D2{Shmzd0SJKyu9Td1>3r$?F?sbVAzKcFtztgFF;F$@N zBB5GGbQA8_Znx+(O?c%L1AIKl*cHvXKE=>c=cy@9EM zts(wb93re%M$TO{_l?PLK5Mn9DMfQ(Edg}AvNbpaNiukE%&sDu5@E2sqSTOnONI5> zCnxTB@swTeLW{;tw%f^^CnW)vN!B3K+0y&mR@Q1uiyiXAGr_hbpXC|T)n9JBhwoPT%{W%o~xh={^!r# zQ02={Q21beR3LZ>OHAK}1tX4-Og@9+ zQt#XM-ZLd&2Xs~3veWvcW6cZ$)4L$?L|_K&M@ zk#0CJS??+}cN*L+T6Oud9c%K{FD67&U?6m+LuW=0kBBIh<~ic+kZomku}Pc$No3kJ z81%)3f{{i-bDDdBk8~rybxKz$(rJ&o$L)gkhJe)UE}tlvf~{8@efjmP+Zh)+7x= zY(anb)hSoEpOHV#x1SR7U}fc{W!!^utssjgf=E|g2{-!8`xTr=k0OksZYrd{C{aEm zAR5ZTFq4#_Leu3E6!7sc#R%mgyf&-p4&)(5-|Wpkzt?SPe@c_?oeYndI{s>)FE+w6 zRlKYqM-a?;^iTv$4=jdXXd_aC-zP-BH^Hu^vV8QIysbNn0N-9yAPO5qvdX_jw6t!$ zfFPU!aWpa$=1z_!T`#cqS-9D=z9SGFt6>POlDjfT5UqZ|D4Eb8{MHA9S((KQL+dm| zd#jyuSDuz7d1nuOj2?k_b6n~YQ*+(S1N3OF8C75k905!c6>!<$aOSq4gk}vt;21Sm z%&l^q@_7AWyggA@@>%V{0dA0MvuiDU63ganu^yT4IAT5c&OT+r&fQtSVDWIc(u8g@ z1ed*}2|ovFt`+z;cRvDu95iEUC}ebLhjt^1QB5^>h3Dww04ak+gND>23JJLy*$-ke z$j(#nf3Bay_DQAiQaH2!2|OjC;G#&sh`x<^$iv|phZnQ~n(k=-ZfF2_r=j!lk<|{< zvd%U&{bzQf2P-pf{JdeWsR{rWz+FF(-YR5p;8_9#nqbsm$WY+ZGmg}DP%)HNY2!?k zDte(0mVs$$@-fFq3WSsd4AR_a>bt2!*bTK&=i4VD>t@eY#jt6?anPtG4Q788f@@bJ z6=1!3A|z*cbZET6PYsu{!6f6}nm_iHPm&k#(BEzY-~o^SP8t6Gj#LjE78V|IVBc?^1g-}d+!?g^ZjFX(zudPnf z-|}b>d-Ig+2$|_xBimAfN5Uy|2qJEP3eZ*G|I&V_M8w>xz$W^w39MZo?l|+aHJ4T; z*l4^#ZeJR2+jAx>;i>5d%-dYJ1EXbSwym+1n87ko5#_3pIrh~gV82U5i>lLrB zFeaUEb%!64C833UnXn{D!ih??Uc=LcIVFj5UbY^jL=Pj2-L7G=CY3QX2cpQn|BJ)d zZ)FK{Q4Xh>MSdh?gkuag^(k;-J#*yor23JCS1~4KLk10EW2a&LX2p~rHuQwkXoBF1 zN=Ab3-Mh|i;t%&;lt`pC1ID%7d)t|?LPyXR+Yj08B%Hu}Di1IDu5sY(toky=H-&q| z`bCaaLMMX}*XABcoWF%k?*=wrNc;}3&*VZip)V^f4F?&~*;dHW5t*-7QhL(FZ_Meb zYPJaqJI}lQ6|v%%kX$h9;G6;zx~?^yHuCxzApnz049yn|j$^Qb=E;Cb;_EMB~U@ zY2(eh{0FCGegiv}PB3Kfd;4w<9v+F&IB^x+CZV`S4qauBm@N+dG;*EG{H(FTn}eOM z!D_G3slAwor$vL$Fgs`1#jtyuw`Pe?+PwgT7oq?%3lWtwwjxZ2pU?K6THkmw>YVlp zfT%X+w?MMRaa!g!w0)-)jOcv)^uG`9eiKp|A8`$r{w`|bB0M89xc2|GHrjXFEhoBS zoYUvxBTEF7OBqv!EXK5G*AvzAf97*~XpQL#2M1{wy{|$Lwfrsr=JI`z%Mnxb1UE9% z$R34x9`u(SA#dJS{R`p$If#zn{a**Y{y!L$-ZeE5gE{@aJfwbP;5yU@D6C4-bp}t(0ndtm}(Yq7lP3N-Zs?4;$I;XV}Jk(X{Rq}5p)F!dRMkYnQ zHD_<1fA{ExY)5rjkNm<>G1~_VWsjcX#{clC{`QUj`Yv|XM5d7*BAyWpS{GPH^uDZ( z6qs!!gNBN;&0}j^i_o<$b7Jwd^V)gOiju^YBcdT;Bk!)sufr7U|EFCle|QMCtdN+8gd46*on4cLI^Ql{VY!sl&s$AQXLz;1gsYM!`q&0!`sxQ?L9%I{jIvXP$jSyb+cOGlOY%JyJ!cQ6{apV+~>{@=ec(Emx!?DM zA%?xu^vJD*G041NpiWNUkY;@Js-B1dARak$HbWWhkNp#=8Kl>VWtKJOAG zUfsVz6YfJ|A%1h8nP7BIJ?MD;2^uF~{GMQ9Rm@5PTQfmF<~glq30ksmE)#ZDWfB8q~q&^p#%Px zWs85w#yl{kcl|c}_PUmOn7Q57vkaKQzAA-3t3{|}18gFkRGT1q)r6C?AxgCq9HBQ_ zTm)eU#3-!BB4ll27&mk6BLRM^e>*m?zL$B$Cvbps@NUZam~*+NruM2uwX=e3Pgx!> z1S*lU{q-HD-|F;f^=^OYTl?f_Y;$Q-Z}i#`2`g5;rKlg|C@9G);C{rU(-rxlzym3Q z5ii&wG1x#08?~PV{R?eP_(dPc?kfWK$7%8zBi#0P*;O}5!+S1SKC?PrO!-R6bhN%) zJEej4Bm^CCuR%*o^3Y&3o9t;pKKG|=Nf;897mhmTZg_o+3ba&X*Wq)TZ~ympl7PD9ew=ay@WaDQTN+GS0Hp@unP4YSz`Zv8X@E~A{nv$>%!s98HCj)>4U zN0cVc%-G~M%ey@c#8z1lMga0f{9vB&_o-iJ%*$h^!;MR#*4PP^rk7Y~TKMV;q~>Zk zbB#jBVWxEF7I*K2jP4z=zWV=0XShuUn6_Ks;}QZsSxGeIm-VbAcp#qBGOC5FIzqhu zvdw?HgMS~e*I3uCeNKO6uo}<>Rs&XM`UY!C$z?l+Cwl!=Kg!m>)p{sA^^Q-%b~Lu- z>wJv?xCb|v2=)O;7=l$FzG9FEmyjP#_Ve&r6ob?d?5ey2Se5C;8sz-F-5N5D{b!GA zfYYG^QEGZqR=R1BUH@ch#IQ|h+pzF-PmL;YyAH7?>z}I0s$=K*$xa##%IBDwY+>1`iPr0D6kDuiFA4+0`!3tZK%KmJLtKBhR8ZG9&C9w z*&q`@bCJX5@xRS5MI&=!`Amny)r^Mg1jX&Lq}U>nk=J35V{)M71&>OMX$xia@9C>0 z`t|-c#BToAx{D3$P}E0ip&^j5dSHk5e(;t>vY@VrNWNScqn`Q^nT18u@6$m{TbKwe zFG*rj$W%zlqM{;wzwf7`tJpCG?HIRNTXoHqIKP+!S$~d0Q&ihS9AYK89CQYO1JTjl zG&~(n?ovnI#V=D=5-nnLlVaY;T@bFt#(Fxjug)B&@XT4%hEnt zh$#cl;}ivp3O}&o%^~s~R&ZS*2 z+`awe_*t0o*$b=E-8Bq8=qVFQd4R*@(F;yb^Al*>+=bgDHT91lvF}uQAvd%Yr@^PV zf-1t0QjbC@l^}{NhG)WRet(j~oQ9|m*uRAOcsXENIwj@`#p-P1x4D;lBbAFZeCiCk z9yD8MqSFMZ3%t$2HgYi3o6KN`k2&J$p8a;C4&9Ja-Owd$X!YeRVqe3f<)@c=#+`6_ zCdonK?qRw%EpOOR#?C3IU^FXQ8DEfMlm(VZo6N)xLHoH)EQwNo6SXLZtL%oVPazn5 zQ1$Fa@UZQ?0Ck5uy}ox6hrn$C7lf0iCpbw+yaj^aMteGzce$!jQ`gSK*s0TfVWIDZ$%pfT=^I;xE+(3)#Vf{IQRvu|?~NI%Gho zZIRvy7f%_4Z(iC#9c+LJZI)?}JZdVtnuh!fT<0`qR3I9oNpvHKHK5tnPM^T9wWrCB z9vkg^C!zWD{22P>0d9UR;LIT#stJ64DZ=>JvYeg7(a|@V{7S zeQifu?QH$)*+?U=hSM}HK*qpa8MwV0%{_mthO;s@Bt|V|_2O24{8<8#e+K0OHY1&( z0yb=@)q!d7!xp?{zr;>sjs|4tt$7Nu9)=zOcZIk+C$9G0l|6N9O2^m!9x6>8nu&R% zrZr;}Wb_58I1w&UT_G=*IL&N3*E&3xx**80D!oVrcu}pn1t{E0aJMWC=BE^J7&4h1 z6?kl%nC@%@ak4cSl!#{DjVM$2p)B>h_;IryZ>tcYrOIrfWfinZB(U`pJA-ayc8S1{ zZW^nuMwp;uAf!j<~gOXc>UT zH1Lr?$Q{mqKa!hxWYo5sppTcV<;4oi*QY$+jCmSl3@Nu3_bi^x#2Hi-m+VpVROo_JFs-0p#Re&I<5pb-4fD z_dW=c70U+u#L`py&;Z}*Bd$}uwvjcxIDmIom{DY>(>?C-n>+J0;8K0wz6<){?+muU zyD<7&p=w1)hdnb z`}s>=BXP-(xdvi?91%^IPxeGCWE=Zn+GB8wMytQT4hKUpi0X~V{M$53x5CF?Zs+{f zCUA>p3l>ds!V4og9~k929($k^X1ic*dGU$|*6W5Kr0bFLO6Oy1$+`(Hk}$fd-F0$J z@=*GA`nd#sPj;mIr|U<&8sH6T@ehdkJ6~2N%}8MGdjy3ep*n*!HVJM)B)RE**Ezz8 zcvqA}RdVEP>wReAIYx0mILe}-j3|fS9GO1#9$y-fc%JoR^w(BpKKieLVc(gefQ4%J zX);gk2xPg6TpQfOcE)+x8D6K(i?M4JhAQB1^xacc?-F!4OWLRk%*OD1evzxWhU(8 zKYC3}4sH_6z-{lZNa{ER5wCVn=z82Ey3g950?MTpMzBhy%+hbqANZ*f#mlRn{q$%O zU9-E}&#uX}l7mOblT4mwvAr|0ZQ1n ziNk3Gc*!O87u=>c0s6v0 zW<8*P{sVhQgft~xDG@EGBIaLra77=)mkVbRB zom;x71EK%H+k3{t^?vWdBLqn>5p}fDyXYmlh#n+FFOw)iL_|b%BZMHjAkh;&%0!Re zTcQ(X)G>OUQ3qqb&-N?l{Lgv+oEP))ZtrdHd#(Gr*SfB2kr8eJZA7m?3xM@n2=v_m z1^$sd7*7Y}7r@0LVUh4r4d{in66Rs10G%AjhMiokmsFuZOAUV65QlT{29n7Ldk5S!S|&7S^_Vtj+y_cd_1>l&L8;M?(M%i6!Dt-(TCu`O35hDXbZN5xC0{FaxD?WR291HKXM4}phtOpP9K)AliLDj|@5g|ZcKWe?^ z^Q~qbOLp>ih5Bwdck2x@2)|n z9))I&>OZq&&+8gzo1ZApZ}n;SDVY6Yf4>Kg0v3yq6|m$k0R~hE7C57pz)lt?5KqSg z1PawWx!;+&dvRlmJNib?6lDF-{y}ivGobsY(?XBq?QCNM*NjTdEUZ5HTJQ^g=Eg-- zAKZUaj*&Qrbx3DZ|MH8R>^q>VUH*w6eJIuWCA%t+Nvj=wrvj%I8# zQ3|G*J{kGM+$n4-_=iCrZY8g)$Rq>trJbMPLk zKCtR{T3^=WuBTiRCCNau_)TR6bpw_G@Y^*a;&&aG0vh~>FitBHLOsOxQxqD3nW|7} zAF~s)%CROjd-xArn!e^FQccK8%W(xlzH*IV|I`W6Hv9{b?+lb^)&b&$W^b?Zot6=4 z*+jX!s+`(GOtQww&$dIz$jaWn1wzQ_MC&CMeHC{I!n|z_#&GF4wF8G()z`Jrd)(Ba zbMMpdN5L!n`h$xtt`kK$k-v33Zpv*z+BhT1bH3JgD;mj2hP~Dgdgk-dsQK@4 zHJ<}A{Z7FY zkT_%mobeh?Y~8vWQ@o&$Rc4j9`fAyO{4Ky4oSguvd&TaTLFL-Pm4HsLm2vRmNdCMO zB7fVN7+BSvY0`xstC=ae^i8c4_)W<}WzaR6q=ufB(!F@-#N8Uha5hNu1fTz>`W<^; z9?uld*aCn{6GE62nibf6#qd_LXH&MAbrU8?Sg0aPoM`++^4LCzZun!`U`!d zKV{L@r8QP;zlS{@Rb`{bq@VayV)#Bio7$J2k+w>Abg$x^4a~HEeq9fmS@&9-`2BE@ z_}-ggTDik~?Za_TqO`=8TbDn)gl31M&j-u?4ph!s2vg5K=v|GpWSkF)duOK)3Y<=z zjBPjCBDH%Bif5U+g)KytXJYv}3!Os0nmU%LA{2EQHyV$%DnPgPla<<6nQt_ojw z_P*6uC!>Cab@pocL}b3X(gV)?lVwXi5yKCek86 zET;%j*XszkK3-9df^cd}3gC}HxAmb0_5PjQ;cNaWjE8woiJsOgXws=dcIQ^6)+qy> zFrr;d)$l54bh#-3UhM*R)f2eU|K4<-_5S+L=iGkJgO$IyUP#-OdA(v}x$VFbR3j|e zEyoM7kEd#6WHDR)T&XzLRN1H)t0!5}Z|9fjpLQxbeQMlXhbt21s80|sFXM6fMz;DU zS5W(y2VxT{l3c5q$aUF27~M54Du66X@zul0<>XrjoRpg-~t zEeG?`3ZoewS8C0R9o&*oE}Kh7iLGEO zFTxEgV48Gk^vK_86s3g|Vq{rV_xcbS;GNhRbWbj@CBiNnn$=G%CJauczcM+Vz5@5m8y?`LBdu?+srX(tX(i9YLHtpAI(KD=j#4nl zGy6%}9+F{3x=Xy(rgVuZt5uS~xn9#^ZIzH(2&q>p$51jvCucY3=? zx`i)8m`AN?Y>lyXOPO(9+ZU`FUOG}`21wDW>IK8B#0R&;jM7s{Bx&OP08nTL0K+RpUK1DU0!)sx z?R+q<_lcRYmP>cWzMtpId7{qIddObiwl}9v&tEt$_}fqwnKlPrjT1yR#cs( zN5#${Ta!TwHkT=H>;CR#>tmzLEvizYCjHffy1PK9*^zGIs$#r(797sz%!S! zd7sZO_8kJI*~5v*Efgeg@5U?1twVKj7f&Yc9^2dph((WoC9dsxiwak=RMBkPEbg2Z z+q(P%V!+D?zj6q+o)@MeQPg4fGTs8r{$zkd6&2U=X=kHoq%A$6zqvkf$T^>8cJBM$ z!3rokxo=KPyGP6Rdy^=Dwpus5pg6&Qfh`+aqBcAW&eah!+*!Ffnfk_dZcb-y&bncT zs`LGaekABr#d5z0*a{AzQQ6mg&_Bfi3)FGdsiuyN4^BLM~&+q~Lc zMdMOeng_-Z#z(}&*+qzgs|qa!hV)-9Z(O3ub6H>#o1I%Id3b8@iNnM==9@Y7kY5JC zdGPz*LJlD6NE~qPQ|p6cNWp;Cq!}lGW=62$6~=1@?~|h%v(2#xX~(cy7idxlMmRMso1x! zB5$@RbvsVh6m&Smw5ZwauTw#2SwcKR+gJR9!Bzi}B;iic{`(-j;;+ANzt|_9TO>|0 zBqc}ua?gf##}pD5cw`)@@B%xoMu09Laan=lh{>*atWf_^TaF8{0K>@^4i~?_brD@g z3!5=Cw|-6VrB&{?@q<9+L%Bs#*r+nci!_ywPN4>s#ZcftoowRzZ()3I;HT>}+8%)LWESo^P*d=;Sdn|gf1dj>SxX=v%RQI<~K;tlu zs`QcV7QrEWP2-NQ5My-mP0bt1PgjmW*rbHRVZ4-K7vh{s9q*e00P#eV zBixY$S@4C8{?V+Fw@uDHP(b~{4dM|rr0onSdpKuD3a^IOT?_GfY)?u?P)GwEgJ+CU zsgZcei#zRDD;ww>8feR_H%;2oIn~_J>ybx+o{Ui<-=@IB5U7qjeLR{~>J2u8k zjc~mFIVsKvaQ@uF9iGJE^p`KKmV>^PJNrFp$c+77A)o$|2BJMCQ3^I}OxOu5(OVMj zHDvJlxXcqCwycvgAtDVLEg?6ef1eBqrOa_){l$L1&c*WQLN(wu)+xM&bHSMnl18aK zWvKmkMJ#VnLD7{L-E1&I3<4eTDF%ZSgZ?ZPb_u2TJynJ@f6zuL_NzX5nBf@fbFC%n z@_P`oD}f#7j&il3qHAs%rM62UA0E8>m_{J$mr#Zu6IKX{R9F^fh}ZKdp0&9j3t94) zOLhPq`zKf!+>i zxaD3lhNjDw^{=UrzOgmK9W9!SGi3)MrjNbq$wOpi{;PJlSu1x+oVvy27}0BFU|uRG z=dAi?BaaN0w>jVtUYt1groc)x&gxNMwAmYeM>!?6Vb12zlczX~Wds;^b`03oCh1q# z*7=0Yy*{Xl4#;`OI_SIe52zZ}<;$1&;P39Tsoi~%&ky*A)sp#$=cY=6hM-J}h5$}r zR(`+i%ibL8!)9J!p28cE(+}(j*OUU~N64}RoN)?h3j)t}9nv&LIo}cf<}^Y7CD9|+ zNTlcJMQ~{tfn$ut^Q|})6G*^c3}_EcQ08m3RKNPBSE70AZ*rG>@AD2 zCM6H|f@zoW=I@2^Nm9|#YHx-LlQ!gtSq*g7j#Q9_y_1$@H9dvCZp9W{q0%K*S`2B; zZC=*JM~X}65IJ_X&I@zA0${;Jqdw83;OySgRjDr09M zfn&0AmxjaT-zAquOrH7=ZG2Ra%?MLQH~UHZlqX=ljgn4$>#_6QVby76m1oMP%bbaZ z(sop;D-^ZMIVWvg_n`Nn$g8W8asKzJ7Yi*naqlr!y^nl`%`VtJdK6g=zB%mma0}No zv+G0zxz4IQ2Y+he#7@@z*vR5@ezg+5xEnwJL-=8i1V{9bVDhu~WFhyq6XL!DmPVrC z+1K%J7UTit>*!sn3oF;6J%_AyqWig|T+s&}?_u6XW<%N4S2jGBV#GvOD417 zu`mGTbw0rVYvWY;_r?j+z@;y*<-1%cPvekdJqt>vcyc;f`dG*<2?&TQ;WM|uF(@fk zRoN7%dP-x#gfJyN!WodiwS#^`sqktjN`)@jEUs!9(PH1n8txpIOVs;+#8`upzADeT8<|bZihAn-J zJGwO5`mF-CS>V2F@=B!5d6o664p-Dp5Lo~lHnXs6p5gJ|YTu(-5#?uI+;yt_Pg^^F z4e1O9mR-5K{b{W>>lvoS8TQ5iTNy?F^=O&++*%57ME_(3{T4{?MKEN0s3_osHh9 zYwUsbZ9|gLOQtpd>g8Y2y#D>O-tSVm-H$RP*=E|2+4dBPsZZv7j%37;8Wu_sjXs85 z@4wK%Yv1kmDx$rkeXWOuX!F!{TIG3MbQTY`FbW|BrD?#xn~*tn`z=6D+#;R;gTk-yk;)-6 z0KP#_fub}&@%{^;Ho)RA>Krf~dF2%-+SS7xxyxl0v6nzE@g%eHW)bnCQc!a<;eXlG zUa@lI7NbY>y)cE{udOt=$CWe1%+W>zy)H{cldZC@$6OW@yHaj~`$iK5w`Y(&V}l$J zWR64H*^5)OVTa3I$3(;Ml&Y$Gyr$!2Z}6Asge<>9RTlRhe|M>D29nWkE_J%-1^XX~ z#Ti6Nv(l%mKErLTkwK(bh^HlPOW9?=zXM6r}7=F z^g@&{qDG*M`A?SBn{RhiwH?kP`WaV_hFObI~i6_ zi0F|Xy!vKg%=Hb~7Cl0?Iv9w(5K@zFZ4YEOq)lK*@qV;r_U7vi3d!l%@J3ObnKA&S zzbSOWaYDeGk1Wq(9Ali6OMXmy-Q;mFXy-ae890~$9zzUj(sfZa5qIqpA&GmBM*H$D z3=SvQl5;VYH_C%}d@L1w@=^?U6})GeQsgY?A^|%&?u(n}X1$9{$_%~Be{j-JQY+V~ zN3uyg7t?KP_W`*>Oc#YcrUXiCg+|9OAzxQolxf^r|J226hKX!irB#G4r!}?q0N>YJ!X_ zu^JTRc~6T0KY(59DZEfN34A0I)rzNU2NWzLWbX-`ria4H67|k`w-qck6X_HzQip_z zHnb_~S?j=-=V`^8@{Zwg4k>>6F=a7ncT8`YoAOWb@!iZJO4KR2qGkGucB{l@vEuv@ zym4M`zD2Nzt|Xq?5e|}T-#$_IlgY7GY^$=>XawxKUYPpWSw$qe(U^wwzD96}|5P;; z34Y@-6?JUL7N9$#$hx%cPQCDOO2#`X=J`*GnxW&lM$aCZ!z94{+Y&I5z`25{;3Lf? z0F%T6upjUPG9nbZsY0!&bXk(?o3P_aJEE)XlU+_Fw+XO@rvBqsc3)iz%7`~y@8%A2 z9R89%5%c@Wvv9vSzr$(H#ARnxh-``rBPXZ8^XGgZkbDxX;CwwSKm{?(f?WHUzh$i1 zNWCw@^)=Zu<67I&^AeW@QcPC5FWo{WD(2?c9!XXIVGgcH`H)u$XfI|h?&|9~9Mna-ZKK2VnNS??fFU{<)gCvW`d4&+TbHF+7YXcX!4dRQ^(&z*?rueTX1 z|MarEz0x{0%nxW#BO?oi4#G{Q` zJ6o?S(8I}a9$zvN-}c>5!R>+o0WrhuZ-9wYBQPnzM1E9Hy>jziiBv^0$q~u!ODddl z@=2!CMjCxWPCGXfee$ycbuM?Go!v3I=RFelgKzZujpSTQNo?9)UhE#ch03Y=WE{18 zaK62+XBF^@`am|(hnNeR3G$Qnh3=Y=jN@r1A5iNESg^k%&;R+|ggfIMaiVJsaEe7> z7B6p|w_cty0JId_8hD)~;AXS##-fW!?xzSn| zVCb=`vl5ubj~^_2wcE8j{1BaC+=}uF-~kv@Hcmh_d)p9$aKsd$QT;E zo^_nNq=J;}WD~|p7eq~(Azx0pt+tEwjKy)V3kKvZWrZ%r0`@G`|JhO;eadCiJDIv3 zbLnu+Jp2(QVg8vNu(!c?CRlJgJT>_o4>(~@ZN+$cFA!tGE?1_!7wAS_y(+Lf(3i=+ z)mX8mN7pY!gaz;X0p8h53|fOWQCMFpk=E$GXyU(v-SCZ|J2Li04i2?UmL@>WYvs%N z+=v*Fz_c2uX%Y@t>Ako}Rmb#%=I97*Sn{S8DCP*qIJvO!v~5t0Na9AzaZdNKL=oE! zcfS@_t40j2mU_~XL1urkI1wKJiO#|F-Ob=C@iveH5EBa&;d*egjYq@G-BRyvbL9fG zMCu~;(2-E&k7*pZLx$ne+>DQvT3vluXt$K*TmFsC_}}es$dBp99uZlmcK7i%;B1!r zM7}4w+ zPyR-prkVoHR|X35!@mk~g?|bXv5LrxOn;oFL6xs#YG)D-*m#otlI}sy4tjgiHJ2Lc zdq_s0qojq0x|aBf6LrwnT7KiIu->SauW05NS!+yYYsCJ}gqp~onNHD^wu~+&wX7RT za{bE3pHSeK)|>WNp7(Vajq)&oz)wYGVGX{_?;*mWuUVE7rqe(Jm%rG*Y$?iMPS-=i`Giz=D4?y&sJ;-@{#MW#=<-zu0>rvWKC%zG*O zoCXhY07LpzaA}~k{RclARVoZhn2^>G$Y2{xc%|oV!l(C_BRRExPie+lED(RmIb+G~ z!^y|wavw!RfJ^{u)g7U)z@@ydE_F37AmrD-F6Dp1TlckveLM9QZTNzcaL+-Qf=QbB z?_wn)4)gbmE=P#UZ554Csj{1Y%ibzU^y=4oOGuf%+q}uZeyxk2SPtYgZ5jp$?LY38 zxl7=6NB5Wnt~aIK*Gx=Sut;&y<5#*t^f_Vn^)cN^pf6vqdqfKbCVu>An8A6}b&3X_ z*XwYC{mlvc`k-)N$zkvDVi_oa8ul31(t%a6hkmgPdRb1RyE*S+E`Pzdgqj zwoU#4{U^3z|Bda!BN1agtb6$G@h#HWL!Ai@9`yo~krg^em53`OxF!;4ctJHgz}$UG zNf9#NUFs@eT=T^@uSiLPSb%HJZ)yaFDQ}_yC~@$QDi{l(B1L1>V37JOcZMe`$==o8 z`9pHZBwZwJQc~C>GTz|3SJr@-)LmDS(SYZziRO9QYrsKo=5z3pCT+4(j~a_NX_Ld$ zCZQfRPXoIh(09)G{gBmr+8_2I8gb5O{(kzWpvyR(78<1vN9W9ZzRASrBLSA zqg7j1Md8ks9bSi&y)(UA?ZnyCuzym5X_7V8BK=fic=O(q&x{jtWZ|HU=;O63-}|N{ zlzXo`Re&0n*tvFcDlI6d&z7Vf`2a`q( zWoPz&S8rxs1w^jb&=PbaD74-zc~Xceo4;&gkT)oHp@^j`OtKe#ddkkS-10ued5=BT z*<2AgQa|-!Kav8Sqm&iYrF%O>X}jHMlXNHbKg8&Xx07I$!n}coHEYoRT`&Se@(>2Mg}&C>O4l+0dws zNijNnQXeaon$B|@N&yyi#bzQcMH*=Ady`hLO0|1AbaXu3g-JZPA1trhc^zzpC`e}Ej-k_({xBI4T1V)iujQdUDPR!2xk zjf=#nOK>3U3;HK0(Vqx*jp>pjb(tZ21@~$}A`u-Ff=T5r&|{rdY;IkcKR{2m0B9)e zFD8``8CXcTRi@Gr%yAi!^97d;Xd|9vJJGuUKpM#$^lrW2^^NX8e_JbzK(``YAA9Xs6b3Y&!>l>kD*0c`po5MXr+ zJ4ORmx7J|bRD345AP!uvd65wF;zA3h(yl~|>M8apwy>UuE%s#rIl0iK#%%Y2}SRIyf_uIt-Q;!8TUWhhFU-ZhZobqR)qZqpEGS@VNFn5;o zwPO2%cCME?qId5A`0Mqp+c(-2{Y~ZZ>KNmnfJFNvB(jazI5-i${{yte->v1TC{sH& zXZ?po>_NB=PXaK92_g(wvECc>RKZjz0!T~!BakaFPT(u+X;u?DW8H}zX@BmfF?Toj zGM4zNS>4K>E*2JvL@M%btDI{sH!e$5R_a{g$G>N<_{4#`woq&*%IUqL7Q%8{nHNi| z%+m5pomnweT;rtdy{cUN!IP&R)4=ho)9Yw)7f7zW`F0KsTy(baD38mqgO`(2T4M&q4p*IAtbhWeJR9UHIOzzm>6UI}qvxzU zqZH9uH_R@m3x2@s^NG+p1$gd7!ElaXKx{G6SpvTzn}_)W^HYFQI~88(GC9{bt{LCI z{uzI$@-XAI96AcLy>V4aEsb&QW+Dw!e7&8vmRc~#xv#x{7af-21R;v(8#EcmyQB0q zPlS|Ix{y47=L%oGR)fjE25S5%wpb$~%307i00XBC)CVly#0C;kH zgl9PnRTp;TPKML$)x*q2mde}+YkLjUJA{c2WvQr8u^IM^!3?&?X<(*=gClWwB|?If^Ks zyYtfurXlo`m^;jrd&!IsKs)r2SjiouElF#OMaxzCwZ-dcl1V=S0;uQN`3Tv1W}uoc z@C8*0;Zx850gY`6&GkH!s$oB>5xiI@J^fhJly*Z*GO@C@+O$#la73Wci>zOqgZL%PX*e}&W za@OemayXb4>&mykcc#PY(+_A)H0aXHwvo!Av_{)$<=F+j0z%9S)_EL#TSRseu6Lmk z$`ol&qM!^uemXWG-+A+I2m=PfjY+Dk(*Mbq|8)lc?_X&8BIF;?N-3_D&`}3H7n@>+ zIR#kY)G*t|*yNu4GF&B?YSFeR%gU)STtz&R{Wr-+#dtrDcvqSZ7%_qF&$0qt9k#i) zo<9(6pttQU%dDpG?%SLeg#8_9uyW2-BP92~cfK-`g!dbdweC><%SLhnA&INUc;CmR zdN_I)4SG5jSt35)b;DRJ_j8adKE0!To^1)jpCU#OGmQl8xUkmkXuzUqd#DwabOk)s zsQ{i`fEIynU!E;FA7|M4?%XS+*D2ox6Mw6P?zTgfB(y6L*z_!;R$Ecea6ldel?ro+~r+KB-An*C)`Do-VrP4owcc{oI@?t4L*8gta@Lz?I6X5|Rn9yMl#IO#G z$!t@rYnW(L<#v+{QnQ*{!|YC{qSks?TMud)quqXt0QlnJk1+edvTMCscKpOCm+A18 zaJ(Tvn+F!?(bw%YbyzM890YMzzJW=eeDBCSZ?zE4kZ2I(8q-!x#zIt$zSbmUietr; zQyLuzD05nwH$2>$tb9S=p2HDwFoe!P82F+AgJtlHD?zU?oRA7T*B5aL8igKMUK!HB zxPS*O3%rL>M;Qj=6_#(sxON6{e(dr->7!;-D9WdA?7Zdyz2Es7vcpVghUV*V|9CzX zWl**Zrcp!9UR2L*^2~|*u6Q)v0HeA=0gvR(wxAz@#c>^otZMi zjU!B%#Q&C=&cCdJ44;Y>dJNYD%KG{iZumK6!@b;|J$v;d+%@unM^2x~^(*hG$>6)A z)^&Jc!|OmcSwt=3A)Qwu2OSsB>#L=*GD%uWO?}+4=F<`l3Gu(RNT~ZbgL%H!r(*vB zh1$Q(fL_(lR%KQt}>RPG|bW2E=}MG8~&`zwC;P;Ske2IORQ$mo-3}-hxm;^MG zoKg&hJb>~jbH%tETKc%g2fC|y9YpHuPASulqb__lUL&R^}fJ z+`SOA*7v|z>B!RP2v<#D%HD5Z(lw^<(WK7m<~}pCT*7OX{{=ZW9XJoaz1(e!t~D2 zSofPcYK>pwLpK|*kq?0nr9DxFJ7?%mFxHK_hSfYKx3J^^?>7&psDB%#w1-^RJS7xaZndGWKM$vVR>VSMHnyMb%J$3R9_t~8ITSrcY!=hR*K}D4<34N(~ zVmN)+dH2+t;!ux5q9P$OfmkQZK1&tZ#HRds6DDFd2l!j57cb&L<4p|qf$n!p3O;0- z7n`qp%{_dYbg=GA6qnNnL9ZL)oZyl0qiaCC6XyWHe}i}e{;V-F4azuMUCHc&9Mg!d zK$dnq9UxQHHX*rs^esOucm|G&t;3u?oZM5?ein6T>=T0QX&=y|$J4*o=BdjfQzg46 z$F`_?J%IVb+-OAOjh*BpMu4(aU3d3yxIi&+ zI4jLjAWKxkoh;Oy)?d!7`8w0Qn*%UbkAadiu_;j0zIIjE9u`G1ZMhJ{aXE%kiC^{t zs_%B!7CjDwY>?&q?lz5C4YQ1BV+WbZv@`{Ox2@dPy07HAX&AFe)MQ_VfACj2o2s2O z*RiNIn^4c;oX*Iv{&UzM_o!S__cEQpVe`Ut1O|8p$yCbSnQNXfB2R5}Gv&S{+XUzq z#3y47*%u6LkIoLxB=QF;c@JymO+k#YE3aNcPvVLZDXx29q#5dY9l8~a^KFtF8TQXT zOGr0+2Knkh9rAnN_02Q;&PoO#tH76D!(nWIf0|27h5)aK>1HtgI~R-vLH>frO5}w}|300dtV$_1?6< zCm)?i3kw&m`@LEID0jt&tE3?_^6(e*70FwWC=6OJE9^`J|X5&Z2P ztHvsHwAaFcf$i*~d$0y+$mVD!w`fn{%DI{@quraH!9`t(9`Etpfz7Jdfnu()hf-_1 z+tXP+ZjF@?8UAi>A>Ewf-bY#_X60s-*pl{BTX~#!v9n(Qw$occ2Ox4Me`ORi@^@k! zehGj{=|Jg=+t&J*jm!RU8N;f}Gy%o3UZ8^es`nu}dI}Ivbt>TXSfv=_E?ckO<5Ww~ z0Q6?fKB%Rl2=_(9&BN~#$-9Vcj+=RztDh5w{D2`A^7&m;ewp)w6P5ZNQp4PdechSU ziB}8NkUMQFh|=274Qt;a>M9lA=cVWKN3&bUG{Gd5tHMZ)wVksOK^-(mP_CI0eKxjJ zb}U2}|K-)#8vlnMz9hL~g8tN7R5uv;;3&Q}Fw=Cv{ZTKGAS9)v;dr@yv+qOffP;9$ z&lnTNj0#o*5JWNK8d2QEv$=tPK=h^%-h}T_9Z->5cZw5d+IF*!QC`Qxy8;;3E`k^~ zu%#Ilfoic3nOv}Xm%!;9YJhnHj{R!zr{8-&3l3cWUIBR&i%iT#gjc z317Q^h0vIuvyMf&QVq2y z!MY3oim$5}`=3AXkYUV}q&40e>uRmXUE1(@G?a^;!zAj*;WP-u8cf%NoYQylq8u!( zRP(qM$(6z@xY4#EkzI4 zmVl9Vs%zE20Jq-LhUT)CLVMD5N3QqMvvb}2k^Y%cXZ9Nk84mCJKo>v$ujjhfWOdT` zD-9ev9>J3+F0v!uUlFlT=z$E>R7E433z|;Ry&>)aftbC0t+Dkeq1A-Dc6Y!jnh)_` z2`uXXRxJ-!QE=VLo_^!|6%Tvm7(bg+;}ubXWIPN0$trRGefG0WTL5WN>|;dVXGGjQ!#vgOR4eAs}I}eGO6?`X7%<0gf;p zbX|hpnX}Tx4AX!0sZNd1U>$Vn9#cP$kA*{olXrLo~{(jj;^1{6{tvq?%7y7C? zWomG?@%fQZBLvdp86Ac5+dp}OOQ4(>RnN1s){OmKrR7g<0a6sBXbbi`AUeJl%z2so zs;~}+`ldlKrp4OjE&lLF*9}@VuIo?bV}2KsHH1!^dhbF!qT~@H$xGYAgZMS>e;$6HtS*Vqm+_vd1exgY33lNo#Wi|_e z1Q?Ntiz#*uVyO##Ts5Kng>c5I))==O@vWkfUXyU*~h*!v@=n|Bk+58#H{v%=WjijQcU?kh!iUDBg1IYTvPR7zti~pXyzfjFQg+(KK7=0HKka960CppGD5Z>!a4+|P z4#M{`y4=Yy$~&HWO!&@brBp&VhjmH8nARH!k7MbNqC2}-0S8ALDuzd|8DXkKr>lpy z?H7`G7OZRt-pm)_^Ih&`aILF(S&Bw--rVo*%DiCGIlqjPJ))yB;FJbsC(Ncw!aIyn zv+h9d`@|Qo^=p^HfxTAVA(%8UEZ{H9nJ@vpfmcKSnddUWbaZ+tm1-ibw7g2>wA|j4tokGS?ejraaW2ze<%C~P{{|n?uY`o zMI}Pw0o7Vqs@LocS;Ugy|K)|~JK3mxKd=tpCi+}HC)@vY*Bn*OHL&?f{YjKFBGm(Ns2(xKTboI+8$u1@qZxrb4$oFp0o$J6L zc>c@mCO3O5nfgS{g}KxpO64t~_4r|yfi*re>L0l@DO?amq9nj&t?J5u{D1!t5kcbk z=h!CL9qDZmQHyl_%AbvizxhRe5}D7HqZ1N)Au6daF8o^W1<7ruHo&^GX$!u&2~&C$ z8G6zm_kN}68z^td3B~um08L{~dsU#D$ob5<^W$#(&)uze28L974F3!W_7ea<_C3qzyKbY|I%W({90pvspiQHmw9xk zV1bU2()51J8QXH>i$fSvLBIKL%Pm~u_`}-7wT(Op^6wD4V#QZoR5%kpOb*bELcPJO zvnpB?!x!~{Lwj~5ybEy$hp?QKa;ILD`Njh9fm~yKTPHc@ewcCLe1TI^dCPr7zKWfV zJO)=~Gtn|EI!)iZLkL<-8j#?+KI|C`9IVTOmD|3*|1VkQ-+uwF>#hhE)}6-J6?vp| z>q+Xv%AZk+ff=B&s_*r}27XQ;fSKwEyBRcz3|uYjxpmR_$%U(?Ih?-kb6m=!3eH+^ z1tZtywkpeNT@NDSk#)BiR^uqk>f;RWq-OgNc`p~TB_6kkRj?T)PyjfHJNdxcU>1EuD?KqcUZ`IG?x6YlQMA1r z(B#`T8#M0ZX*y6>bf+Dy(9$w0Eu|u7KjIo|{&b=@4h*`LZ~-PfjrF@YQtW#QBL3WV z&NSnjj{pS*Qi1|!HAHBMmf$sUU6lUqeg^MdP=9hA@8%s!)d>(P9@yH_dA1FI9Uf}| z1P5V*01Yg>S&wiI!yw*X)*a3MiIMH_`yeyc@92E`cvzy`lsBn=nt5D-8LE568>~Eo zQRx=(+dg^ox5>)~XHaTDaDX5a~5APfX zzZfI*EwESXD=*F4R@c#wlAPtoiK9MylL+*d3$2nG0X8qCg(Pe_fC<-PWnI@k4KB4i zEjDbK`DEg$Ow7B)!Be!OgH2BKYwvXoLJ^mbE$jtmElJM?%35U`R8=*mi~-R}EBxw- zcpor~cXD{greHy(OG(ez%0>T!-`jY_iy$>F9muV0YXRw1IK$iRLCIzB$mI!<{JKPa z&E>au?#gN&ND6@;snHTm;`q?saUxA-`ORyM02@$hlO<(Jj(@95V1sBM<~Oxw7$P-0 zXw`kLl$Gu#GWcg+(^L)+Qe86~0U5X+AptoBAZI5CPAEb!e@rH&VnU87JT&(tX#B6w zA4B>OstCc$!s?4b!==e8(o8pzfp*mAJiYo_;iv(R9U>{B0|xwESOok?=7q{BzzPoo za0nk>6U*>vee!6SdLbR_!emH$K(t2L6_OX53k<$43sk?L-kgdo_DOo|?_%1c%@xKc z6y`*s^Tt3&>Fkl z@KodOUMioI>GYM6R^g$KL)Ks-o|W7Q7W2&@Vgzo4td1}Nr)@Gu4KvSf1hGxP^lQUs z_?$wSSDc0y_yFcsD zb_Z!W&i^9bl8{4?$1rIYo~-%$ zx29N-b&#~^E-Tix8>!WGaY>{8*Kz#i+tnCat0jnDPa?gY=kVbBqjH9mr{nITK@uC@ z^tA_i*T1G%MbZXT2t-cY@pp&^2p=W-O;xzzG8^kz9lDyWcAn}|Webz1G3Lc5j_f0( z`f)w`5%21LT0?1U4LDz~3B3Cpz!ojuCw;p9(S6YbL-<;x8{bvwGU{V|zdIQ8 z)Y9tT90)_{uF^YlXq$lnVA$8h)Mjt!Zb20$CDP3k66^EwY3u_flN4!^AiH4cfAvj( zdK4}Wt<_`5)Q%Ok`8ihewR)CS8MU((ML_nfp=n=L_1D-A8re?$+)xbIH%P$jfE5frXZ7Fl#Wjo8NFX z81Ye$#85B(7?^GHG-wh=t*EwiJaV*8Ws^?h$DL*s(YvpFKL_Ab9|k2KX5;U=|0|?m zX;&>E+1(WQa1LLKXTApjj$f6yb(ZDVaaEd6+Kz z+qt8}*IvraX%+9=ezMiM5Z;_hb7~)yP`Z!Q^W{l#NpN7~fmD{odDzxn6)%LHrQzQ=zSk?L9yZ&g?6)&X8E!Pf9iUM(zA+ zkgNvI1zXZ9fG(pKx_pIxTwUJ~i?s63pcCAo%_1Xl;%wQunmXFxwN{L!>HO(3IX0(T zH}d-r;}Erj>2u{^qO?oFoNGkwjC-!RflNQjZ)$55aO- z3gj?7)b;6~b&065J-c7=n)O0V>z;w6&crJKMZkLTN-Fbmq7?wnH_!D>^l@R!V91PX ztulAxh3n>;0Ov1{iw^F^WIH+bm*{O6{nX#Z3erqp3O0cWo?1YE88l2QDb~@~=P*ZB zE-SjO3I&w#{~qyyUaTRDsezN=?jYT+J$CmFd@Is~cl>S>-Gz9G|Alv5S;Scb;rFlG z!~b=Avy*|V0ASf1M_TDf$#^eYg{x(YP`ql4tJQ|a1emBo-xB^P%TG4BF72ul?m)W# z4u-Xby&~`ID{X9f)fJn(YjRk(V^OeR>BDEyP+yxOh1BK-K3ps!YS&UH~1!Q z7`@;94+sM6i8RtST$+FL$A#aX+~%};>VBps+_lb^b|KNK45)PUWllQL^sOSRO$l1w zf`i0Ammf^$vY)O{t5UnbS(MqQx9CqrmlYcGB0InEb#e#8Z&AFJJ`&wk!}0-JY`CH* z^y50cEK^6n)D(4GCW#$ajpqmrA0L8ktwnZxXU~gyT6pJa;_{X^buX=J$~z?x-7$1G zArDp;rU>?_txR_%Nn`#B>I<8HhGZV+VGM%mg0L{0bt|>XhATj=&%gR!_Nc{f);AX? z-jj-tgP#5Rdz2u2O9*_NZ*sFS4wjv5ZPxYo_+!UD)m9~;e}T8=YFmaiP0?4pJS&JU zeJT;VwENNz=!Rk$aKl*2JSPC@mprmge0_5%P!93(GMT`SE9_bjZyssVD3Zy4HCgsH zaX1EBnaoJ?PN7xzO#gEU-BWeq5b%prV7ynaK~w6o#z`(~Q+N-hvUXlJRIpKk&iXta z47kL({C82b2Y6!jPeX$kx0xoBe!?Db#macZ=g%9zQ_|d->V{$J2^>K4VyY;}l|Xj_ z-jys^c+pDgHR~piv`Pw9B&y4n9KpAINSDNQiB&(Jv<)ZPzajhzT}H^9o9%To%WM{ggb5# zaC!OEy?8>I4oLB&&$?Bi-gpRZ_ZgPUI$JAMD^+)bUQ$Ys?{Lfn>c2MKsNIKVIiHfm zN}j}w;uX-hcA3UIIxS?V+}7&Tmeel(a!c@wR|tY3Xg)c{c4eIlDA4G%iz77g{R5hD z4aNbOkq6eJa>CF6v|h?op=vnKlkcO2Q!l?w1h6#No6U{9P~gO z2O~5C2fvYS5D+Jw*6@iWK%DGO7zc*0DPz6jVgvXb%QqvrUt3>ex<@w192#!C9YQ|r zhaW}&wO)t)GtM+%N-!{fYBC3GLKyh@4=7v6{o9438V4di3QF4TMfgo+k~On9e;;K8X&Y{?*1F zx?`B~0yrOJCb#5!Ql+AFL|-QS?)KmT8vbKN1d%Lbx8VX;t`lK^*bk0?9(PQgS(^>e z5}mYmf|2)odxAu2Mi5OZyvaN>Gwu@empM|IpJP5n9}Y{qqqTb7gxXqoZODD#7q3nj zJLe$0xAPQ&9Zh(cJ=jD1h=3{Z(7SkI;7+|LmGev3imT@d(6LLLFNaSlxL@A>OtfhV zuhC4qyCjLbDQ8F32-v(@jC%gz7mH4qpolBlhCSN!p7dLbae_bZ@V~WjXPMWr&1YU+ zfc-o~PWsFPmJb7F8>ZkC2yK(_a}Ivu9Kv7)@QD@NF93k{a02o2NT2P1uoEHtGRHqs zx8T0!|Hs{%heQ3p?ZYENWl0jUjG`2wNOnWYl2FOMjFPQs$i5pYn&$m>7&7?2IaQW*Wcc$P3$AH%1EBMZs)$|jWBED;{NV~2eD#=pB z**nC^NIrEJcGP3*U%dYA{Ec?+bFOqYp9hL6F&SGuvoLmiVNfS})AG91{-Qfz{|W}H z?9RMi7kD&lAuscwTbN&%Q=#_Ai8*WWWX1tERzKFs|7Th8eoDuK$qv+`$qo&sNE!9K))0 zkOmrxs$P@3pqz{rr>T3Pc3NQD9LB|JM{8rP@qrLUTrTv(jt$8zXsMhh@L zRT)$2)s)7b<%Fjx??(3O)IN4Rd?Vs4Rr*7R2g24S;)*uGL}L$0A0<*0M%21V23siY z>@4jInwp0qrI0>iBK=rU$M9_*C82kcuH;|1CahD_HXq-w)L^;2(T?fv-1noF)oX=hW~J;;GBU_K1k7OmoV8Y9)bKS-nj<%0N=I6^_jqTX0>12fsO>mF1aA^K+y$_Mk z1zIADVQX=+u0k>AA~oSEl1cxDQE|DmCsoEU4db5I#L>OfV{6IY} zh+o-9aJKDfzpZ?@Br>+Rf2Q*ddPlo=iS6kos7R}p?p#EBvweDm^nE(I{^z_|=40m0 z?+pBcsb{G_F#WMdQ0?*48x$rB0FbgWSUWQ@Ps^&#yb1ia!g83J#_o1jhu`Lv7a()_ zRZK=rZtB!L0=jv5=z}u@bkpuMJV$;Xvog=d$;TXM!{f~kA!^}MlqUyz-jj?$%D<^v z_^-X$y>opo)Z!};%fL?;?(hEalI)XT#}x#KTK7}VqdM%t2pppndjOu>e0tx=bd!M# zOFko=2a7AY|8Y@im>OYJoi8S;C|GMYvgkHMOPQm)`r<7Q+A2%rYl>nQEnpqHaah=z ze-7q&c!^F8;&CJiG~WQj0ZGwa+INVQ%6}GczAE3V@DarlfAiE@Hq-IPw!w$-UVJ%{ z+C+R!jjozJK!K;4c{W%T=*+KQ&P1q+eeaj5xmb16T3bjr$C+W?D&{m7M{a`%%VO-E zKdLtZkVRzMK-+9qF+CH*%Gh;Q=8x^#GhLTJrW5%QAFOmrdltqS00Vhqs|iKoECX3I z&${Vf*33WVtHFv`G9q?A0ksKE&j@08XCpG@44Uu_#ux4^d3`jeE#A4Kbk6Ng_-Kx` zI>V;?tg^#aSzRjlydPr2u&eX1ZI2Qv2xiKkpT!T16^;wG9tR)N?`B zkj*g$YVxby^17&ziHjA{&dNN*dedWqDtDE{786c4ANm-f?^`?kYh5@2Akb|IpLIyk zYpNDGJJUyZ8mQ%uOz3Hj8fuTULRf-O??2*Qh1U3d6`-$tUjNUd!GK4TNqX@YlK6}a z;;7{_>~<8s3rMJN6Ut`_qi13C@YN2diys7&3h`yXw;0YntQ1q@3+pj}K%Z{vVn;?8 zsy-l<`9#X5+hcTO&+$nwhCIA=GePOe?7&VwcDS6TjcVT5Cjn%pV_o;;G~F(=k$r@% zX@)&g^M&_k-7nVNQA1xW4?bNK%SzJ$*zBJ63LCr7ETBG}0kk^_(MCZ@^v%~z8s-%x zbU@0e`5b@Lx@GI|RE!Vi$U!n}u8NFU1_|d&-;nn@h>;hvTi`pJu~_6C>Kb{u zRM)7MaM#QDtC~n<*qSy`%In0ur0a6LbvZG4rtFZ(J>og|D|DrJ<;g)(4^-lb(TLfH z$g~Vb>#I5LvD6zO+HK;OG?cnR0^0k?9jobYJN*8m=q)Th? z?7k$|h1_!kQc1&*D2J%6QHCBu+q%i4+|?mt^YX9@{rQV=n!EQWeHWyCm>iNSd0U^m z$FyYy3=&OfcV0)q)?@%vD|6!eHhhoeHwxbjSVI}?1Abs}@dzD{a%A4Y$HRZ`@otb5GkgW<_^Hs4M2!@caz`7E@;h z6xp*@H%9>qjLTX)@IoCh_b~lNv7?;w*nx_Jey^+)TUS;AHP3~$(dr<>H(%|x-6kHA z7_}9m#nS=RFj**XMD2+Mu>yYbuVh;6orBB&Jvu$UAN{T@%U0zbm1E3YKTP3qytjBR zk5DAh31tt1bck0VAJJbs_h9*X<(l+|7XRqebc>U$G!7q=>Gk=Rg;y@F7v{Q(an*6! zOSO%0jC31j);GwkQy6Y?SfD-aM3+TZFiT_S{j=KC`F61&+p(Am+ykARrdjS)oN}0@HqH(=zd1U#LcAs*cjTa4Lo zY|xiPb23KaAp3oGJ&u!0gx`XV-PZl&bvkbvw?k7t$gBN;K^1@nCA5{+OR7}{xoXHC z&AO7b|MDs<`CNEvrme8h)b_^CHs)vKzBuW^V`Wrfp??3@Q(D-MP_W8E!knVv1drl( zx(XgRx)fZfL~d}UcJW(O$5rdg;QO)(uj`=*Vu&lVjo!!g5U>-;ej zX^0`mf8Gd-3y+V(9b(W6_srF@E>{QnIn=4EODq0Y*HBr$QdDr(gHelxrNA#9u)vy4 zDtp!VW1FzA>!4)R7lTLB6yp=mM;BAv1kI|ZWNe;5wCQ1vgm z`c7S% zO;iz6+_am|amebnz$-U6p^7Ge>cInK^UrCsmZP{AA0<2p@k;d5#xcYSQ}qI})u1{1 zUImYGmQSuE$;Ihq>q1kLkIz23>!7e6v$q_Bh(JZd*?J?(BlCglB|A6?_%Nz!JlH0R zRkqlICiw<0X8g1aD&1m*1brZDwFU_iUz(iqaWN_%I5Qs7HP880^sD+pF;$Bw(@@7Z z2-O>+syjFZ1j{szzRv-hT&~-Pz9WxGE@boam1B@)=~RbM%J00+I?-ae=2$Ln1=vWr zhUx_39@C^EcZ(Fm>PY@s=foLt@i)sI9Jk~yyGKnup+i4)Xhw$#@{wP2JI;J1uO0T3^n@&$w=QCjck&xGi~KXj>xPi zdE^Q5k`t#u;8+RO?*=1B^S4syI9>>b+i_rfXak+1ENO*L6Mncqo+^vryqe zwnDK1b=M*)`f#BeOqcB8ul#Hts3Glz)Ks(*6=)%KmowP=$LTEr6Ff7pT(hQZpk%oG zmX)gAv*(@lK&Ms_ssWcHhc>_`8C0!QB`@!#AryUG6Z0EoeL##n)VPImv#lA=j@g?UOap3Bqw_)U%*v5A>Ip2 zOgWoRFg;HpKo(^fU4J7v&Qf}%{D>&qOu#I5#)5t1oL#r^%$Ok^XFN_ zUVx!!$QQ6PfXNxPu>OrhpiUc_-;GLWyL6P7xq(6OcOF+;fZb#Cab?T;IL&0&5ZEe^ zszPIc5bL%L$y*mYYiP z>Fk8Ft|Gs!^d^uI%yZDw?zCh#Tn0F*SC7rF0EemdQzk=~$*^YDK;HZSRC_48DP# zK5>ACExCZUR(&EUg7cPKPWpH^YzD|B-WhoG<8`B}1(<3wxN0&Q-*Wa{JXa}_zPGsL zSND;`30E^z>&X5L6=<1vHj>#sU0!0UA}&CKUmkJ<6<^WM^-3pt5&a zV&|=0yWRFRMR9)mV0L=?LjMGmiuf{UmZX6Ns>Y`uV|uxY=#?FI$9fo(-%vRSf-ugf zmvenyY_B$4-a<+^Upp-^{*XZ^kZv|xK>RaVVVXU3XhPBg!}Js&QdJLoPCQTIEaBWGkD>CHfUG-VEX*TjV0n$fc_e znA{ndi+bfFGAy!mm65o{0-VrAJMtHjUNuQO3Q8yr>vx1tA86b@Dq~6Zy=xsJbBS|Vo~9XW77f;P zgx%1Z8~YdIpM34Knn0y9M7vgpF7Q99nB{2|?=Jicv4=y(to0ND{Xy-Ag6nA$wewFF zpU&&lFG=h9dtGXQ_=4^D!(_*XxtN`)4w9WHNOpHbu7;+rP1(dfH?VZA7ghFdMI8$e zt|s0YCyzcnYmB+Bo`#(N-52v>?5ML(t53HQz6QUTynb?R@~raxr0&r;rtynPvU+w_a(Fsz{_B0Nf|iDBox{_nzP7@nvTHEj8rW2{N#iwksPYYSWJ#n@73+M* z3Ben!YNByhO&Dw(pqGTVYEI?z>M-z!C;_kJG8rrtREcaR?ns1xh^Aj!5|x&{mB9e| z_fn)luw7HN1=4{5V6bDp1o19g^Y>D@zmAe+Is$n!4|OVCQ_~CA$72+5cWvo;pedfD z-8DxYvaV}_zN}6#?$HlL$nH}B$5nmEMN=E0oIZVjP+Y6fVe_B$x~@k{DD>AB{V!=Y z$r<*m_|Gkp=^Nnm25(9x*Q}7GDz|jrOGsvq^T*H$oxFF%|3$j?MqeG0{Qc+T=>Qwk z1kEBZ1(laLQ*%CPgfYvP5J^CZr}Y5%fB5sx)mDYH_95~*GDAjAONsorF?gFlL1#%E zl!veIgjHHto)l#s%&by64!-eV8dyDYg{3V*V>J}*>p;QPk3Q~$(I6t*X#9)5O7!oD z2%<|kUKILBBbMSQzKFE?uw&60mt-q5htADl|4!rdQJVUxdc73gn1UtMl^!oCBUtIx zh_eW+WSu@^omr?j8x{L0Zwo$sS)6Fl(4xacvH$r*u=NQe_Rjy1j{ZYHA`8!&G{EaZiXD}6X@qldMQYntweY~j8_Ae4zn>NH`u8NAHpv*Yi!RWA-^ z`#fQtx3cQEuCK`T#9k=m7_^Hi-4*xWK~`TZAT<6~o~@X;1? z@?S_r)Fjm?@B{_wGuf<&5BDtKGaHojJaN60VlF!teI z%Dot!pmlswoU@yqsVIZJM+}^yphDf;Mk|B#DaHG0OCd zjzVvM2zrX@kh@YY*YESuG)!l#QrExZe6;(pkp<*91c0s3$znhRjulwmUkop0g{_fT z7qtXFR+q%~&{uNPrw?Ot#iw0@p(m*TYn(9DD5}Djo>*1dtHUYFm9s0p6IP&$Xqe5x zR5|`HTHEJ39$+Ybj*Ow5DJ->hDqJ~p!|T*RI`gwXf?OjIg>%{+9m*+F+n9*->;1Cg zh~EKv`MM%({35#vx_q|9;y0KNxyI|{+-Bhwa5{5 zK01EE*)bnbW8r|?9i<6eN=^`>&th~A3ThA-M{9mUR^gi*z3jqo7n2rr?XR^56mb}_ zu&MogEx-C=RADca#);zrz#ap(Oj=!NV!^6?gyaU`Uylw+rZLqFL{toDbg2B)c^6|m z+8Ek#|GQ^{^7i^vH026;)^!+_!G&WOy8|Po`R}eZ%>7InXB#q(C;PP|8)G^vUL`z}D(qr~6KXyFWUScD@&s z^wsXQGl_VL#`;khu}U?pL|_w#dQ*~%Z}uywr`#0Q@bnbvttx_qbYVQnB$S zoejkHt$M$h;BvTKLLi2}qMa>@SDnSOh%bV!(IAim!qGOZQO#1~kj9F zCfVQ3BgPTmFSHd!Ol8Hs!WO%PdjR_`MQnJfgzU4D<-T>}PObR4cphm`-XEEB^PRG- z(;6HAj(P#FD5m6%<`nPPsos)b)>{VO%S&}ahJW@HS1(?Oj5E+@xkwPx~aN+0n)y7 ztW{ok9!oX|fP(_Z8VfESFBYgHP{hF+vI6zk)Pak^(8es_jF<^p&fqV~$1xd=XfvcL zh{P$exF*iX0i=(OIz{v7-%&Y(xVqSx;Pgbx4#$vllaAB@&;`kwhzplR(UEJO?ry?6 zGBZ+!EWS$XBxMQq);!szFMqDB@C`n}Cof)+mMuQuB}>`?b8YLR6lfq}~V ze;yb`m}E#aE}FrF_3L6LROVaWZ4I|wPp#Gzjt*N`X_Jr7!L`o_8(O7!fogOCvi}FGOgjgJF`{>) zJwL#vg$I%MC7(o`E2&E0=`{+6h{Y+g)jHA-Q0XZ$oF9~&N&h46`>3WW!Es!mF*Wa$OlZT3_z(=S!S55RN%Ttmv@}vM zgtjGd)w=w+)2p0tJwfzmr@Gh?7Pb$f7#;|?y37 zQ3tFZlgwZ>5q8qYrOn){d`3b`@5~VUVw=F*SYaKZrEIrL^pogS>tp#;J>u8AZ&q-F z8KgC@sM~@PqVjyS$~&&_kV5>FXtTi?sTRHC-HX-(E=Z|G3$y8(3g3ZDZ;HMhflxq5T3|ZK)QtEDZdon%`%BdA9bNm6R_xK;sq!gHKW)qPaYyRdJRB zSDuyZp48wGC2#0u$C9%e8c==nRe z;a-BPT_g9YDHDF1F*7qv+j#N3LVP3Rn3@R3d@jQg#VGf9A}yIH-kRe4>FAuG=#WSP zH`^)yjtkL6!|x&({Ldbo?JE>qy<1X|$T$B=mXU#fIIxz1-z%qMF~X(qXmSH)qMTpab8G= z5KCgKJMPX5VA|^t13=@aMu)}v+`!uU8fNiF2%l1Nh-{1kugUxa>F3fqbBWtrZ$Nh_ zqE4niz~HNuXoRdqLUGPDt8ty{`o|hrKt|-;SXP66ehcIx%K#-z{qxhRi?N?qycGlU z%NABNOXEMsPrBYak;}v*0NWqjN~VgY17PwA0|I};Au5Fgy$CQS_z)+9cVO%XUq)&> z^Ry(Txq3qjwZ1-d7^yRa{Q&T{g%pfhUx3r(th2#sxPlXybO2YgNBxVoTn8 z&wgF6S;VKBbb@^Q&n>J48R_sAKBOZttt}J0?~{>|xjBgBp?>&;(2K{V7cy@#DO>@m zcRfaBwNu%EB0>JF7qqX({;&H#sRfDT-p+? zuJC$ZvB3B?K2F&+*(6ej_uUJA4{G>%>`ie{xB*f?Nm1XF zFRau%618;nbI{?$#B<{eiu+j>SyZ%{*JGKaA&4(A=9Tdcxxz^ML*n0|PvrzF{Q?KC z0NJKT>@Voc(oIsumo|QPHTl(|!*|9XZ#Mb|!q(7FPTj4RyJN-Y$dni?ZBVh3k0w9e z;6nABxJAtZd%>%C5JWekIjOp}10}yK^dkH3m&UGNA`Ne*UwI|;MgWozjn3FqyP4VNmt_s%;!$ZvVU@Z_XAm;bcXOV4$U8LijGlXq&4oLF+Ub2@)1 zm)|+RIq^l0NfEh;lsT9mpp|3p=yxI1iL_aAt#o2b7%4BZZo{bWlP3r{_*$xhXxX=a zSGbgP*(p%b;@Fk0*IHi3zx;X3+_6Xvq1({|8%=`hl!uPyNmxR#d-*_jpV{stH%^v+O7@Lgq6Dp$j0-QN_4dvZI(c=B%Ag-cByI zawC!=Nz#$wbc>>6FCr;3bxvemiq=yg?^hc0s&I)Erxh^qP8|$Jw*tM@U!*s_woz48CmG#j#ykq??+}u(-&Tx=&M4mA zPL^X6j=p;(BXLE$UT^`cmy^N%{x9U26wB(_*J0Fi{T<%9{Koq<^f z=qT43*L*rBxz;I<%3n=v8A`iu-(arv(X@Rp%PMnD3OF+%>`&r^!=P?Q%t<2~lkN!o`^LE5r_A>F>fZVRh*TXqwy@RcmNt z8wtM~*0+C&b>M+{;(UqCx9>L1ac)gKQ(OJ~lq^UP(ucEARcJ8a(HHY!C+>pFm705> zp4|IZpP*9@mYng+5};@NfAo);TG6GuVV)mU&o||1WLxTGkBw-Zc58ms;L$zh+Mly( z&CUI~^h1{RY@bV4{!!M(3K=HnP{Qo+$Z)+|Wm;(l6D&FhVXkh_Q7_OU-B|ldzon6Q-xMLQR z70{CFV`1IDkzHKfyU{npbgjKkgP)^e?)ZTMIP{$0r(Q19BYJCNX+NuCa0Xi!Dw0e) z#V$t=c(&?Xkr&+AwL9#Ls{{?J3J%J~9s|k;cB$p}ejA*t*i3d8dj3hDNxmJ>HE)99 zd@}Yo5J5Sz0TK|fUTdryIEeQU(A6Nr6$T4LN)h8Xb#or5hdKUq&3UXC_13xeyFb1h zxD@=W6m$5UtDCApG{S}i@P6|N`!k{YK0lLnu2oI)XT#|y+kEeVm33lhUmfsI_KpPL zR7ipos&javC>OEvLBeEM^I4M^**yQ+iv)$Lgxi-n6@L_;`f~EB8#5*!1>%<>_0E8& zfVgr0qmlvSXUapDX9jPHb`L{%5_n|50?v9&U|BFsQT2?E27G`fw8UY)EcY?9Ceqs> z{PQ>Au*W)6&&BbuXINGQcaTh$EX|XZmtOHpCy{(}X zH7c@_jLdf$QH}qq?u}AD^2bavVsl9P=eN_B8Hz3qcS9haI0*Di8vEgWvih(4w4_zP z4u~F2lg9&;zCI<$)^1QArRPn;r2*5x^3ZAqvro5#ehVxQ{juSyQszz| z(<5uVk=b`ApQ$C0eq7zNyO7MLPRYofbJLXPouVJeP0aOqa(^HfniT*Z9IM6Q|KNz; z<32QIbp|&=LYJu6WT;DFUfYpS{*>4u(!4||i%-yoU2fkOBtxL55UMAMjIrOWoObtB zw!Mx2*v8+w(xlqV!tqDE66`vP7LHTR2vp*-H{$5K#xa~o{M(1oeyQkrc+iW({l>Z= z@*;6_t6o*Yis@Eg!5{mu3keGrKSp(Ij=wyvr9Oa$KSL{*ALLOrCC|&LLi^4_ntpPW zyog*(J=IJfYpVu*eBX>FFqnZaVU;l{F*`Lv8xhY*Qlmc_PY+RR?`AGI{*OS3_!MqL zqtS02+fv|6EUC^et&1~oxpF_*MDLqo%xfVc+!y}~4Ss3IYA{_SPwcAqlpu75+Bk3$ z)6a_I*gH?hCNp?uuqTz^j;-#ee{!+2%bZmC`R6~O@{cCd5LWkkxHS+W3%MV0{sqVD zihHa9$yv0)kqy#2EU7T`D1=_L!>C8mtgSh^tMZIK-oyodpzi<$) z*H#xt5=xn zYc-#RMU;y3t)Jg8^wkG#QK1k2Taoqjo0HS{#27J7# zeX+*HMy=^S-De&fWQ~^es|^7!7{|D5d`LoDDG=%)<}>!O%N}%RlEMf*;1fUu7L!1z zLZ3iWKrTBBJ+@?SPr?LJ?m_JIOV^tr_wd>i4ypR84+8ouF_vE4f570nQ4mZf|3jg# z1QdD`8Xv0T7NPv$6%qwx)&}4l8>&Z8jkY-;Tn$?AoH3||~J;Ki5Je;*V*6I*ktTAc(Glzc3uG^JhuYF>HN=%7dwj;W1TKrYNV$5V=J>Av~o=}Q-#vOc^OtPW^JV(*7q)q-Z4 zX`K{NGk%)&jGjp|cLc)zC2Oy?^C8we%EQNxry+1SNn{U8&cYZ6vTugR`R_baUVKNoIXGFQ?Ouwde4VBuIo zd?r9!;LPL|n!gm=(;&~W%L_bq5ufehMl6hL-fedyA%7eXqlgI!+{q83v`H*!qRt4@ zkK!Tg-Xx|c9(@XPy+Xg!aa|igp5dI8y z0dgP^$nu^6gyp^Dy(aiIljWa^)}hnb;{qzk{I!x3x^A(jgMD?+s3M0~)7|uSI=Xc* zALeTgQhrA1y=ryu9;$0wB4@BUkgkGjg#rJW%?IrEGg;CvU#a>$OqgU!DvITE7_Zyc zW%#eH@HLd5E^^;BbyqfX1{U#h;$5mp_bTfT1c!N+|T z9UP#MKkDQc^u6)VbXlutxzAg)-cLveMh_IAVoDJ+Wg$9$M6^GF1<}g-dx{B;NA8F( zpd%(rcFK~yFT8${Z!_>Dx~z0(Vnb9o=i!4B-iyLS3?G^lMozx6shuvQZ4_}{eht}~ zv&|QW{+Xx-4ZIHr_oPWZKp(UWz{M7kK#|9xgdR92cF7)ayH9|QfEpC_|I>d3!r}kR zFKO_9JPJ|jb#6eX<;8BI0pF-~iGE{We2)DHZD7BpwD|^2rXPtZxXM+SkMDf%S1jpO z-g6Z5EkJ5ULaIekS@G*@Z-K$o%-7Z#`y2yMTeY(TpvUIxvaSN2vkYij{u%uXS-xeo zM}gUxd!eenn+kD zJ8O%}yv~Un(`FkRv799zo)g`!kr1$Al?!{+Kuey5?n=N?2caWad(1x5IBF6ATYykw z8_cLjMZ@giVMPssDaB9)#=ns3+Wy%0y6K7z<@%TCdk$B4$#Nv{z!LD^zDPl`Y4NbInwzi`DL442zAm*u+7r3xk2X*Jo2q=_{!o z$!={lTHsz}ww5(D+*oF;Y4390)tuT{;p~1~-Hy|DjUU7JJp4aC-4jEn5b~Sd zV4AwR3?D_&G^JQT86^om$I|0MGKyqa5o%VFY))k1bm|at$}D@8q;F|E=%Y)t$hXiK+MS1XBF=4XvVRLPz(EUHK?J7k8t5`l3pn7oG zs3|4D$nwuZH(E(UL)`*kNt@FJcCk`!72^wgA$s+GE|A%NaQk-t1zEWNfGjJQVL5Xe zlykXl)p>RwpHF2ZgY192d-eW+l&36qz&UgCbmJ>w?H@b@Ux^CO?bCF?`WZH7H;7&R z13zFHdfnu*cc~84px+BHw3s{lQZ3jYzgjbAlqlnT%~#d!T*dFiTQ>e&GMx+2VoEd) zpr>3j_~cWrKUkXP$M+F&*?9UF>%`jJ0szpw97W3VFIWk>oZ)HC>= z2*CG`cu?EinAeEyH#3tjGRUu|s?*r~lS|_&QjWcSa^)M_cvID;IwlP^o#=_)=iLU8 zgb78=V3qpqphQJ1zmYn?z>V-pR3`F0^oi|S9eXUr>r;**@0Z)7JP*geUs4ACWlty+ zlidHbXyEd+QTV0^8AWRZX!3Sxpn$bMv3MHj>`)~HQ(aio#!Kn~Yrfdg+cI4Jb5#!# zRhdWO}+G-iKh;)bs2`s}5Qs&|ivS2t53DuKXU=@4jL0$*F$ z5pJ4xTO?R(8$c}~;AuPio?z|J!l$@#Bf!$ERQpzE{?bi{G1^Vb)Iv_xL3=FPip9N)M1 z!VTBlM`|8_DNnaRglCs~#W=oKR7~Zvabu=B((l~-S9uM4l?pPyuva-i?)@Mbv|{=@ z_uN}iKjvHWW{?{9dOgV!Yk#twUYcB!Q9Pp{EDSaUGuS`YK|!%bnZ{6#S)i5%G5vS; zQ1z3=tgUZme#sdQl`cslpDma*8~6T@QB2M0SdWu|-@8X=eSuJUt%VW&WOoHvSy z1!TBzRUxwUszybHzrEL~Cb7*kx4Xw;X44(%x4h~y4_?D&H(FtT%ux#8VyV*3AggPr zqCw2xpvkVo=xCgxO($5jaNz1>26Ze{N&X%*^yM&MO%fTiadsn^>dbrG|6fmwFQ ztT>mOQ75n3D0FjZ3s>xBum|U@PyWWybR_TxuK+ykbu~#v{c|&AoF@OdnDARvAp$>? zt%jR1f~(8wuH~5JT}`em8+xQb@;u}o6XL&`woGmCvPo)89hh)`_eX(umnyzt5DQ~` z-8@{fI;x^=`t(Nlqn|m}a#0C>Z0a4D^8o4H9YQTRfH!^E;cN}Qf~CG+L+t?EwCsyX zUfsS-<8*pG)klSKdmkCl0@b;)^Hu;(#!E$RD^_2;3H4vWY~!C;#*BR|wZ~VZ^A};KRl< z8&c`|S z2Z+Rr)7RQN!!w>kpdUn16ZC>2OaM*lEa;kcOGQkat1KnZKIM$SCv^Rfw!EtDj!ICl zId*^Zd&g0IHcelKQ*;YK{PVEV$^h;a&M*$Ny#isOIU&7B&(mqb2L9yE{9w_t&jb@l*^y;s!@~Bx>2GK z^Dr(4c+4>K)_3p<0c@9sb^1v22QMY??7kuC@=z(SGH48(vc9wLGORiv@q7N(r(W-< zZFzY>8%qxr1SR|N{x;aQs1JbpNBdDkAh@yi8t5>RQC35q1^&7f{SqQ&6n@w7%-iC1 zI{`0+mDB|E736sv>Y2K&Vcs5xs&qVohY>7eQtr5OfBP4-yv}WBHb8R|h}AS)Y~{eS94+d8I%k{W4dC zjnTV6cKys+Yw~>prnMZ#ah(a>RF#f)LHKfw;kk!?sGt4f%tQUDAj$)A)7kET2xPB+ zDp-LNQk~4zYCm|i=bu_e4on9OCES*D9hg>I%mzwRK|Z?GFNN|qL)g^WG{slgM<%Ct zBX%YM^Zw84FJmiSnjO!EpUpZwi5{E__;IQI0W`E*F9fCI3#D6wi(x;N4re$~9bj^f5UW;8z&0W2H3x4Y z0-apS?4rw$K5i*WCwI%ws>lpg?f)-TYH|7*#!sGK$S0apV$f2Fz7Ah4$J8(OjEZP~ zcsmus#>~$VIhhaVb2415W5I8Kyb&$wvQ2qon)2W#Q^JjVrosBsn$N+?jg$3?NwT*z z&fU}X`etQ<&Xc%eCC-HAbRE%D{P&=alt0-BqF|*o{Y?wqvj?gvJdiVci+KRH8a#xWDnW=f~Tm7FfkHM&!humloY2KarC}VQvhT==K zV;F+ZJMCs1BcJ1*sqH7KjOE360=mubYZlTD`pWZ-S6ZVl-q=m!6sn}VE12moB=2Id z$||W7U#g2@^-*k`*fcGW<+zS_{WNmXvH60WZR(LV*M52VcAhcMXxK?} zM9&FC*T>h@CpouTqfSF+i(ON_fWipR?G*~@-mS&ACjzsIfa{DpW5MWmMZ&{?rl#a;M;Vd0Dv^$g(XHuo?I z-*#T1og@lI^8~`x2cO-qlKu51K9n49`s{cDvrw!#pC;?9_(VmTq-d`^1|^0C)H&4=1CD-XvMpr$&1*A&$@)|uyY4U;J5X7 zZ3AyETz7n+^_%}oIK|C};T2$ciU0*$vKm#Eh&wno0kT!Yvq`ER#NS}euwVr_v+knh zKaAEzaC#v@6l>HTkjH71hJ!GcI%T`Mvgbh3ATULEfJx6;p!aCGoP>0I7B402m=)iZ zIe$OQE4Zepz60XG1T_TnKtb9_%EXtt>%Ap*H6J`m`iIsmavMLG>)fksGb~l(UGBb? zi2j~8?=rGgFL(hTVD<%3CHT%gcQ`QOSsI8Cb}_dTw7Skmt)C+cD~GVjw=;FYozOJ@ z4iI)z_HnI2lK3wN2_At)JG!B{)^AHID%-a%#QNtREgJY1Y+%@}(}C;867s3;pzGot zjwM*hf&M1^9=UirJACfoy$9|@lgsI<`LNLs$jrc&?|S9K*MHtWlMgVzs(>gX8U%}f3t)-*Aq|6lUk~E70M<~1itCmD6JNOQ~SGW z1J_qoR9!u<<4GGw)~!2D1Ah9`?Xx)^fsf2u4lI%}u3vxo%9#Fc;2xf0eHrT~ehuO+ znyXA;QWI}MZWOK;3a3x3_+&?fwu!iLJpo8pI8J>7u0b0>vniY8P<4|ndbcfp={N2( zH_Apy$Y{2|Ftd7SXTU2EuHwclsE#BwwRv-MXh1`E2%A@LOPgv(9+RJc56K*hWAcOS zi;`XtHA^Oo@gap_pTsN?_stJ#XX#id1=Ln1`e0KBdirvG@i}Y>XxFIBf%!soU?Q<= zdNEv<{YB8}-tB5T?0^SzL0vy<(Urcv=f22!7Q4ztets_;7vK>7b1%{Vb1w(bMMxf;a4U)t!~cSMNZr^ zHZroR^c0@&SYn6W*;Ryy1CvDy%YhA=)M^>tyT8N5Rw!gd<+o|#V^EHDD6AkhI?1u1 zZSF9$S9f`3KRpe^T4TjG`GhW8`fkX`={zJ<{D}wb6~w13Isd7m@h6wSY-^m0xATm4 zmX=T+TNqZqB*HpStkdbEN5ajRUr|rqF{Duc^Z`w6V26ubn){(0^<)cDmNPV7 zIeEWhaUFP3kGq@dDZ3>#0+4fhA22VquPZvh1Tf9OPOhX}EY4U?KKriZDxaBciDuG6 zWXp?7j9zhmQJV^_nn505=>;(x3tamEvJDx=f-%VcO#>dEP#Vv=EM{FdcHJIoC+92Z zko^BJ_MTBqgj)`N$(1RQU#?$0F^ExBE5tnO{8}Sgx-sQfFf0@QX;)bkq!ca zfJEsKlnx0cL`u$mJmtOLdB+`hd_Ndye(VwU-p?v?%{3>!h)LS#>sM)?y_^F$-}wjR zzfRlsPzakpaCI<1@UGrJZ+nkVcjfyBq%1-@EAK5HxACb%7M0n#Z0f&8jNGFGZli8Q zkbq|KH>ZzS8+2(m?pW&Z=e@k~MR`rImTAV=AvG1v=`OT&`A-)99+o@yOt#!PyQ8zz z+^O-PyYZX4ZxTawR}(#L;?A*Bbilyme*r-xaTphGe}`_lj>hpfeNlCD0m_(=Z#niI z(r@u^aa&ry#kPy@X&OisF9kaINgg5l%NO903&I+yxwDU6U(ibnfoq*D0isyZ4zvyev^ge*h(IY{FlHN z1HhaS$EO+yhv_!5l`BdQu&YeN3Yp!z&IyqVm+K@VDQ9=-btl>oA(NI$gTqb_9A3I8 zoeFlII;mk+l5?-A(KV*iUP-=0Km3xWQT?~jXJFAR#)PoQ*CDXlVw^=3Reu*$Fg*U` zYFzBn<8ef0EpuWthQv<7X_Y#Q!LEY`f(`!4f&Q6z*Z9NKvEe3l1hqm@=#_WyENBpI4oiFMe=StM+3T zxexhc=v4kiq@&VR|CoUp-?){V!qp-T(0#gslwrrka@R86MV%p?e zhNY12JvB0#8yD44JSp9wk|Lc>i(Kj+SHHGC{3aBRJAYk$G{!66JXRI7*SDA82V*9f z6avO{t)5@CI_2lO-0nGrd%CZYbY$@aSV!S`Vbz-MPt5YWVXAoS>`Z0htMHiZ0_%2a z|KFi9iv%ZBEcfC>Cl<8LwJ0P(3Fix-QA(dZI%h*od^@$Q<&v^*+u@#>N@ z!99qc`4_{@=reZ%_gR+PRpcy+nAQhJqW}8?WiEq%w)BiUz`>>K8)g<$>D)EhlbLow zyxO384hd0wg{0%avnYg8J1jeN)6kvW&l>$0YqFs=Yk9J)@euPaxiTc<$Spe&xdZvD`q3mZ5%HL`OutyIE~3m(7=I4;)!_{4~LWig6KgERNM0>4*x2Zq7NK2{aQd~XudKN|G~ zY6t!KjGgwQ`uF2q)Vn5zKKv+fa8~2)ROL8pJ_SBSZGNacKDA^uO~%xz3_I&^icJdC zcR{S!jQY|jD&G4KSdy6aj)6Dbn;c+UvgA1;BYa*DN~U=MLFlQmZSu*2y{A~*5vzxA^jezHG%F_2r3?fYClpKzRY-g8z?wleCB&&R!*shY}uD);wf z?^~zQh^GdT{#-@tVuSEeIH^v|{2etM1CtR`c4YXxPMf8f+FFs1EZ6t_!*bl0jq9~? zX_b|?RX9}`=g%%AtrEJHRoL!ibg0RzxkL323uwOm-Lq8a!G{xK{IF|X9~Yf6zl{b5 zxQi4z1!w>Zp4h$a!(&u7a=EmF2Z;oI`Db0?m-Zk$I$4bn{H~RkK}UnhN(MeJvo_Ij zr+}st2^?*hJL%4Z{JcKQ_AwRO$wV7PTJsGK^7x_*QExkKWabgPD*|>_DS>Y_IXDDl zP{se60{w*xD`~j>BmH%|T02^;q`9raO6gqt4}LgoI)^lirHHq6LfZ^ei{#n)KkKDW zJ%$d`oazwH21jBv4FtmQ<{(u_ZXxUP``f(+^7z}v&(z&s>{I%-LW`G?w#<40%eWX)g~vn`7w$Yr@kYLAzPtbe%DhrvoU;2 z!zPnPygM0YKR6^ahB1ZLFt`CtZQVG&nFaWdR6pCqy$#?@HEWWz@_nx;gOyKn-o)4{ zoCT{231-JO;gfKN6_`*NOiNJ|-5vs(?IAqzytdi*@Id3=0hjL>~J3*^Knh zduqx`&U{m5$z#WsMl46Y_7Bpios8^dg^khb5H_-B!A%s3Ar#`2dKRy2@06k!R!nxy zXlG`ww^c(8dn7`Ok~Q}hKE(qugGE5#Na+TGX&chS=$m9_t9wrR7nTdfB5|8| z)Mfvehd9Fsr%FdZArD*6PpL8FTv5ipR{qyJZ&&Pz{gftZlxF<{VgcqiwvVA7zX2-l z%7VaoF@mnZxhc>j!ElP}=UG$U7nYgpJ>`Z@3^`nP5UxUbF`P zs@FHsW{q3&xxng?XgkruO?cWt0$Oo(P}zO(`6ziD@{ITxKglBr*7kQh< zU})1_i1G=+X{lZJarWtBY=4G_-yPR9qfjQV&EPH6yHMFhY65Q$jJ++W3VQG3#}@A& z6J4~1LL1o!@MM@g!QtIjrm0_2y?*tkhuR2l^o(rwcQbOR=y1}!GI^eaM{52HRJhXf z&Oah2Yv*2#f5)}J?Wz>sUGww5vc>8;POxGW!$zX5S)X^)y5i=dhcS4MAVw;PFkUH&D2@(&G5V(T)LXvQ1@?;{{JpRUhIslmrtQ5ynXnsa$s-I zLF4Hv)wBHoJZd6^a@{b5eO%Z(N$PfHV=BcOCLlo?dTy;lg*W8Z2NA z?*+D9+=Ms>7wS%`Ji8iO^6?I|&X2({cXt_l0S*6odI!Jrg2-D#5HmeQPG=r8-L@E% zSYnzP+*wScz$gXfT?l43NA|eI1k&`3$_2q`J}*~OIIU$gDMUK3!Xm)pDj-p16EaY` z%PAzzt>3eS=6;&i*Hj$Nk2@To5)=e*lLLk@(Z=?+X;(=~Cwo$7+1UM`+Eh_2-TC+A zj277b4-u`eKt$`MorUgW)c~WIPKQo1#}M8aqwwiXzi<`d+qzPOjOf5b#N1M7u>3f* z`kT2xGEA5t-IXLP*N?htIWYZo_PCC0fSj7)AV-|B2Q^@!Z#0C>?^&dMF0~I{vWB_T zSMe%aIabKUrzFct4Q}E3-JP`2MRqJY@}_Fd$`?t$6klTS+BEn?lTTLSUlOd1%S5ll z^rC2HOf1jj&F(n4i}L0Pyw}7-p!EPm3tZRX*b-H0Th5zfEFRK%K?akeR zk7rm3XV*4${AVzZR1`Qzf@wnC&z>2K))KGsdK9)YKFLg}W4}(GyTa^KD^}Cm82{suk+?{R6@fLNT=BqP*y@ zW?{KDdfn_ah{aU)hmW0osx}S6Yge{9VQH^dDRL$yVPc>=kJ|-m*qm6bZ*JO0o0WRn z@XYs5iaCb^%T{QJ5RfD>I?Lpy017DgbBid@fIKrVHFoReY%KXpM4h7}@zJVdUSp^|_CiS4pc zf&zCH@J(Exr*)c9eC;N;plYmBDlsquc`36d%gcF!z7TZ!#Gh?u*U`2Hy-P3s%;a5D zEMF+cMT?2(u1yKx&(zHQv*KS8gU8^JX=Zmk+f(K)fK!2mDBTt6I{x&Mge?(efj11& zxOZUW;SyyL#zXbe-D7YN%2fy_Ar2$=EfgdV|4zJ}0Js4WN+G<(F#}2GqCVsbtG%uK zA9vH)@_W>Hziw^Ip_8m@5{h&6>_2-dKOueb(ZGbc}fY8 zpm!?;H22b%)k9b99k7|nnz91hnCv;QKI|<>YE4+5e#Kpis|gXxi6re zMZx_B)Vd`N^}$NNtLb*5eDHB6jjrD${b@cXV7Ih^(MFL_?c3fu_gIU z{&3n#h9lN9q0)bsaDP$WzdwRW#F|p40}9j;yO4?2?j?oIMD>SDc{$u)P)2La{j*zu zA&_7x95fr};MgD(%PS!+4-7*L)C(Yx3y_d#ldV>ro$fA&560$?e$H|jQEaj zSOx(9QQs&W>6iXBqwk$H)sJwD7x*-Gng>Tb#|Y^;)ItYR9o3T!?RrW!TU%38qN+1CKkex` zx9C1;d?7=#UxF7F&=ifu)zhKP0N8{TRl-%^rGY2Ln-`)5EwP*>%P-|3(5#$)fRTmi z@z5%Ig1bg?XB?N@Zi|l#>7VBEi1GMgt`RDw=&mu=4)I=)KF@6v@L37kdfn#06yHR% z#(t|?<6GwHQhn%Bn#SOnjtRj7xDFa=;ew-!5pbqvysId0eJGzp;7fxz6P6_oj^H~3 z&|fHaxcMebbXwG{EOTXfcXggGsToA;LTu^D@17N-XuFy`vj8)_rnqdW8LhkVm1Uf< zd~A?&@HB6d)DFJv-PhVoeV6HA8(QGQhi3%E-zEMSAhlj~2WS9Ho;eRIIltX{D;6n1 zX|ft=ZrMlviJg5s=7w&$3`dYrcMA}gnU(((^n)o#cDXsKRr6|${%p8l$=6TCreCr1 z&l_n+Vy12gwGK_DrL7HL(7~ z;Gmzkr5ENoS`R5E?Qw!wVIl?~tNEE=(=Xw)LMai(&eTNT4Un-XkuTitQ9nJ2Kc@uC zgaa<8{7xHQl;(umO*0?&XAf$hI+*X1xykVPbqy4vj0tbB!$5@udjS=*TPkOfnHeiw zMy=6E@rhaf?QeGfKgq!dmT_=J;tX?>)B@?mwWj9Go7Hakhq1kvAZk3^da24B@UuHH zQ8+*~5FI9-f$%-xyx2kQPpOWDMqNlw@OLAQ6TQ)IR_xa)ATJ?gEEhXv35Tc;oeqk& zdNE5hDZi5OKIyvgzjjz;N65{XE(F6|9mzIK5UTHb?xt#2Yzzs%(EC>Xn2|S}LLkD> z`}|Rl-hooxo{MW+o7?^kquCscjDojPMu>$rFX%ah?hLNh-Qh?J)(L`c=LY+@;fk~y z!vlDmTBE)5ByE^fjV(g07pX|jZj$ZFpPN0J%+-H9B_d)Gy;L_+u5q+FB{f4#@sYdl z{9)rjYwdFCT+y9ihyuEcyWZ|-bgf^BUSwqd%SZD7J*dq+9=#~8h;+*UTR#u$fGhK_ zCRDsAW0H+$oq4Gv!Rx2~skNyttgJ2XCXYENyDt|Bkn!P6z_ChOhaa*{D*ZiPsBEys zx1+Xq3jK`l{;lWYnMFTJYul)95aVP$m$U=?=rYR5j?`|^JEj@uT1t~-Os(Elq)_a$ zYl`!hv?(*4B%|)%1($gX<8n5Q4;k#>Yu4ThQ!E-Lr-#bFOA$Z&5JdB)Q|^y(n(?lH zz!(w*sU2f&L76;k7MmfqSNuuA4|5&fCDT}>PN3O^gO^zPOuW>p0 zg3y?%d=j9V)A}No&jg^~l^oe#qo9Cvw=9XX%Ht5PSIT?=C+e@ynD?!ByXGc!;$%d33T?~-u?b^L*8GD3gOK#} z9&S^@!3wfR2f6&a_}X;Uv8h(C*@EiG6U;bXW({aYL9qj|-kl7NAD_1}&RG1=IliH+ z&Rfp26s|~du?UBZJ{)6OXEud3xYqrg6Q$tIP<*3nBM%(Fd*8LD#KCCVv0{m0hw^9Q zVNI$xH-sHB+=h=vJrDo6AA9otKg*i%JA^H~AGYK;-fizXE;8_Di`N~KhxGRn=h7%O zs4oq%qDqAh?yOumnl8iy`>vdFKxm`-5lL5H@GW>YlFF^R9+ayg9NAx1jBm(A(q$&a-Y`|Af<@8`9oB|k z{te?L+|kADMTyoc8Wbnr((wP4c=k;Lwq@G^Ec(u!%fNo7CBP#z!Bq;ok!Q>)L|MxQ ztk*Iu3p=LM89lw}d^-D^+p#6p*VW+bcZy4pD+76DO3I3WdnZI5$w6D3@+A~zisRm1 zT)tLq8Jsmh>*W51@Zr}Tm15ak%k_c@ z8>uUD(9?*vQ9fLHmQsT72@dcoU)1pnlAp79^lLR!ZCIYGINf6LGv!u=QUZ~cNq-2M z@B4DNrly_!Lp~6CI0O{ZCpDAdocHiEQ!w9v{SU4K=FbkM3>@23-wj_^XR0YvNWS-> zYJ$G^5MtjUcwU8at%iwP&gyyyXX2-^6gKuZY_G7@%*a1WbMCkGP5O?psV-);li@ju zhn%w$>afR;2&v1+?IblPk>X?($@URj{#y`Uc~3Gb`rE-lMzMgf$u|SY?90+b$^o!k zf(jux`6wRu2m;uJ-)8bD6&Pm@mz_xPV5H;Eb(M!>Wg)+7wVX&I03G z7j^jc$EZp)Al$E9WR})CoO^wgel~U2Y%*(e_udq-H8B>xs(w}BzQ8Zr-&CpNIn=oq zf_ZTlcY|n12x+@t4WoZwk@{_1d_O#~Mw~n-{0{t0oz{WY{>b*e?A*uzUpC~_Z7IJ? z_X^>D?_Tu`Ax=Rh_{CeR{qT3QSyYw1LX9g=Ke8@)b;6CR0ka zWgk7Kk5`jwVgxx&G$GM5?Oo(lTm{4sQ}ZzgAI8yQ)Ro?cT(Y#w88P>+1S_7}J~xW! zULb8`z7xRgp>ubmZN=zmg!AoPHT>Bq1hfUwZrILqhEHOr2h9I~aH8H38U4kyu>uh; zcaxaxTcSEA_*(79X85e`@N|79Ckd4&nOe;st;{|_E&DdSS3kYsU+TYC4Js#r=o#fT z?_04Q28uBwi*E{Oif4PX*=h`@y~jT)u`1U|7y(HTVpRx!5WC zPb1s_K6E>=KdVXn>|uMI=Bk-V{RUqe&w#T;>jl2Rq=5r4+1mpdtlRnuzR1Dl)_*eC zq>JPz|NgR7-PE>2Ze62HHQj4WdMLH?K@)}%#0s2#Uz+6@-)?rv$ju4y8@8Gz2I?On z5Fwgwbd#xD+}+_%lyNS*HZ6(8HI38y&E6n+x&;L}ThrtQdtIo1u(NnKj2ZSu68K12 zP!c0d9f%hOh?4v0orYjf1?}ZiZWz4gJgJm6T3$Mfr%}-z_!b-}bAPakhP)@|QeiO4 zB90uRO=G&?G`hhG=_BOd_EHl#-{1L=n6C^u6Po(}AjEz|1XT|$=U;FRoV#7Q|HD+% zJ+wpOG*l{;n| ze;1add@p93&03_>VVilg>jy_7CYc8M!xvd6-A}_vsOzs=w-or4q8D%Gn=PBDd9_!c z<4QaURI%TG3$FJV+6Ch}&kwlV&ac345IzIPqqDB=M8(5>S5U868Z7^sd%^R3#z%=L@Rw;4~q7A3wJ@ z<{eL8LRq%*+QrAd9X^J3=SdNun018FdoVg%UwcxQ=9?-1+CIskhvU$moG!;;F`Anc z;m4hF*Q^Q{BaPJ5wZ?Cn{LYkSN$PPSF;06V{@8yOA3+EOE|6%g@3xb|MY-?dXM`M| zU1q@S$+VJnl23f^fDG*sU(A6gK4&uxnKMR zvVZ5-ONSqEb0VahC-3UBc1C-*CJV?M_+Gp~vi)fH&Hxa+0gcaP?iHi+zvh!m50uU< zLLz_$H}v#2LIT~!TtGjrkbZ~*dmk=m~##3|fGxU1{ z1}MHPhBhF%`~@etX+G!#*?Qr_Tt^s2E@xcRT;lQ?MGU!|svxwYS8j3lUXtUz=vZ9S zba{yzv{N#QAob9}Jqh=XT#(&?q91V-W{+#5w|m)r$Hd88#t;sy1;EmB`2hk9X9`V` zXB7W@_C>@?2%+hE?noH_c*Au3*kg22E~z`{vKB8TGIezweU{u~%vc#Us28JjmTP^$ z?*jYcTtnwlIO1sL3jFCuAqas0Ec1#2v;Mt2PN!*ULB8bQoYf?!!7W>NK;CL2TC(la$g5;(0aJg2m3 z6g*%%jDvWIFlj(Yt$2QNF-KCKQ-wHQ#ZVr7Cy~)=L>ANFVXC_gc8aKvpENeMj?eU8 zG8dW~RZzWdxqYujVEuxh5(i9%fL13YeQ+dlP4R+4_IRJPnC^?6%#g2WSzaKuOtqNi zc_H!o&rQQ)=IjahtlDyMW9BrYiBaqBQz}@&IJ7-vvXiP|kClMzNm*{5*x%AP;04!ERjL|?`i)Hh8nMx2(72Yep?R*@{vyuQ!6sQdZgw&JtSGS zyiCxm{22oFgd@dD!>_>g7h#iMx<@}o+Q{Xnr^&2!$o^>|lAUQ%fA+Vx@9J0W1)C+9 zqmwIuwJ~%mawdRNj|A)}=MMgQ3q{zM=~T12ingdXEKOfDATI{qk2M7B^>K&6?e4eu!_E26KR1=th$HTeKeB3ajuEP(eCLeyR1-`N}^fR|4 zcs)NQ$X!iOJfWEieMS&k;!Y{SJ$iFqQjE0!IUuJgGBT;w#rP;t2%K!8-7f^&E@VA( z3I?Z^EvaK5DRfJ~xTG;{hNUN3=5#PL{7&lO9^7l31P%$A0ytkAK-gS6PqZ0>!F1_= zeFO~$)PfH$?)Hewy(L=m6Xi8^uty2BFq*!(@}pIejFB!Ad4jEgQlF$f#v zSpuSoAH0p?LWa3u(`PQr+Usq~E5BXDC&)i3g*-M=QmP0#-Wv^afiOm4+8qgAU4aW9 zD-&+y=G3XTJ!8J?OLp|)@;plJyl^QKvosKI8>Zd)^ZQ3drU!gP<#FhZ)9dS$cGzj1 zKu^B*>D}TtzxXn5CO#(B4gRu5ykPmu(&SX^??xTDtK@_K%izpSykwkkrt&F+l(smB zLs;U~E>sYJ75}`&XA@Lbl$Z#a+_e5SN=@mPRYVFi3|=NKXL*ouwV@~PK8C&t8RLRZ zA;_>e?@_yV(vx%Tb8)PLDW2t*bv;QBj!-%P3j`PXj-B&d&lZct7|?f#*JXUF{V3d- zIeoUZs&$IQ0|I244#+ds8puQQ2Hb^zj_&`C?Ef__GV0-i5iViAUCc5qi zeI8Wb3aqxxSpMb*qdAa7dOE3LWCR?-t;qa&t1jLpqiV!YLig^s=I+}r1w!CuGMan( zS9irI7ZHTZ1=8OkoEFbp@2R231kE1odtMjI&*vN*xDO^EJ*s9IQP7 zmhOFdZscVFGlt2Ds##>Bh+3E{UyLjw~3pHtMtxlo|6 zRv9wZNxSW%U16rfus92%{3h$l(~Kb6e@6Lg!VRo;f2&(>68BTI_t^F@Crrs$B z*zww%=A6CHIG)k%5&qaUeRhjI7-Mi*9=c{^RjMg^ZRU;QBaR#zE9XPfc*r2uH|+!( z>8-G4ZNT=owX-+75bP=!DA^SFiMMVY-lSJe++H`#~SO%g8f zgol`WZNb5&dpGgF?jxYWBaBKKTb6_2TFH7>7fnWA)Y83-8n4#aOGI}|BS~Pqv-3Ka z2|i`UbLF5I@NI^&N1JnSwS_vb?}Y(@KOj^risPz54KU6VqtldL7pUVyYu-StE;8T} z!w5!W%=z^Kz3+ERGsS2{+FZAXseB(ZCSHA6(Q*lhjcbC1DMcE*H$ib_j|QPpk3Lhk zYIP~i=ZF+eE2`h&bK_#Rmg(P43yuj9L&*V)1$0IK}P2+|q)y7rxOldGkd{i@Y|WpLe>TKf-UTl;CDD$izcr z>`7+Q^6VkV0KaLMd6Eq?;=@f6-Vc~th18RDIUyk-*B72M;g8wq>Y~*zAJfl0cp^R@ zYsg0=Z9m0l5v)=E13g@>uj`Bud5-R_yy>P0!pbXTv$#Bu0nC2S$_267ywL?rJ5 zD$M8!pz6l3C5PPpQ$f(y%cKm38y*P!2(b%$%}*D*8S+SD$)Q8`Sj!Z0c8Uz+rZ-g6 zTYmEv${sZ$1H-I=1+&*MdO}%uh-f<+p=00bt^Iturh7sNBnUGodXO$93&_@jmvoGk{OTm|FV%4B0I&bEk(nrrmv;cR>2x<6Ow>u}Gr z*MSpCHisc-(AY!#)X}r4?Zhu>Gs7-a)>F9SR1$Ir-Z{SwWv-snr)Od~?j*@8 z+=kfE+C|AW{%JF-&18wbi11!g`M^UI>AxynVpIUYAph3W?Lc_8l<cLr!#S&}n(&|0e=)2L(+3cB-=v^Mo3Nt@ za2-mph3s}twF8J7hKpyep-Re*s5}s(I^#pG0}(M)2_esM&#nRRBRbjFpDtgEi$#SH4EYVffbG%RoGoxqbBDOlJaw zBh1^*SW~9_>gSptoKB+V44>S;(WbX^!1+|J_NMF;uZS*IF3gVL|LmqmbY%Gz=>*G`;KJJv$Vi; z0J(dea?Y4*|yn3fenpSScGPDDtJ)`!+3Gba-oOeG_YTtMnzOulWD1JP1 z(Ycp+Eo8zJT+S%yo#_U%X_1L|e899GePzMRL6Rqr<~VR*&?Ui_ z%AN3m8J$0m*tLV2?|d50lT7v63Kv)A&dfL-+eSHgIAGx#+RZnj7ED$RQnk{mu3d`` zqO&+YyG8`F(1cAi~G^)QV*NCW#}+lxMxTjFP^T1j&S;LB4SB zPg&lcw+y+(6(UTPE?0?)!EX3l3Tg1=sk}EEG?Wy+R~bLzE)Th+q;<~Jf!$3gH<#=q zrj}WlOG#@>*2;6ZOK6OW<}RPJKWsiY+!4^N&%b~e6iH==mJhN5M_JJ7K=jPVdfiwz z7RazwUz;i}aorqDzFEY5x0Yi^ImiK;K2Fo!CPZW#1FIN?hM?6U1MmgdSSejcDH7Ci z`Wl4vBDke-k7Pz)1c{H!tvOr6IfDq2a0<*wYbIzhChulLeYEDT!^AF_olVur*9((|%B5+rRL565_nV%v zf9r<~z8O6+Es*C#Az*V;cz`Sq0-Qef=ckGoeFVR_t8heEcAg5%nCpVMOE$}mGKpU+H-(%vo@K8XUgGe0Zx6h&$=;*tLc|a**p!$y3(=&YW&eiLPm-5} zC$E!S6*-Laz^S!sj)GptBQW{1^9{LK~n7zJ07$J_v%&6%Vt`q-#` z!NgiwWs~}w;no;T6!CNd#=f2@-Slxd$Sb7RboPPo8%EzOn_epjYw!^7T*^)EhP*dm z@ug~yc%cmJy(n|*=$Uu+AlaN2U|3K{6j!DWToWIriufI`k~FA$f67xXf?xy+zK|CP zlZNJc!rPVdy=$4Q?E1|wh2x)QIGJR845C$mj-xje?VrQAzct9G)IT>>%k_heci8pS zYW(25s3E{W@~3KPOFLnYaUG9$*HqZ^WBP%4G7@4ve)(LX@)Hg9dbZD(IbeqT*4e#< z)L2B1#UWylwMq-DP|(bz5Vf1)I(N{3kzj-B-1=Xi6wnC%C13vKQf%#oSxR8DdUV65 z(KFK(FVKE<+Omxe=f8Sr-Dy$f1Nxy763!zbd?DXL1aTtYQhIiY+>?48s!i#4Iv-i; ze$Ekv0)J`ep7jD62f&@&xrQUtx+&_qeWZ~gMX@K+b*9ZfEwJgVYkFRKrdzpg8l4qX zmGf0)1>L5pCKIRqOi~S{q+1oul=?>((%yT*B6^Fs0(I_6(OhJ-N`5K zNBytH;eQ&4#E@xx7Bd`Lty3?dy*l3`{FWg%_D&`&B6T!KQNq6{k2&7kt=-h)!8p1CEET)uinq>b~p*r+*A97nKPPD!p0JuiBUV;|A0V> zKoWM=*R8Fe+==SQcfL^7n)cwa8l8%wKN)y~0)m`9Z2cswRwck>^Yv=Hs^H+~rEr@2 z7U<9HW(lZ781tzxz4k9lzSA87w(%A4*4vQul47c$aWR5 zk`5mNJmaPkVIE>}p6xyS2L?U6XI#0&mKFl~-55Fj(3MI*`bV3^m0DJ*Dz-nE z?6{u@DdCgrn;QtDD{9T*#+go;#tH99rrw?POt}Z3iiwxbo?)}%&$LBjN(i1xG;s4$ zLZrr9SFMXS#qSbnTX+=!i!4GPL(`9<@b~LWbDW8Z=0)!bqn}M^#gYrVgIMO(1x*G} z9J}L<*5+hN$2^l_t2nk>_BMeKs6M>xCKn+`+RZPbmvQqB$yxQDXdl&t@>S^R#^tQ{Ksml5LmpBd~e|QtQc>h9k{{E z0Xqk}OhW@_PA&H&6`P4K~i`chrUWKo{>c0FLcHKX#>i9&2>ms}?NdP(P5+G;s8zPTK zxIv}NdP;9Le)qvgGM|Ump^sjHbJ{hm{dO#VflgcIM((6R!<)$G#vk}Tj0|vw zhih(M2ziPA3a2E>5yT8}8JSsb%Qw9#6IMhDKgIFN7u%5b`||LJgRM3UtZ04ae@bq^ z)PV>cI0O9WEW-^zmOm`_0D-X~Bn_PmM!x}Azl1YQpQ<_S_Z!DX>WB^7>%yLz>2D~W zy%66^Btx_Ey`+f*C=6yr>^SBC_5Ct-loD@?Rm-TDS=98X*raqlC64+D+ux$tXcoN1 zD*7wW;=?3quw+&z8LMcyP)`P>*kw5QB=*9edyDYZW%4`m#c2%Ovm zw1u03krZys?YcHqJSQi$LD;%V!KJ@n`=39RC|W{_Dt47l2esK~`r=9B;KHU|qP(pP ztHLzf*<-BGZOWB%iZpjfdkWAa8SDHjsskA7);Iqxs)O$DV4Og>EmtIQnkY2^rMcb0?>G+t)#O3=D%tdLCg6) zie$?R>0Y*ld=u=Y^Bx}FUBBM4&ai}Dy!Z%IK)aeK*muxj0fhcJTkhzbQQ&C&!(V;v z_1`EkV|G_qd4lXiVK#&dfXb8JjXXN(9GqU;F#`YVGzWS(hKncHgf=>379j}UrF=To zDwE;z#f5t`>G%!0On%}?l)(B(+H0p66qAVc~(OKnr$BP(Vp5`YG1o{KA>)KOo3LaA=;AAn`Kb4_6g9k1PeFGh38*B@LOs zt`^oPMx@^2@>R}ag-qM5j4>nI`DCSn&k)1&|JhJt#6ow}!3#3myNKTcF4i?hi%0+a zqsP#U|CBTT`EA$~t)N;#Ff&Tf9~WDzX-^5`(@E;#*|Gli>ZL>(&q}Rh0ZkQfThTYs zVK7{fnzWp;x{c1X4U}$qRD|VkFH<2DEme8noIF!6t9wQLMDu~2*N@!+*!Mr3s*n(X zkap`Y@10O@zzMA2!aiW>CA_Kb^+u%mE zskgXpbnw&S+Ms_<;wjx+5L*jojQP2xG>$5Jx}N9sMd|?iYke!1cm{gKN3uYE?j{)+ z@BOQEK6>&$JHSG3rxAY3I`n_G8<2DxQbuv%<72+DTX}EDfkvE`(A#eapJfZr^B47Q zv>Qg~L`q?)V@=+dayew#<3PE=8%aH@?tyxq8R#t`)f@Ca^Y$n(gx&X0W_$I|D5m~7 z|4@1zPGHN&SXhkyYN$%g%DU6XSnOyQkQ=J1EMc7Yx(}kO>d}d;vuGD$@`hjX^LuWl-dG?In^;I8M&ULbT@xL0BVlAmBP`_)Z+H{L%7Vh~DQ8hVM z#*ByiOFAgy@}m*=eetba?WNC|Z|2#mx_GADHpj?7%dUsYT^3^Qh20ugkg_5!P7B{% zRDv1Jlo=mmSo?Kl1}k5~`2DZ1{FV_pGb$XqTS-Uf_BhkV9kK?>s<-%`VEYGp575+K zgJlz4IGfQ1b03n*->nZu)7*fIeD6})`(V*oLfC#jhP_8hSpkzLi^@kX>mz7DKu8iG zUimA>2~j@R$V^${lLF)rt|9*8@o_H$oFQGHq6pQIxB>@K(T@OEjpxz>+ zvFS^7Ta7|anEM|d+OZ24PgVVNP<$1oJOB$tPso*)l@NlorAQhhJPI9Gx)n2mr9*Prywt0 zGP=T1^Kp{`a3DzjL|E>e0+C;{d}3ERa@_-Mq|^-$zwSgrh!TD#enF{zrvvVGoDcs-t*Y%$@Ic4*OA;Pi>s`#S!9&FTjPf)Pl}q0xd6CRb2r8Qgzs0DJJQ)Z4As zJNH>X0ud0v4?4UCw?gO>EU=l!{nCCN*KxPUCAB{a_1)k`1WzQYDTC)#)Atri5SI}!2)V^{2X|tCVLj~G;ZfC z-w%>P;?DF42FrVKw*eE$mGZTy%cJY9^>!wte)mj_8OIc1xm#a>>Sl%lD+uIBI^^Oy zlJFhkfdU%1ip=MJ|A5RMz;EN=#Z8tJ*b#2(Rcy|*hwAd+&WH=UloZ5|m6Cg9{=oI< zcSj%qV|B1f#CYLO!rcZChP_GD@z=qZ0kz=KO8&`hqpv4rXOY!FFRl&6yaKxISO29k_BWmz4K6m3*CO5gEycvoP<0PyC4AR_ z6LVFtmnGw=HsZ!K>{>3!^F5QwXkV@IhWgQu5;ahF*Bsjt2toxONc{3-Sh~xFZ|DvN_Er0rkCdN z9WVxP`Hw(YAH4BZt#lu!6o2Z7W^w~Rqi9-$Sa zZ?x5^zYzZ2_@+0=z~qOFEn0qX;3JBIyUwUrICQ$zee4`2#z1nUyL~F0j$p%9jPeft zo(cyN;+&zYr$&WF(pXH7%Mt(<*H6giD6?uJhMn&1Q1zy{D@p)r8tH$6dxy_?BY&n6 zsiQ}6@_YV*`{g!dHd4I2?ZJ$eMyC%Da-D>PrBi2}>;6TZwaupwJchf6_cMe(HJOh? z*Up1ePOlF061XjrR(+G@v2>b)=KYsG?~De}oZT_%)%|lg*@+c*-q8JtUY;(*Zgn$D zwf5o^&q4d^n07PSP#9pgeoOYDikAQRHvju~2jsjQybG;_eBwOvFjMf9_`g4a?0@(_ zv~Vu}|NQ%3IywKUAy# zHYTsbQ2Xfm3-S5zvgWTh$@+;LI>Owv2uVJA1M@u?Kh$TvC4G;oEtS%F1mKQTku2$<{)gi7YquHr4LT zv|q(%5~QmMrxufvomYn4gt4=fw_Hj+xEM|5z=O}?NwG%fKoD@CNYlUj3oQG(QOh>J zt{dfW)y}Dnvm?t;OS-q!ID9b;4dEuC32D_8x9%*H$?5UX@;!(945PY{U}L3Xzv z;9Us%AI=meCEqvvDPV5R>)pR5->dZ&tL-NcfB_flsT<}yP^gpF6smFXlNHwGOP@pg zVA)J;z=pOizxqA&U{ZZ*hN22ya&`+|Cy=yrWcIqg&^ExP`oW!s&xx)08@l%`fEeq~ z)4o9NUQRI++*MOklZ}1KuwPm?oLIacQs^^s`QDJ*xGo$tI&XSewD3VTYcUy&y~MXzfXOcuty zzW8#!;sxvKlBb6<=fDos&mBaVn4ZjlU-r%MBO@k565O?GB3ok$$EeJT-NgjtB{zgN zHbn*dH7S67Vcp|%4XavT^Zv>{Ru{YKwa^g+>elCkxPL%k*?z=}A`qa|sVSTS&L3_6 zzPvi;=8Gqql&{vv8iKD`>YU0Vq^DpUgeUt~o;&6dPdx6XKJXJZW>=n{J4TrlkajA7 z1JY8Gg;9#@t(A{#erhrD=sQIl&GU0A)T9ua0EDn&;o8T1p9Ss(^KUid~_Nz1i!(y_UyfJl+(G4`1m`yJZ_=_0l?aSMB%O z+zI+d32~$Y04GiHXU3*8pTdM!mSmHHUKXJy+K2J61fBPUgC0w69nuy4&TkR@A4m6v z*H>;2_}M6Y^jH)C{4_RHc?k2ox!86BhDr0+6$bPrR%#VhAv&Hwnm5hIu!bHGTP4r# z1DSpuB@m66IwG-R-9n?hOZH_;qxXoCQ<5rt6N3pYJbo0U$stx!RQX;&z4>vPCf{nF zrsSSdO>wPQpLX-KI$g(`ThE};*DzhYvjPW|`193qpEJ>2&HZ_P3-gZ7+~Ah@dDr z%^L=%>HOAmiAlUiG37O*&z+Ghdmw{R{Ry<;JBaZN%Zr3dWp5mOdgbbv3pnpeQ>wps z2BR_uol(a&oxuV+V*FzW!o6mAlGXzXa@?0 z@iwE+VdtDZoXrL&0?U-Hy=nM#@`H}vXi8vDt&5`v(_;#Runh8Mz{`PGJHO-WC!xa| zIZuS>9H(Q;{odw^@Jh>!I8d#B`{xs=em+RXKPm>#SU5ZTI7{pF!Tg+r>V{*kIYUx* zY!_`!pqp+d3|B=2?Y34>JqU{NWV!w2%?EiiYcq6ZG@0;Ay;f7Yp3X0zR`5YtELW)FR->^glpRTm(Er(sg$ zb}K_tP1<~{$^59^hNSJZ%9A&CHwTaE4~|0`b^M%!J}Dl%|L@6n;j;_nBW6k7X4Mwc z^R2D^pGGxqrs|Ca+tDd@FH@U!*ac{mfBgXrL=DwjCFOUvZ+#8rI6O7IeL*xPn?|4a zG1aU}PPDrE*Sb`1ylZ8tSeh7buVCnHHx@gJ4Mo;95XIQrXV{!!7fP!P?Jes@Hst8? z7;7tKKg!3+dpZac3u5@%`O+crZ(TQ+Ki8#>Z*dBIdHLO4TQudlottUM;yF!ft^=bn z5q)$cq0#Y^)p+lv)Et&RPep!V&H3lIa!k5HMrN~vXQ{USBwqCZ5=zwrMKHLD+mDYx zWVfB@FZ*s?y!41tQqsAYs`;{3mb)9xvxK0Bpq?yPz#Mb$V~F%fJnB8tWmAy6u7_(X zM8Jh>*d#r8zkib!l=ih&@T0sF?&m~R$a%yStUiq6Z%3auI+1gp7OBtXj&-^gI`xWvlbO6K;Hhe_#CddeFma6cW@Q#2BFa+iOKzm&+1rnQLt;F6nQVTDjFj|c z+7#PJCFxDsu%Bt)yLiO2$I+#9T-hW;ihiDXr0QSfybixQEI2$pwk7p~`EzHB%;cQ@ zLILNR*v7ZNfj3|tFad3qp72RC#2Z!#SlQl?aJ4bd>v>z!MapaD-|ufim*lWM=0Zlv zayJT@2OB!MzvMK!v!eT+Hc^BBjjVZ9fFUU-z;q7&Rb-%41p7QRNMFKUQrhXWj=KFm zB>uj6?4+hd)sEJ)3mvi!#a)6aj$pfGPnLrB58Lkl^#fV~DpeuLn5IiB2kLrBou^aR zcHNg$Utf%>J^2aP50tshXgZ?f!qqu4W^l`8!-1k{%9}Haw}9|E5%fLH9x!--0Kw^F zfWkn=XkXJ{pgPyD)ifHg#jHYsVWF5A{5Qp{8G5ei8ex97GowvwUgAb4$Cg95ivQJV zj=?)oF;~>wl++9ZNw5r1JVlc(boni17sD@=cGo8vCNUO$x3usQ2xZiUc+Jo^Nf4$d zy6U(leZtA)>0eu7PwX1sevD0=ygHkmt3*56sC!ULh@3|<5sLK(s(4LYfW7HC@6Gp?Y>a(jnSyb(Oj=E<2t?Zv&ou2zS8`^oWi7}r{)9g`;y_3(>X1mv0zjZsz)MB}V zF}LtP2WUqGqfY(J0i;;quQrOQ&Y>mH3=*NwU9L%Pb$p%_Ic_(M>3TqB_6o_lwZIEs zL=dcOK~?hp57OG)1y;U}lMi}^5c`th2MTQRl^E7oT=rt8%#!X><(B%G&?@Z00PSQ> zB{EhH&F-UMVsvh*dSg7WJ4z)|+R@Q*YPS||cZhOBWg{PwE|a^Oj~kbkN)jeuHpLSv zF}_8`p>0hvo$wWBh}b-GoM}ES#wF;g^RaDana%2`vj3}#QQyp7ov)I5)N+)0x#=eg z^BXemq>mnMy1xE-#;TW|edzRN-h-WQnLtl){yGxpis9ut!QT>l(X@kt(i1Quv_Tn3U z6tIFiRY^qNn3nWrlHz#|zjN=nAoc(5?ppcT$>M}DvM?cP8q>}`9{4t8W7JsFQRc4@ zp~TnB6iac^`dj|=6tN&<#qcSY>Nf+}M#H7EJ`RizMCo?Gu_l0P$xK=iV`W4WbM1il4uRR*yyk**B-@ zBb~?eCh6yY1Rdy8(I(o?fK{Sy{vC@C02l#KL=tv#4`QAS9MIA1ZFE&D4~EfIa@ ziS--Wu|C4(v{{KAejNc3CkP-}-345y_A)`vW}%QI%6-fSa8G^#nMUD{CZ}G29R4LL zj>v0|8q*S%nxrh{-q*bw&bJ`23(GJ)%-I016TWKzert3*iED&nLlAskrZl4UiOE}; zc7;)8Xhd#(RqgBc&qi+cPGT)8H?ruLS)H1pRfh+M(XyK&7Pw=}7Jt!+yn)*eVnXNG z1wuOYP$>aMKsD!bDFDdb0od`VN*&-y|5D8Xrcnd5tFixtT80emD3L_++Ex2_O_-Q7 zAmPL$I-6h3_jz=6gS9){vh1b`s?fNA{}y({FO3*hhF(tw|1m7}HN^p}6+N!#)72^O z$5!}0KcqqZK69H=nzR=^9S7h3o_3H|03|p_z&tr7!4~0rw}a2js>={{YY6bUH`P~N zxOUd&VV9@rH|c-&Oil$;*6+d;gC>wWmwf!mtqcHO%m`LO65xgg(m*Zw2LCH1G}?_&?DwQrTYUgav6kc@az6Nssmt! zB7TACoE5>mjVtc!C-GRg{rQwpsZ`fyrb5A{2Z-@u<7qE~$w-+`-@63R396if~_PE&+KTa8j&~A*m2DEV>YO)RBd6d&r;T+%ejZ6F?_W-)IoFAyPz;q7X)WhoOPn=qN?X}d_7D>5OsuU4|^ z>B53{=n|#9ALFK@9b|z85H$dosQ!&nzdO74&oBS?oA~>U?94=>opT+_rpH&NdE$O` za?(HRQ)`Kx;bFdawM&U8wsw#327m~R)VtC6Q>@zdXhQ&Y!PLa&W8SR5uE57z{&>rY z!sp~)>iA+Ong%~s(-b3t=S^}KD{3{-@F@H zb4ZNI2EiJokKB{eU`9HU`_Oxw4MY^6ssyUd_PQ`*TaTaGvVle1pVK!p?Ookt@;ou_ z{)edVRwum&fXSG26pUd1&0`3%b*wu=g zox79g)ZBB?_(Dixzf|`=>o>`+nee9?d?L4RD}DaLe{#FYhg5}JELvS4zdsM|WpVQg z)jpF6R<=<=GtH_AT^p?*cV5K?K_mUb;hgStzm8PaqP-Fd0241ya2Rs9C#Y`3fS<3e z)WXb>O#Epi_B-sD<;N~BBq+tk=6Uibf`@Umapm-&%sWTXwxY^%ur1lbOQ*)* z#)aNE!JXV2*+P6Az9yJD_dO=zex-m&8Ow9jHBz_-x-XNtP3; zX#m|nP{sb*4~#xxMs}yo^C6c=$UO@ntFrQMvH1G^Qi_ z)aHo_8#B1gtekQf{jsfLhra|1MMr=-!D%iYl39RTiq@9vr%t~4OShSP&Pa39Vp;;s z1Q4w7J29?=N75^L-=R^x_w_l32Vk$~hB4^$Kydj1@W}kJ?ymO`oON<|9 z2P*F5AAXR-cmiyO9=+mZM5Oux#ulC{GH@#gWzJEDrW%_ong==6N5Nf7_7~fp3$Y$~ z!;EH-J#ni*uMgy>RsR9n3IINU#S}-72$VrIWW@lAOj&PBwloeLs_YRWK&r3v|EUW9PsXt16UASPpHt%l7K`S1 zi=RTGoIDp&MS@u%X8AKn|@vxiyTOeGR(D@{S1EnS9ANB6oqILG^x+IX>x#!2740JAwrgj10~CYNa)@z zS&BW@|7ZtUS+>5M`X&P)B%>Pn)|?k3Ik%M+BOo zL2j0AE3M5-N2E@mSG>$2F?gjuUPbr5U&h8Ku+FrT6VHWgvyL{rhKT&!FTO@{3*8@|`ML-<@a>sw>9hct_2XY@6n{hP|!5|4l8P<7Tm6lE1Jlw|g zsSqy8iiSSVSD#I~}{=!Lh z)o9)e83}Y|V_L;C=IlV3ezE18vBv-m+{4X$`TDnF7WCj1I5Nz9nFl!ux_9aw(-T}x z;)$w0@P&ImrN7)LBbm&L9sle0(X9O5*_h*u2+p06iFL0d$l5t)=RyPE^1LG!lH%VY!g9Q8XY}UNZ<=N#cZ{jf* zlb3JVh5C7J!W=+ul{>i+z*k3jBiq|3awmNFP9+0hZ`P3!H&6CGW{z`e#bbai5`&z3 z-2ekK*-|Pp+3?R%t}(q-r;l$|rybs-+sfE{H@S1GjpA+OqvnmTs!kc}pZ*!q5$EyS zu|)kMpI-B?Uq?|X6QDA}F^c6%jftIpL{X7BNo(g4dP<%LwcWl-ET`+~S(lb}`Sp)Ni3H zbD{}TB`Xl`_GBKWlS3rF+cZ{47Jo;50`@cC>vilrFdzX?z|)=)hi_U@-{(# zf3~@-wlGwQf7JvT3kmF*&v_w#jB1HCq8wY37W*mKSUpm7eWz?;H(1fcVsG&omWKc( zO6SBtS7gAz>f{J?b=;_rLG*C;X#oOkZu72LCI&b&eovaEY?qnh%2{TsIPWzX2cm^QA!NUjOmi0z4(4Tn`lJrvlI7gyxEgvbela zcJpFiq_;wj+hELJ1srG6A&DU);1RE`l{sbydf)TSci)?Q=^NgwOs5K#%AMATgjP1_@Z=xFXI;m zkxugEp=vSsx}nD%cD5~H!$ZdP#I@5cd?OUc_Syk`C$@yoR>P_abKGjm$JTn)QnFZE z%`5%(=UKTU1RQ(1g4D#8Fa-VT?PG?u90I!t0R*rm)oLRQnE>y@T#|DMkn=f*jk%Mrs{EGmyl>MMVBVf z@dtA%X43m0dfR}T^}})!?00IbM_lYrRJyzqFLWd|hZgDC=`U~y9CSf16WF`>E)5Z3 zc{bK??_b(qwZ+6Y1*I#_XqpM1)0ErduQ7_ME@nX97%Y+Fj<#m2I#A2(+7{GmR;*2%fHXjkFA3@ycsKj0tj=ilbP9d&;KsDW%0T`I5J_NJR zcKdPv*FdCy>CDmYH_I1ytDIFJEIbehWt~xIkQ|;jzC)5B&#Gf1aT=!Or0Q{NNT@?N zs)X)gpgYsUQ;?w0K^RXE)>a%VK^D}?vsyEkA7wj8{Fc~qF<$(rN7!>ktnCB-HkzeS z*l+q1FP$S49`wgrZf`zniOjZE>$lv7Vsy@k2{q`gzmlKQ-1$qarwpWS(TG>Bj+Fy) zD#h;91J(wj%rfEkgrDuR9@Ty#VA}m&?j5FCO@A2f|D`MDz9xO!2Pl+7NHQOqI7sOM z4^XI%J|Uw(vn{xq*7vCSt4mL)PQ^~ztI>%n0dk`=KL>nyeyi}KcG1+t&(&TQFTC}) z!C)?7FA>@#13sf_NMMVt4gO^AdCr9fHOEsaJCdNqin zTTzfw^*5#k&ozC)0@E=)D3t$H8lCGaGa+auqc#L}>}7b1x3B7^w}&9TKw^ryv(wvm zf;+wO5s$IV+`KJqv|u}Sa*2S3<;+_Sxq7N(W}Yp6!e_B1EFI7- zop)hi=P}jB_@9}U*e}TmnGUYb*JQq-s@|hbiEBxgzs^bNc$vI&j8I&}!>)ts)DVRn zK->f6*;i<1F!c5cDsvCY=M#x^=moQ80J12Jx}XOi)(NnmUC#6U4e=4<3v`1o$;`X> zEGv%|0PFz)R+o=`wFA;m02E-2#+y}tm|0U`xALu22odqPXi3$XPQ4?|ow%VU)~PP& zgKf`9i}g(3lFM9r;3<5w;#PN2QO7+9qcVjCf3E2qSO#tp!$c}~zAahFc-vS?Ja<GYN4DqdNA_tn1!^%#uQF?bEhzOT-Gjn-9R2`b|HKF*5F`G+1@Q-p_}ib0 zpG=Dk@2ec4BsTt%u|C?cNkFI9|59C8;t2%GfEi-DS{C@ONY6l%b>qg-{R_|BmL__? zINaG_;i1?Nt0hn@K+jDO%?BhpQT`H*o2?T@EW5H%D_x1LK-neKG+iY>tYiQmY7;&=Ghk8LF@b>V65L&EGeJJx!G z2^?aDCYk~&l)1Mk|tH?rgc&{(^Q$?)6sHtQ!k9{ql`%L0s6)8 z=`%kh_WgTYM_X4PhUBZHo2EUZ-d8=wHW}0Q!>FK6n+7d>^3mPdm9vNUY|5P_&+*>ZQ_^?UJGUS|sp&0zJtvFvQ8VCe_XQ{iTbs_SE|V^!I-3 zMr>xz2g0<6R24q1npcinuSvQc(y)nQmVB*g=DIoEpUuSIgdD3!5!-=QP7aVddpG^( zzPlr115I&VU3>zFu`t-^CA6=n+SUA;>Msd2j@lLni`jVy#Xm%b#1CkxCA?0(&}aGD zA~q3WQt1)tZ)yE))^%@NHT2-NoLFY@>5}Z(F4gc`%9RjJ)i{m;+Mluj=kP%R_KEZM z{QdDq?{n9#Tdm2zvU|K$Dd^K9kpPN7e&8Klfr&x3iTd=CL&^L=3KjT1)t%@>L6*oI z1RY6A0zcR#RFHfw!CHH3jorVV{UHQ|u;yhdp6u!o^Q@qOPp z#EXAub$8Ej%L)t@G;L525s8!{-T(zm!#uDGex9%6Gp3xJ@vaj}GiXwW>yTIBr zVmGI2gD$5&64qAHc}EpY85Rry&X?!_X;S)r?Agxa+ZFQ?_C|0u<>&Q!8yLJ$0YYOH+Q^q49~9YE4;p7F};+~``H6U)PUdr ztX%vG5_zKFci4tG|HR5&Y}(e+fL^q(icM656NOO-)E-Hs(}N z%yeYvy)it|_pmIAlC*!U8$O7BscZ zAmS7D{r3wx2$u-llI>TY_sE=a@t1Q`F#JVV&RvPH^B7@l$7}GFY6nsoS=wgUcznkWY6!2H$QzQ7!_sMi} zRX5bpdW~OuGP~{pR7|b%UOb6)T@LX)~5xk9i3evqEu6RLVcQL8S zX8Kt-u{}PZUm|kQjB^4-C)aAmXj=Xh%t9^7imf^}-yzXf=VFSE#>H@^3KRw38N9*u zJ9CY2k+`Rh{3jBD-X}MRt|RKa@KmmAVFgzAZF?bg8SsHVjvL?Kn}T(sZ%O)!JZ>Ic zN%7+ao%JCuB5e*%S=N{w60ZioaRAStWP=i}t8M&t>A9%m@fqe$4&JLMvyU2H1g_E5EE(a#om;VOzhW3>5|gQdczC`S_APfhCVF4bATeSmPol!)Gh zlVTt4&e*wa#?@5j#wzDSnVI4*UY=&gA-rx7)uh|{D^f`mdycI5Sk$KZ0_)evvMMA~ zk1G!+v*Je`M~r|}Lh!T6uz0PZCa|Mk%-A?bk}wbEi0HH1jaagsp3E zu|_O3N-tR9ps$|Sp9|kQx&5W)g;MLbPrA8Cc@OJXOS0gP2Llbz!nH%aZNvk(#7H!s zf^_H0SDXuqV9m0{cNWGvKG{n>%{@w;sHA(=NeH6ZvPj7^VqOZwdFb#wP}Wj8c%R@- zf16R%UD-6_DoaW34=*Qa7Lq64vC3haa9L!kWC2p*pw0c!JV(ybzOu^FTOr=HVwmwe|@snMhgrEB43bcd)S4_@NNGFQYSZRK;jm&uLtmxxe7XG z_Riv#$y6AiH2jHb8V#^SM(1n}g5{~2TIbPP1$Jp0aihZ@JlCe$Di-qnrhFw|s7vY= z5!oB@9a|ae*Ro4_JF0F`_K0qtyhwg|DdK)2=Dq_#Cg?|y+@#kNb)K`?6rt`K$46BZ z2XBJ{18fL^^Tm>pd;-v)RTa*6V|;6(+R>rxvTkKjW3a- zQLCd~hZP#G?^Am$?XPf}ri97xEQL_>c+T4A_`YN6M^X{h@Q-rmr$ndKs7xYA7kTr( z)cE9Tn^7lyzpe-zk!qM;$W6g)2j$+17=CT4G=Nn?$8x2W)el_n(a(lq$D4{j1Vg1k zSH^(XC7sQ{kRhNjZO2ZdDP$RR0k(QtRV{D*`Vl)W4<&5bVw^~9LgT06iD>72uzodFB)BGvyn-r{jEdPc^u zqnXjw;?>1x{atjr#hV=8IYutn2VeiLXBUVA9_NcN<^W~9SA5xwg^iE@-6RV~fd)2= z_*3Q3l6Rmu#Twx5HuGJrC*;oc+>SuHoJ!eH_}uGB)f&m;E~X$(W5hvKc)1>Zp8P{y z*v57LIWNC%{4L8f&9#X=!V|ij?v!5RfRMB+v;S)WFeyvl~ zan*Bpf>?)%UPVE8_1o_^V#f58)R*9U-@r&joqbgSxK^MUSFNH%ZLKV|S-urqw2%~% z8^-1kpAkZ57#bMI+u7jW{;KOwoa2yQ!j6yV2?CoG+#x-RJWfKt&;)RX0HYRbLlT(4 z;DZl{QwC_lB@V(0t*nC|Wsh4VTnPD^5P})OE;HuN^0QgMFiy}2IeS?~d&IRNT@&40 zicqzVz|s>n;@vNV%=gYrg+Dt$tlJU&?y_?+5%%oHyQJpeymR`i3BJwyeh`BH!DF0c zr1^Y*O5x-RwkA~>nJ&IFbz_iT_G$37S!wqv+NUEkRofvA`XU{rcTKAXw-MrUtdX;+ zqinAODg=SZ)TYwimd&UW*QTe0xf2a%@>zTb5X@KmXrX@~XH;bvPgKgdN37z za#j`&rA7Gx0>WbVky~_A|6li$Wat zjEa5PZ1JYUDyG0(V|;JaZB^q{HLGx zp5Mo0N938h4o}M4QJ~MFF>dDZ{uK88XD+&&?k})2NpCbvmV=d4otkLX-Ut227Phe+S+A3iy`0u?_GM+%y-cSfwuCa> z`{(neu6tnIgd41wD78W6>pFj@V%;4wE*Oj|T)?vK)cZ(w&?e3` z0+f5=1)69Wlny?(2)QeW5exQr)|E$#^Cy4)%#?ZbaZTnz!^;I^iiJZkd-8DNc03;; z61Uso=N;?$%3;8hGwgnksJ_e7o!nw6JFP21?@@zXN8FuY>flo9}`5JA?&Gi|z z&zwO?$XY+Ds|)wpx}|@ZenDwmCEL%u*!(;`xeJOz1u%G)U3yplg|S?dn@94^Sc~h4 z&S}Nv+D1sgsoxMfSQK={Y7__`QNqZVo3csq4MJs)GrS{F`+@5IR}k&jCrZ}t{47ZS zlA;>28)sw-4*|5-Vtf~{mI4rJFreh&*A9)wX6E~5+9w#y)T~;ku_nXmXB$5WaxxoE zxN$~P%Jb~_^E*Qb6BeL!+Z4I`wfrmY;L}d?66aiX(q9NqyX%@|Gqdc$L?g)#7n(I2 zX7cEG=bURn>KbjNDSO#jisbHD^XYql5D~DAUs#wfo`tc4L4c3452(gA#nwDaqHIh%R%PDLPX&FIzJE>p~R{#O&(Onz94$|zax0u9L`o@7Rs>R^On{R zc7M<7_C=^nAGjHPEM7L6;BU2>c_kaq=&UU90z0sZaZ5W*nU)wZBL+XkllWR-;iI-2 zmpOQ6`6Dj<#kr5&Ef9R}6WXGBcbkTW&Ve$mI>3+6sL+OCSQ%brJ3r;D-0W1Vpd|Iz zt8*1@C8tHc8$$#VF|l@N9`j2>$p(?IHL~2J}OLMTE1!6Mr2s4%hToT1BVpKtoOnR z1wQLZO8oND8tRvmThy=Wb6U6gjot&SxOd->2UdrOF{YLvf#C-4z>+m9)2Y}PGb2RZ zpN{PFMG7s5(HY;8bfhzyD-9v3kDI-pMHBiER6`BeA}}Q2k&}FtEzmfqs###!;+4St z=5nRkJ<&WGGy3lysnq8JecmwzerPf!q@5VR$3U~6HPme9d zhA5q~1hZc4pm_5xvIwcLL6;33$QPEodirBt{zdkR?p?cQsbYdLNvHo+nLt2%2!k;Z zO+A5!ok`8hPu7}F$Wt4;Hr5SSD!K$eyEi+`+EMgeMr(*ILBn&ZY8uN3Gx~!>-MrPD zha*k~+WvHl`a(%Mpo-~kE;9mEOoC6#aryKA*&|~VSwIkzpdcRnlt+-LvCFJMh04Xb zLZ2KSgyuP%U$ym+Yz9G>`P&ZkWvRCcn~GzKj?p`YkUb!*Lc0NC*`;vFV|>oBc@7aw z$&yMJ_)pGGAy-CG1N^jv3vKEQ@NiszBEs&FgSn3Fk)1Mqg$Umd-457UBIkUgX|)ok-tU;aF4%=p0{$o-Dc1WcN_T)ye@Hf!mPmaJv zH2blYx)iPkaaV^QkJYwa^yRk)a>1{<*BBL5-=Ck{v{wp*d4 zYru=WKNu>tzPiqTBtq#=42oA z9y};}!uiqwhuE9%_i`H_}dlg%q;k7gmiAyUF9*DtiH^g9%5RRDzwFggK=wa0WA zXbhZZC&y(pFg6_%w#x$KhKFy}5aONFc9-&MLS-MNu;8qh1YRn3i6p6g+0g?)kt*W8 zkNc|cHp>_OBj#Yvf3siOk$&j0^xkTM{QFPY(=k9BM{ZLKfP*bn*okdZaR~zKLH~t|OCjaVozTgi2=ral z+wJ8mY0pX?PV$2&pV@V>2CB=*k89p+hQ9u814>#FjZK7~arM@!@qUm4Q3C%#F*v80 zG9*?EK{}VR?-bG`Pmt;Kn}5P(y3}r7c~vTZ#MU?=e(r9zQT6j6DMH{JjPt(rq)(bo zZBzCX{j=_l#L(ooUrkPPE>aDP@PgBao3H8uXG%5#l-xXu1o9DQCT8Q?GOb?~M7NG+ zJKW~=?iGs(OH2{hF5hZSMD~HnCSR-&~q-ND^Gk$`UPy_%n)mv;f*xgT(Aa zbUINkx5Zwq^+l5QBQH_LXQ{#=+3!4?F`62@h7ufL%fgfd_yE83fhpEDsTP$2-vcuR z)NHIw>!+Oaf>G;684ngGSS5t&fa?fE6J%ey*@Y&i7sytl6To5_s0OG|5Ma42yoR8e zkc<698$e{efK-J45+zCC7_i}EFD|lSf}MGz5{hZ2G8QQ2JHlC+qGl`ByBX z6NfKF0+o^Hi&)-yP{h&VY8s#A!KUwHyj{J%xm?`6!(Eq(xijhYj_KZqhO{lYz7paO}HX1*# zgAt~at=fqNvWMjddc6@1Y~u2PE{y{2Dl`{^CicP=n07XS;+X#hUnUu7qU!zVG#nDEUk!rzw~OpiLSHgl4vAKRX* zNX4}IJ(XwML8l9LCD~NOB45AxOcVllwGgDV6M8G;$`Yzx4)em6 z3hN1fYjh7cm3N4hXnLYmhj3kdX(#QM9CurPaRdOtHnW(s&b@^zT&GI&kA(!kb56Zh zu&Ix15sHa((_1lwcs;Lf3D6;w&LJWWP}7h>9!#F6#y zs7~FXI;S94KsC8L6fBK)fgN4SfRVx<0?Nh(Y)AF6_QIxjt3K{v+ZcaqwAPlZ?gb@N z|1~Q2qF{H61tFdorx=F-JZROS8=IgO__0ko$@~q8Gp4ECP=pSs+WGs|pubrihkua_ z<3fe*L+L1R2UlFGy7LjzKd+4mx%+jW77=DVEfP%ol<^?=43SI%IQ-&QpG?v^k|Yv^ z)_}J)DH6iw(Q!tboxRF?&#a9XMaNkbtBLl zya>QyPHl19ZI+#--?$=coxsE?_+Gn$*vRevz(#J(y z1j|hRQ4mj-d**4^Ev*bay`{AfXE=qEbjClub4c<(14)hg<9GA~5ixiRAGv>(GD#u7 z%(UDu&d=Nuw++0ORgwPB9N$fSle}m{*8U!3U3Vxd(+_MK+t8fg1C&o&MA_g^=5m??L$ZAsks1X1PgE1|U8YX{9R)aYp9CexIFo1z9(a{D=(*GlAHOv% zwF*AR##Uc<7d0OGov#b(jeaplBJDXB0swybqRp63Xb4#)=V({U&Q|eN$4zPJKA>#H zRZ;92Ag(2;gNNuhgqGZAJ9TeARvu$9d<~Gz#>I;NCYu!lve_r9o$020QaE~HyA!r$ zg;XjY`;$fF|D8qte&FoqjLxn!F%L85%=IaApS|(xM&ja@{Rr3&-CKK1_2>_|KMV<% zJd*H3k=$y*{Wh>wfq4Zy#|E*EJCCx>oGUTEGm<1|uXjE}FCi;-$%5xX5(UO%CP!iT zPu44Y?|QfGv(CKf#b4oO+Dbg-*EIQZY2!vH*f%MFWWp4#i~PkBxyp%pBev&1Z~J-n z1VLfwy+Bi4hui=5afDy-3+C$5>|*w8*mlOz$v=rZ^Pj~1j_TkkI0SQD#CC(v4~H2CFyNV5 z{TuxbuydrS)$ywG*w#5thm7m`7tczqj)cSo@5WsvJ^|PD1j#DQr!*X%U(#{T4DRsg zF;?l!lBMr~D$p!wuinFM#)5|rj2GgM4*->NKw{mzOeh$k z-q_tX`DFo!e8~5Xl?QO`uzVvwEHip4}G;nV^gSQ)-p~IvD$t0<9tF=Y6OQ z8&=`jmsp-qie4qiF?gO=M}yT<|JKv_yia=6)^@BmT39^U1-E~ixtP)q;mcmkhHM!E zu>@I6Fpc!zX=;CkYNB{&0?hXlQf5eA$Y1b1*c`S!o-shP_G@xm?d=`wnoFUMV}fCW zHOue!i|P(rB`M*@jr|2h1ou(+dg_e$`$faewHxJ}YW+&8*^jB_pf8$pqJkKjF~r;8 zI_~+IuWGE`hrI*_B9g6jm1};CAudmXK6QU~^>(BUi~X*lxFD3ptdg<$p9U`vG6Z}R zq1*2@m_B6i8tKsD3;wJ{#C*#Y*Wk+=pv+ELf>pB<6HphB6DV?uwr%dR_HT&V1@bX# z^a6_Bf-u}_6&F$@8|~y5Vmbd*I>qi=+se0sfrShRu?O`5`4Dk*MR}PNiXq3wRio!# zb1Q%kDS}IN|60O zJ(&8Hf3d%TZZusq5N9#8Jq7b6A_zioyv*Iok+~n-w?^!uH@1FBDha=0Zg!cxu4&)z zPZIlXexUzGacY6%+j32o2e=Q9sm75`@kc^@V7i7f1CLd#ZLG!gIUFhHrK(K9O2)I~ z4KFF*$3yZKJNm*WNruk8cq9gtP~e2lufcX0>j7`k=4DP?lJ|q<$OjmfWzD^CRUmUXnSF` zP+|}Cfb<(uvk#R2UIe%k%$@L1z4AC4&8qcHwXxDIuWb!ftzYG$lZrX55OtuoPc2b8 z41|YG@cahzlWKz1X#ZQUt4wRoJXE}i-$!{ut32D_ZONky?}hIv!IRBiJoHSOGo_x7 zd^B59RewVou29}24ieHqLnzb%J75C`IoJowFIt=N3=3q0|C)IP`>g%3aehMO7VdceI$*}z^P z9oN-V+RYT|a-wM7!(9YYO5a!SSlv*f^4bl_a*Em}D+Ta;CZt@;36RsuUz3ksJSyeS zh-cFuH+O64SiV#)!ye00zQXZ)Fy+y(FE3Fxbgsix12mCuWpUTIzRX&6b=oob%^A|%r2JHlTX znr<&m7`?zZ_tm70Ue}H3jW6*EB~wh!4hU;Nhix8oh5E~=boicFC;H5Dpz<$Og>&vx z%Wn*QUzx?b$?~eC=sWb)gE!~9Y^6GU&_wBbGkZ$9zagREg0QZ^5G`n3tz&3 zvU=MZu zMN-6x5F`2cK%?P_CP!Nxp^Zqd-x;iM9d_R7a}~ZTe8=*sH*gH)WIVm!g8H-}VK*$h z4%bsRfIm*+IFxrQg7DsCi}FJTg>^7#j!RAuaL`C$ismmD{>kfwW4rr zy7jB7F=TV;=g&(PcC#b~NRX|0bx?QCj->>0y&p-Usn5(A`-?kWHJ-CQ(N)FFfTx@{ zL{KxCaApr0nnPJa=JN>+bPjyHR@*AXqJH1@rVjVEgX`1e2_Rf@q_6-brl2^GKTQ_E zYJpq_c8vc5^3e&D#pl~^i1jd0r_Q3uUUhWW`e>$6IMq=F3jvzxN}It_&-us9`XwF? z8CrMf2inEg!5>Qm6r||lBBq~ZFLQQ216A7o6@fFb&JfLaOxH*S$i;9%4ru~Ox)Ozk z8U|$&X4>R!rEni}-#LFYt!j)Bk~OS;9Zp}u^vRJnTk%)ZW5k#)|E|geSsD2o;_{9j z`d^!6k>JF1rVwPb6L=ECo2CftxJO-crK9gZy#=PQDc=IKZYCb-^H|cGyZ-9Aq}lIB z5v+t^THqgZWpHXwJB~}4*LvXMDQ0-!>q6hb8E4JV;NWmD({|s726fvrorJG&6iUhwVZGo%+_aUg2jRcU^^~odZ1&NMo!E) z3PNiI2?vM{HNbFNZLRAb5-+T*bZTTkycFv-jQ(KHTPg{1dJGX^Ic6oix0pvQ(1kH~}kJ65V7sKdlSvozY2 zPEa|4S`nx^qEXZ9MnyI)+?R^-_-=Gs-u$faeIjm1gNGs+8)Hf+JyDtbg>1Qq%lY%DDoe32 zSt(17F(4SX%P6xRbsRp}jXeTe0U<`3@|BWzDAb-DVRAzz{$ z>kXLBS=y{MU9~<@6lPXyU~9UoAEz>G^)YQ!LzQ^fxMoqs%1`|Q{>-r4| z1m}eu2X5wnq(E#W(4Z1(-amH4Q?;h5E8t?uA%&`uE?&8ZiTvrDdX5j)Ln*raj>^g3 zM4&q=6UbTGKaa;5SOq^Zc3Kz&W)|=~q#UsNDl}tWupt@PjlbI5TT43())^M{eOwWi z@ZQ}Q6M!wM8Z2zn3y$k}3DMVTJsur&GQL!-qH!u z@YxkM4Cz`N-qM^Lk*%-DFOnDGzCOKi1glj@4W{Eq$UUmhN`OIg|$dNzAW?dCb((W)xmSgi2w#SO`YKB0(x-Uaq(!aA#Iev1g;LIcT$PM%_Vhp3l|#E2#K6{ z!J_yvboNgosFSc7j-dbK8{5-}@|qXh0h0OsoZ!h4~0$hS;7%F!H3+Hx!+4W0Bd#4n3xlX|ZZHi%gcl{$izQva>l zfqZ-i(n;j;krrO#-srAPrPJrC!rZ%QOfhd<^aCkLkb?M!jmnJZ3t1lg; z7PE7FQ*YBG^Iw+~5GMPZ_M!kYi-9T*0K59*jeGqN|H*CDe2Y1U2Eoz1>I5w|jfw;^ zqu-Ve3OlEP62*u=@fm9k87R;fS)(9Z1jwO!r)8jLX1kM3F~?&cgf zOS4XF;ygTB4>_pzgpzLfsrBY0{=re3bYa(>B;E28BnHFyXvIhsn10BHQPT;r6s^t6Z|KC5xPxs4>LR1$k|LkS(P<}Sy#X1O3;m$FVgtV z!+p55>GY`jXohob$|aR&BU}jMwy(ncBXAnbyM6vd%ABL8woa^XE!Z$NZ+-N03-<>_#vZ;Z z6({p%JnX>9{z=SeY0yQLHaeoh8@RDL;b(W2G ztCO}q!mFip;3ZGa1YHJ>Ni&50lg3b&31+DngTOX7d8^*}UvE988+2L~j!>Uw0sh$r zY2&)OS;y{1X-_=TXgRNc0_Ac{H8Jaw{Ns64g^<{1VbpAT=`Cx^MfhT2fDM0AKOV%7 zNOe|UGmX4C#$Op-J8O!>|Xf==c!`uurAX+`w-tdF0UYz%;H8J?4|X90MeZ)rV5+ z1XBFnmD)x|2M6$ed&!E6sZ_`7iUVIJNEWcUT(eGEoy}M!h2Xkmvnbat zAhF(mAT*mkH3Rj??~W<@^A>Xt2Zc+dwKy~1Lm+Yx2*cP6 zA;m@ON#3vVpnCM>Pf>Jzs(<-Zt(BK^_IT_a(u2!GSN3MPp4NXxTg$yn5igG^Pjgoz z2~x8iSnK@Jv{uJ@kMlOIW_rSd8%ZCQLgc8OHmPPlFQ-I#0UKYHq77E-ne-f7_f;*M zIc$mD9+hfF`O4h@F8piZ?{8ZC@{@144=qugWV8e;t>e|cn>#jA)JX(>e>6HjXQakk zQfX*;?^eyp8iC#3M~MvsCZmgz&R#Zl?6k}ShOgMsH75VbGc66ypq|v%J>K&&u*=G^NUAPwq(&P|DS7^SMHmNOdau=nNvQ z>z>L#*uEJPwv8-hbmHj*sC2KLyD4)qPfh^wT8*Fl+x;tSU%u<$(uH>PrZpd% zTru^uxu)~nC?4dCE8r}tVu=Z%B5=Z);V7Jx!0I#uZ-UYw{GY@MX&aa^(W=qstWjff z9C~5=+iS8R8!O{BS?f+&xGh1g= zV~*mHZJLo^sfYMdb*4W)GQd}(t-hP}y5XFroxd!&^F*?TsvQs?C-|kVl$k#}eK77^ zE^=$rRBOwQR!vfaBJT{NasMhwed3B?3l}R4D`C+vTBcR_u!rz_??&n{dC{b$ccwZA z8DPZ!_JoFHD2{tMncl)2?j-aro2>1M$3KuS%`IL~vOyixQa3%XH(tuQDqzz#Eh^p3#1mCBi5dFs_? zKYW$Vu^2|HUA_Hyj&hnu3bTY)Tfud6(gE0KuQ`kA>3L+i7O-2a&N=@E-(LSEqdu3G zm&zOC^LlE>l2Q(v@rLUKkQ^v+ZFQx^TebOBRR3G1kd{fiwZ4+>**0hY1>X&>>fS|c zxE<{LADMId?x~;R;+XphP{F_pi^cDoJ!+63^Uaxj2@5uFPEH$#9Y9tccPJFX2ji-8=AQxIX9;oQHuf%o4KVS)PC1nar<{ zfi+Fbx<+a)e6cJT2Mp1>J8CU30Yf3MM4)KE zW+hD_&{H^x#9ptQ<{uNi-0jkN6p>#W^kORWA#U^ktQB17zg~QuE#pV?EjAnJ&kSyT z@!)G!`C(?NR(IZ&@a&1o283&}{sltDpnmwwAa~KhM|@_!{ncl_=%wg1nq4wpnxQuq z*;o&d-C{c1kkn>V_J$@g>4)2szXt6?M_!m}i{^^1I}94WqRg3e>g<@(YB3~P?7=-6Q(*RysO6Ik(9gMu`h|sieVd-LX!Vyoaw_(2 zzgexf*~v`2I7A2c{GVz*aE>#R^Q<$gZ)YkKWsOu?&UsS~lT^0fhk)F(db`Q}KKb~e zS0r+2i8#I>mou9%Q{+3PJ1{gNZL?C8VeE(C2&2KEB^me8Wmd=m6;;qkhJ3}u&)?&A zWn;q6RMt(-({KKz$0&QT7!0T!5_882?Q#q4k3J4F{d{UKg7&pbeyX_=1DZQ-2n1BE zhMVRPhORSHtaSq^-sdMiY-U>2uJi#-+fJzHzObXtFVU+U*y!lBn$7*a8TYF$K_vKe z5p;qf`Q=bOE$RX{w!2N+5Z;U?I%O_UH?(R5XaE4Z5JeyYP<-2DC7~~q3FKBz$JC-Y z^o#3l7*$+VQML*TdG(Vox_5ltk5SX61ZC-1}lYrZuDVSM!h9pKOX9M!c8~I zOWav}kOd%AR>JUtEa&a9?BTrf(e0a`zD7}%=Hy(F?Y3p%U4h1!;q6XU@gsjbO{8LL zrOMAkclx|h)UR&}t=-XW{@`_ao#P*f30R=$k(&kzQ6k=)LqH|;;}voo+CY&aRzvf!l{EDqUaolYc?jx4VfxxPz5b3}bQEc{Lf0NMzc#CBF z%Qn5D64ZnQ`@K>@Rn!R;T=#D|QTlabpX7_(^~I!GpOiK3!0M&}iRdN6-{(OoXLmzP z;b9GQmDQ2U86r1F?(xJwEV&a(1jK1b%ORc@^q`Bq!hm>e<;vtcPn`&pjF{zVk)JOQ zj`k@5ohT{zamaBoA5 zl*I8_+H{=_TYcJ_>c0i=Xna>HU;ow4JCdWD4HNqH|GjE7Yzx_W%pU<@P#z3BDRBCM7MtOtWoIJXXhX zs(}CwNrCsId<(_4Z9Z8hgY&5d_Bq9)LnKlUb!J@AyNydCIuAz_B)LPJB7(+6#v*w6 ze8k^gQzzx&J(u&yjIX;*QOPMVaFViMqj+Q$PFn%H@G8M_|A)wdgSPuOP$yWFlM&M`qAib{M;J5=DZf zb)2Pcn)iUX-{jg(P%xqIy_PFRruC|V4qZ}uOHC4=30nRCgq1_qi?J35mTQN9r69%~ zXMPJksZ%UY1CrbbTtoxH_##=sI#d4@1YH?K2h5S~jx})G&%h(BE5?MS*gkIe2wx>v zAGxE)ZNs6WNOjpR?{dK3AF3i1*rk0S*t5xuvGCcC1o{i4HI z4oQL~g^>Ro*3{g~rDL4D|02GAdCZ=ESyK@pKE*=h0Q+h=IHW_0okL3(-Dl z`{E372diNlht)&OS#kxVTtL4)%@~Ul?A;=CeG^j!m&UNmfgs*rh!Vxtm_L-kXkyFhkbWz z$Kom=Q-Nbh2&u@#Utz)<2A;lZ7Q5lP~(sTG+cwm-bkM9@Y^2&uL1(Z)Lg!aiD%cEsgkS@FPe;o!bdB zQi%>TM!q>0(WMUHxae}!jA3VrNp9bXSATBk=juDNs*CL z98v`0zb4yS(Lo>_FvWtuqQOdq=b1tG5P9uf)P;45XxqyAH@Yx|ANqWeS%?(}iT${b z)H%;jsS(^G_}|!-_8QVQMb!tG)#M53RA1_1dq9$jpfE~i_v#?c^&Q+Uyj*hTHpD5A z;~6Syq5hQj=6Qx%uOCH8bjB*>Vfv|ht1y&RL3>>tdz@JC4V=3VW;C0M8^m8n8Xcfq zrjF8o;T9$qCfRR9sw-mu%X&)XN7rLIXSSVprMLfP8%wXtb`1#a&j>6y=*z}uXxw;8 zZ(Xbi;ph+gvJut&bxJ7YX9X5)ERx!1k(Un!sAB05I?Zw&wH!5biZRtmmbNp@2P{fy zld5L?o%Vdycop>btXP<})2)`9$^4h;nn0zq|D>_Zr_DBgT1xB5eRq1}2g-4NhZ9 z(twP56N@~5bHVJ^4th02y20Pl7-PYKkI6T4sPU9ZHpJ$ozMmI-BN&_eFhRCx)C4%~ zO7)i4Oq@Y;9AzGcNTu%f+)Lx;4ZZSOwp+nJ6+emt_-H~q(>Y=g3jb8f`m*o7C$UVEj zT^SPS)PQ;qW=Q`)j+xKIfFc+0BC-sY2a76u*TCOQZhCeREw(0}<3pEgQQ@vDJl-TB zH^g{q_9?xOZIJa@M(F)fNUhIQR9Bnw@fBE6P zLB1d`1ZWK_@i`dE_z7gN6q@n17dBne{F|P$^j-zBLRx%K=&ahbKs$!ws~Zat`(i%d z;{#JT2@sdh)NglpT1QUV?I9-r*Z`z$Ycn6xF{~i8yhDP99Vn8*M(yIg6_UN(TKcL7 zuIgT8;kbRW7gh$PW~QvocBuLEMoF)(99CDiBJV>Sy=2E3QvN-Q(sHp$iL$&p3O(Cm z!5N0;r_~VldkI43>YW?zWD0B9_@tG7p5>$P*uuh3EG%tqs?@zb|3K!=u%rBm=EkVB zM(4(3S_N=^UeNt3y$b-1up$iV98%ucBL zkA@^84bPWD3MTGJ4K;`x7ef%AedBe{&rbdJ+LD!z^%rI!6bUPA5#7YZJogkk3w>SB zq+COx8>B@+dPuC-AYtxY0lW{_##2|gpf2g3%-}P4E`Fu;*~~;Fk5pNs5Kpw^W4UZ9 zz0?gS@B~(I$GrMrq2!_KIf>kn9K_^!5$)5y7m8E5Q&&7u$443=MXoca?fp{Z_e{@Y zt9gEYhpvMGr;R8;Ao>g#C;aW1{}G4=fu%K`YDKK&lfu&%jGvb_B-yGndt%QVa9CfP zusg;|&neyTf6W2S1oSu07edTF?u_4bp8e3F8%Nh2sAH1Ig7NBF(?32>$|Ue#roSSGvth zf19739JauUfm_d(ufl#7MHuqq>*z~?vMUUQm;&@-D~D=_^h{9rvZlhl?+a9=t)9Zq z!~1M`Xk6kl!8c+wIk0fnd1NOj-1joq&1G72js-KQzy!WMj}U7DIR|i=t?cb2F~eSWxr5iN{QDa4OHi!p{tb$Q$h_GeSEdP&=FbtSx3Bd%J$vm zW!>#8)43M3=Gwc*p&ZQaEJlFjTRt)jG=>eIG?tH)%@&;Bw+S1Wkxn&c`FYf zMM(nL%@6WPND=aYSeesQX2)<@GWCHrn@K!FgdsUIg*-b`SJCLQUB@70z=LtSQAxbZ zw(GD$TiAxEjo#P~eGSf}<711_)0-eM*^~jkz&*gdq*3M^##*5VTNTARm{?sS6ojZE z%7vt;d);3?|J=)+_}e8hLt-`*;hXvg0+HPo#cY+#Thapxyb)@WwP}w&E}V z+5_oYU#K%AFv&FUo!nwv#0!>wBg{efSuvUC{&UC;82m2KfdiLPEeDa;Qo))R{t%4S zHDBvX={D%G(+kScj50J@+&$$*b~6`)Cj!UzKn~{z5h6Ck*o8@bo~Ro?yO#|&uOiB; zE{-yMF3V!8?-UdTX&zBvTTW-im~Rj`hPOqV1aFlWNAW`r*^*S+-4_{^#5TCD=arU^ z^VHNeR>P&73zdb7S7Ww~f{D0a4?L#I^-~NmJat5N>j8{r`#KXLE+be`jKEsi(s)2T z0i3iO@im7!)cB(o98g+dYnLhITMXZQar~sSc89G*p+4^iP?B3Ia{3zrq(j5cKBa^q z|C0_S;y)8bzF9nY1adOwsh!0%#-Njv78~^1y@Bdi94rj9^!|B2uTmw|@=A{#znW5r zQ-x=w7yQ|qO5DTG5~^mAJC2Y^pjm)=>xvdytH!7`l*@byAEW&IlHg0sRtfFXVD3IB zdb7;c9v7Bc`R?mzfZ{SjLH_&vm#EwAAE=KzxsUy`*VF7sV};P!z_T26EX+v@?&Vug zA{>yPh*@CA`xmO^EUNe;gADqTh1B44Z$fI{QHyKHGIW{}#w}p0%w9fl zI9DstDKw~9&;wmAr~|YiWf9l?cmF`%73tuDL80M%LDn{V2|nrAh8I~>!=R}NcnILT zn1I`ED2KmN^@Ukh-YF0v)1AxdQ(YY-?m{i)@EADRt#Q%Sl;v%v5ru(%&l&R{5($sZ zl8>u_pVr?ejkbfwHx@x6*jjKszvXxw6|A&|=fgsm*CPT{+;R|xtwVSpy{c&OtkN*5 z@+%aYBucsJkEtx(hQ{I1aQX&z`-aQ_m>1*y08`_x4R_zKnH*lK0{g=Wo z3WtgW(+y9qa6gW3*#(l7*AY|Ym6ee0JIunZycCe81Rb|6cv|_v;!ZqZLy3h$5BI?D z$(kJ^g@a1jd>MX1ih*>%6Jh|q(C!xrndkdqS=h=Oxqn=(s}lNkP2B3 zUQ8~n;ZNNw*O}Bt4un*lzB){pcokc&v1u~W3;U%B5G!~U9Q;q08v62j(NWV8G0Fai zH>8s0v?+!YV|vudP#J6Ii3cP}Gf)~w`?m=Nzi%`h^at_)d2tQk)P<0CdVmF9Cp+L#Whr4e!(V>({sY51OYg(;-y?3;GXe=B z6oExX6)+)EK&{bu)uP!6GuiqpK-%?kVEUpOqC!^Gr_^PB&F*xz$@Njx4w$Z|f+jz) zOcaUE0h8Um9>bh7ZannsnS}rBJMwq$W>3HTE^otMv0B=OxQ$t7=K7ici}C!6^YowH z$9R8UL6OgGq&B%xR3TPX|Eh5hOmbEYs#@FtVuk5nkVD-+fEwusRL({gCHXRc>vgW> zj1({DTrH{o{@Jjqn$Ka0F5VQB5Vp8Ar@Gv)!L+62F+Q5Z0hv4L`mH*DOMjmiE#F*> zwquJJ5SR0d8bQ7J2jWB?xd!H4?UExfc52fc%>m7rM-STs6iHyR9NnP%-62P^B23oM zLjFkFjf6p=%dxV1Xj^Z72!xF{ai+9E;0!v_a1-z8VA7n-!*={jhvxWefEBl=g4-MT z=WmT#4a{?cU{9ze#V`@3*mt7+v!qvu(Z@R!X3G!8O$qkIZAkFS#7#?;9t>Ht8@RcfHQdydTs*F^k)|%f~Lc{((sSFh+NM30CR$ zL1IOWY(JP{TnoBv52XrFo*6T&G)X%7qUD`-ncAAVjcDw>+n`FO$>E6rv6vy2G4g9q zyc283=NCdnhgtH3Ag$!Chl6g%hBzfkuBLi;|h3KgTBighUZ;a{E!(tehIgYZnc8J`mVLuF zc+(PI3HC!g;8Y51-w<>yxEan^_GT6_-dOjMF1PBzZ?9xIKdfU1-)38KwX@Q`f2A^Z za|H}>enR6aC1wIR!1`V(jNOxv> z6}-i#f1ec|A@}vH=0H<;mkP)4@OtvPyyTo#D#ztQ5Tv1iD#dl(e&#SE+be%@lV(KkEfBO-m*dd zyrRfXNpeWU#Nm3$6w5P_&gULHm!|$CKv)12;z6J{Y=xc(G6J1Lkf+-X$2h4lg!E-N z#h(+=cP!dUBZ|eBW+S_WjWSjfU?-Dnq8{)pWT{l&5l}L^%QrTn%?(?*&ck>^AFcMZ zdz&6Y`W%u)rE?J(e*@I(>?+TiEb+cX=R_Zw%60MI0^f(mbH|3WUnjs6Af$flAaE&Q!q2ap zP!T9VUPl+K12fH}zl5G3()c|Wn(tErX$Y&#Oic z=Xtvw++2})X4(9oE`QZrwRjqABdgd6wpy$vgNVTb==md&2IbC;NG8 zb!q2g-|vmqpJ#LMfmjmB>U8}DzGMbtGMal)W+f+d<-T0=oJsrJB?=>T7gSdtiM!pm zf|aK5cQJ8I1P-hYt|J_6RHuq`4DU7Cz3Jv!#UT8^`tWjW;MRiE-y@uLRR&c|28ZUY z1gTHCvC7fPUi)b~O4;1(oJ)R&8w~%>ycl$hN1eT51os|~S<)HLKN#v~;dOb&%o9}& z=3$F6+h3NCQq|+y*_gX6{pK)>cerQ)XBK zBC1Ve#VOG3%`U6E`F-sUBKcS0-m=}D&z+hHGIRZ2SdVs;mt;v-YV`0AACb4u1oI_; z<1|Nbk@(1(zx;O>XZ~dJk7h9X;(LZMue)q`-m9$AD03l!-jn}w9Ge4UB0k$dn|-?B zW~0N?OJSSgzB3-L_u3J8X9A63nFj{jApQu>{sV~uiN%HdQ%Cl)(+kN=Tl>8C?RDG2 z*Mg`1q|r+3!NovJ9v-Y9k&D-8NoG|K{cF_rdE)+TbZE6$rpF+=zD%3~ciwe$A%rVX zor@sY;5{rQGdhG&O1hltF)<32Ho|m7nYd^+QaKfbhZe)nBP}Ruzn6QQO*Ha%ajDI}b+49VAP6+Ss>kjjgV=dGeAx zr*iHbo1A_EF^muJ#yc!?F{eWfu+pWRZ@frP#1|C0LY^FdRQGvLdDZgnAV21C2H%OB zs7`O--M|$}hRrDQ@oB!}$nTf@@{uz5ebm*xGQVpO&@qHE0k4m!z)ZrGStR!90c9~} zwDlyVHR6nMs2kL3FP;mm);+#@W8W_=;W6T#)Nv>LOJiw2h$tihVj>`-h)qw+bhXDa zsy}5>42DoW2Q#t%k>2uJ;GwB9t>&Wpv)Tzx1)n=e&}%PAUP+X{k_Cnbw)K}jz0wT1 zPf2DFqV$1i|0qg{I6x|{jVYgSuX6Zv(B3bY7vzpzdX@KlEMm}cBZ%gV6~p;%g-ptv zr(G8jhUZP9H%pLblnCQp%o*wsdigYXsr14;}i>Hwv8_s4V0Wgd=N>6DuJzE;apo5>S) z3$F9905$qG{^h)L`cvo`c?F0|`A0ifzc2IxpIn6mr`mA;uAei%Q|~`o->xlLJ>UCG znsGMSGe&fB15>)Z7p5S!WPDJ_>N)PYqs_G=WAWUphT>YrlNIJJblt!fP7iZXX`t8` zAtms_4p=hS&(dcw@fVS@WaDmCXTV9eFujs-6fyiR_>V{rNoIuh*Lj?hyYF)Lbu?B5iA&{@Y5B#Y)ypH`b22_DsjV02S8d>wKn(Wp{FX`Re4Pu+&Ww}_H&=`1DzQZh)?nE-63e7o@@tIM z6!uc*gs+diOnFIs38QkYKJbdqfi7u3@_*M2Dd)!z4;LxUB!(o#!rc+~Zu%bdzJ1|2 zBF*XZT;^VmXLs^ZhqlQB`hx*_|DB?83F{CFBn|E+kP zwqDH$OKGY33d4->HIzP{0n^${psr~c50ENqN6puqr_FULxZE%ZF(A|IZc_N$gnE0G zHv`u1Ke%n@p@5`!6&Z10TrihbPq@=2l;k<1xuMzJIRm@Fjq42N&_k{WD> zGL^TD^lcTGtFvkd-~ahDzgjUQ7_tpvc)eWkV@E;lo0&;JFI}ex8_?-_W>Do$wyI0I z2weAQ>=)@6d(7{n(LE=lf+=?(Z5{3psS#TPPW{m3L{I1mi`PZn8F*U_Vug@z^+Uc5 zkg@u*Fl=zGR)z(lrKFz6H?0mKa6u-+kK_ z;Agk#TtoEowEE2#u4dm0|M(l?vp+}DspuAUHc=qwwKMQJ@XF5Hebf0Sv}SJ1uddcF zB*9Sq4b)vQC%n;(rE6;0J?oJ2zpd2oO-Jn3B#7T;=IWD{GXzV?aG{`K&48t9tUle34m;FOXh@ek^@*FWxP=C7BC{=Y+KR z4z5^TUMimX{YwsW^?;-SjY*CJ>TE}$p3ZE8ry4k~57HUl{TX)2uYH`mJKWN&C(19n zJ8MMi(?IeTlLZuogK#pV#C-m}mf~3H=Tyb*5T{{y#$+aY&`mS*{ySKyv6~uVzdIsm z#63cpH-yHZwvF#HL??rIXE%Ut+-HN*#;C`3x~z;3?5=1M7u``Id?2FgZy5k$0QU~6+zDgdoX#Sm3l$&Hk z-733&Iohtm{{R(biu1#Na4iNGNDOoopQHm9V(vrxX-9vsVTb4mNW;uoZyzI%;jzf&OPgUzBWVs{p$?PE7%M`_OfHZQk{toMl2pk zEg~0hU!d``&?|L%zAMl$InMeYIS&4w3BnCVEu|VlowXdLrLsq0a&)m25UKzJ300+R z()Gp2!IYO4!~2geD>Y#^1T$2oc3E^}dB)gj?>q*~TWGL_ObI^p>!#9;l6MbB%eFr5 zS=d%ZS>1?fB^%f4?xh)9A_B7BP+L7^uDd}X$&o4;p!b+(w+Kv??{s0#6APwDsT+tW zJ5tf6JNI_FIv?%}G}>iG#tHHue0iR6GdZ0G2pr13PYWbM>7RdB5<=v)21&oUkJ&jM zbzZK7&1imL`p|&5NBTN;e8nxm75|R?JE&kkSV)ghPCVg0Ij(FIw#$0dtwRXo!M@Xkx*QefqUoK_`}jV{H1 zZkpjN_hHp=3v^)L=k=Xoj&(C>VvkK{7`11m zyyxu7+mn~gpzn5K*fbN@sT1MjGdjo>Q6!R}A^fW)g7yybt+gjT(Iyuph!o!mpb?s% zUW>FdYTry8mgk=gviHl2i#roBudtQCa2@`E&Tx*PK_s(+RYAa`(K2(NCxMT9^%mKL zHzin}*iqyV4fe)$_eHAd`;GRja|CQAz76)Bm%pz6=$gJw|S2mSXS(A3ZWG$;{M6MY@Ql@757 z)2V;4ENo+kSa5^PSi{XNxy(>cNJ3v8?3HKFmio)X**>o{IYF=KJ zocv{WM$<`M@<|^JM&m5{L7y!SEj8 zhBqg*){jGBvG)|lKd5W2Mt|3D{RRU=WP@N({nYogLgLgpb77#MaMU^Or zA9M>Q9U(Uadcu4e#5Dq31{4d>cd?~t0kG4be?&M2c_1>2Dyz|E@of+{S#akLY zVKV%G-cl~6vXu@Y)5I$M+RKfjzx@NBb%N9lEk0!0ZD$mnA_n2l2>*V_JM&wz?R+a;~V_+ZumfI}goYPMm2Dp6cSWNfu3WrDcmbq&)JeVK_@W(%Ll@UL`j%-W*QS0{3 zxE=kop634DW~Dp`#P0z50_1aH!tmmiI%9MuXJv$fm@{Mcl{tX~wRH`0<*qNj3r%-; z-fL9xd{HuWJ(WMxN=6H;4q3kDXeld!D?cTIB6i$g{g?Fd;4G@HaJ9r3wn0hG6qoxc z(L(7ODzT8LC3VrWs(=zAeli`s#MtRQxGbQ3Dg|5|Dre;Xj^d+0%Q)66ju=lngQV*E zWdS!QhnLYLM|zjiW??f^*iR;VxG=R!JjkJO|9>DCEbX=-((%Spjn;Zo&~UsJv%(U^ z%AenNlQ6wOwF;3AHln?%h*Gv>N@v}+ODZNzoH}-Ic#8?vwbvK64VEBozBu8gT+>ZK!~COxovsWY(Gfs?XZ{G)R5UYgN&*5 zR$lx(mSsKytA<`r6Z}R>khd5<6+Sb-YD6hm1%AQD>B>c4cbtR?=M#7I^i(kuzo|XQ zqSQb)Hi@w>fWk)VILz}ViS+d(n3@T0#~UrJLpTqJj>;MS#_bqlCE9|h z#ykW0TT_tFnW#U166MjgcK!I>LIvWMdi)QBz+~43KVd~7_aZK?4&K&Gu}%0gWI`FGi#c^9isuE2=Vti z1Xg=kwEz`N+=^W+A3i3_*s-QA6~M>74AH;L1edOw8zKaWC?w*K4QCH7vcP|t0KQWI z#%wxZT>MKCexl@eOlSx`FGsHa_yfn0fN7$}ARIbGq~<5q1SOqDETXkzQ}T-$VQh1~ z%v@XdtyTvR1AdcW63jlly~ABmF|utHLQm82?$YuT3Zju<=$-GVG5EH#1mX1r@@N^^ z68C@oI~XeZ|JnckaI&Bsk}4Ek(HZmne7lg}yeN!;Wu51}EO)R6SRwm(V5kS=RoxI8 zoihue&Jx;J(oT82ndLPph*l3sS@vSbd%;DA{u=PXPmVJFtBQ$Ag1(5WUxDYQ5GGOi z8<78)c><6kf3!$aM>bH?#!2f*P8!OYkKKLnhHw7s^L)i_ZbQ|lUZ$tcLRxiWuB-b$ zcW(Gc=FFHn-b}W7-Q7g`8t7xm(9}tE(|_;2dH0z*vmihVr!ID)Qv>HGJIuv57Z!{~ zexlQN`JNtIUZ>07RV-;EGCw@pfo4k`wyI!oSJC2kjdN?&5>zr+!V{+Wf3PSVDLU2R zZ)1Dav5k>AL*e1-q5cXsJJI@A6WqUiN{UdMxgVmgLb|rfP69bHd1&$wa{LarIJ&z& zTE~yPUWc-Kd89WeUi3^F$^YDHR-TRP;qk4;`wg&4R9M;b^ag^9y>!Zz`(p6;&3KEfokGs}5j#Q=0nGs{KUk6Qdf^bxXp z{cpCz+tok+ovL6Z<@0xvrwUHhYtR@)`>w0;%oEF|1{Fo z=`oy_RqVsQ&pmo~bH!aDx~FxFTBScI)?~(XMo8o*jq*?T2ln)Icmq+%S)m^Rv!CUC zbBJC3@KCt@=?B|vcp;tIu`|;g69?SBnOXgqzg3L;V`jROYe#Xjwk>-E+ImvmQ-#Lo z(&|J?i-~=-@(hDTWKxG^tTr)qUrqhx?Q=LdZa(F~UaD(cxo}`x9vqJsmj!@odFy)%7s&*(AZMcJ+*d=l5!7LCs2D3kyDR*W15WvgDi14p#2n z43?h}`t6E$iaSX_?iT`SC8vp?t7q_E_%GMb9P+xqIX*);h@`^rJ+cdK#-W19nt&VK zm;mb2ncAtL15R%KcTwDOq!$~R^=*c+lm?X)4%6QKsqX`BiD}Q@QxO{gCimSud_jAK z+^xLjW6$^iG?^&v-VV=z7WhjI@!P~Y#sJ8o7!y0}CTcD0%L^Fg+T- zQhIJ8_{WrIqbBBB&OUUlZglhsGR!tFnlJlJZhGmbJRHr9&q;0>UbRZL4TbTKrRkD3 z{{tzdO!x7{X5P=3Sb)7K6W5Z@|7~eN_g&cEUHQz>b>?f!?&!+nN)HcLUhgBP%gOt9 zj;-}593N)jZFn%K+MSoqB0vnmh+CwUd4)W)NT8;qyq0_3R7Gha!}>+>@et|n{yN%u zGmR@{fTMlqK@5rU#{-Cu)QR)bmT#c~-&a$!j^V-UA85K_#*Ngo-W=R4?9BNy+=b!@ z{VJDMAS_6xlpR$IR(YOs_&hBeIxyWa)brp-`XHvzmv8h|FA;bTT!YlB1(dp-nms7( z5;}U{;hwGvCNhIrX}D-K+5frpgf?!P8gH3l>mVCW?$wBP}(8fRc84IQmmfo zy&yR$b;DI`%HXl|+P`rlCS-l`+GC;Z7bA6VmUKF<^Pq)m!qT0yuhZ#Qz{>(Zu|G^` z-~aJy+37AZH0|1$cv_2 zUk#zJ)*38xj*q2hp!w0v&q5maa5G3gz;c+tX*T7M!YVK+CG_@GXL`QPt)20IQTLuv zO?F+kaF8a_6%?cd1w^F@h;#y?6cHmTMOsvlA_4-^13?f}qzDKoT@e8(k=_YNl@_Fh z-g`|bAwZJnZ0`Gh-uFGv`MxpEpYO-_gW-_jh}X5V_TFpFHP@UEwGu84KWW#1c#1@~ zXsENG^HiTsQ_Nt)v^R>Kl;4WSAH_qDM$sB?qG;g34oF`Dt(B@OdIE~PnSh?l!Yl;GpZO~k7~P1*AIx| zM^kOyjO$5R;}b}wKHJjrc>8>prs5*08xHeT8|uL#>@9Vd(SI=E>aad_L2rXnN#j50 zL?1d1Gd8+zO+{Ow5FWZ1&E*~(pgT3f9l%Wk*iLvQ7`SRYCJSxTyd0%bn`dR z-05BxOpx0r)wCU#5~tKzkXn7}=Si06mmL@SGD~BBR}`z}-S*)#h4658zfd7qQXt&> z`-gfCd!8^}RV~r+nh&iWm%C_{r*xCJoI!Ogf52$8k>?~e#FuM&LFf^$Fzb=7xYXM7 z4SD7vCyB=~emTh^uG+Glt?9=w@Ey|$fwR9ZJ;-}U_pIGFh|9+CbX=C!<=?C!-`p;> z7yU{PUUM}ufN(b$trWvG5@M_c_mpL=3}=`Ge~ZY^$Gj60Vg$ovuChFNo=Y zWlVd!utX+T@gq;#O0|o$+w{TW+>f+qgV+tN4i9;kac*S2n*Ng$#zmpObiXA_w-t1T zn5bttkaK-qM!a<8bnj1hWfaVY`t`Fks_AaAvCjY|K0W#AA2KR?V)=@_{+DN*S*agd zW*&TIpuINrK8T5*bL`Op1Q|g{fLHg`UG~nG&YyoZ-@oB3d{RoGWvf`6{jMgX{F`Y$ z=302ufn{o%i)eFy`SFvAIEfdUuyk}EUxae|3PN_Fd%^+I^4E>p_LWuk*Mw0#v$*Lu ztH|if(Ik)n0m&lIGbnttH|p-`tt4pCRl$JC4|+8zmAegH$PPr#-nAY}?UO4!-!CEXX*dDdFF>%bOyXPg9=%+RMlz1v<^v$5^t!FV(CVJLOC~ZE zoYe2n7p)6f^EBV5w)Vl61nj_|`WZ4{6~KqU%UQZKnY}k}moO;0E+)QpW-=xHU^a1o zw&}osB4I{AyThNq4a0 zjQEm-D(~rBC$3bp3G9n=4nMElX*|UttN6QH=7TbXorWZtHSr9_O}-F+x_9iVa!P4a zkUY>#F|u=Cg3#fXD9CAiA2F;?Adcd+RkRj+?R)DzjM$9@As_9fVKHu>lmRsa0rBJFy+_zakufB}{@wg7OO0*g=t zz+U{n`ez&JoB_Scsg*OUl6=jB#+SIsUynOGac69e;lISa16K8m;lRc<<_Ipu!bZfY z`tUH|GmqLa!zJM|0{+0Z%xaAOEw5Iy5U8F*302>V2#EiGN#+-kMzb!gq&w!`H9C1C z4qJCJGxU<;X>;n=yC5$gm@qjJ{OZq`$Y?T#l?=w49DiREUZ_Yb@}#;7A|6iyC6!2P zGtjJJtN~wvw57pKFBjJFyg}Rxz(H!Wj=97os~jDa$4ifzB!7|VK3ET|d)-q8t?89`E2tKgxjrSc{xDEF>S zTAawpL>l9-t~PJgN}1Ye1-9;s!Zkk z{rXeh#Qb(GZ#|QNuV+T-iLDT~cEj{f*(;j$YOe%OfrblvJ@h~TjlxT!NCOaGu{9U4 z7?FrWrY=CYY_(c&Od`2vc>4h|U5djL2)Gh=o|sD+j~0ORco+G$AJu~9RWB$wxqo}V zpV0{>E9BIr6nF=X$cFov)`U_)evw(Ivs+uzK!w)H3V+-XSMz^U&wdxR<${e zS<8aLKPziL%_zpTNw_AnL_J$nSMf$nQ_CM`jUzSdEG08OG=mQ2S0z&py(m$0x4q?t zCG9&JFxct}GZuJ4s8FlRsR_StgBl%$XQsY!oFp?NsPYJHvEUjR)YS1B8TyctR6f5K zADT~}h=X16aM`rk81Iiw23)yqtGal~ThyE#L4o1}0^m#H4ubR)O9SG4uomR!GXH`Q zs&=5EC>Ep=er0e0#@-+fcN>mhnGbHSe`yhTC8GdPoVwqmNe4+sV)Xq;Kthkno!b0I ziyjRKbi?KKi7mxm+1B?871$T}{FI{4UH4tT_Xd)jY)7le!^4egfI?Kcr`b71SFc4+ zT8)n)dzhDvZ@k-*5xSePAn>kRIZ{K2b`drLpbLFkKw*6iJ(Ty3@Si30{X(#M_*FAc z3a$w2)n2}*H2qOWq+La!eVA62-!-*#9>~Q}X-YdU*U47Ly$=+2Y^JGPLcLUk8EOa~ z;$c)hU|g`q$4(8lthM>P^I8#XKzA>ImZ9)sp^~;!DzbDPeg(%d|Bc1Y1h}M_sKjt3ZxH03FWDAe?U|uKg&> za&+|18Z&#w(X5tCC~?1ekzU|97c-kmmKq=V78x zn9I}KRyCk%M?`%-eAd5-`Xpmdz-hs+6n-)q0ka$d79%@Jw@WNjj&7^{e zjIK5ZpA1Szz#AS*JEkg*-@Z}WUqBU{_?{~fdA_11#!w>r%JHZW%iPH1e*OQUCL&l}=kXpUUc--dN-M(x7%n5r_{VLC6a)h62{Vs1p!nHm(gO0>*7j zP9_)%Ix9_qs`;T`gT@k_x1Ck#K)S*813%kIF0Z0*(YZvYFATctiD|RtAprubFP6(u z94@i?DW=UXUR+<5Y^?Oc$@!IwE=UFY2elUjc>KaYKv65TxsL+Gzt<_BZmmYS-<# zRNpa^+rLfm3riF^yv~UT5tj+xT$|%jY=9j6$xE(aBgG=@*4sZHUoKCiLi0|DL`~5XQD!v2mEwIbT z(HO9lxX&PnLZJA{IzdzqE-S!^%H7{~ARUIhva#uqZSU1{vQyXoTX>|N8s@tSU>sh_ z9S0L&h<$MrD3KX_H(HTZpQ%tX?Y0G!cRYtBz|i{}n=k5z{0iz=jVOLnJK=5(o*})e zbhydgqT^zW?#JyCWNrz+U6$f%GsKVeNMhpXhnsu8^R#iX#d!ON(O;Q!(cRn+-1Ovs zfv)Eq383*>w&1_=hvL)Os2E8P_&$>*cyf^0^+W`G0(wl~d7sZ!@YD-{D*Z0~>J5 zP*oZXGnykPU}nTb2R})@B^H8N6L$pvVSFI$w*8T!5`pp+HCO7LORWQKrFVGbl6yX|RL(uS=6ds4G%|2~oDa6(jjY~9cFUq#51ktEgv@4i;{87Q08!?e<-_pYp?AGe0NE?(A9qGO{R- zip1B_SRD+P)cJc$-ir7<>03WvqR0REllCi3#SK@Dv+|wfyPi?gS??`*atoq8kLsfs za@caNN|Wyc-_19r2IqYQFQV6KHxE#1mA z13hUq995*f>cX1wS+&K(=1Eg_J%qRdf9?i!wn~_b$xjK!3*Dnd4F1=`w);IJ8Z)?` zD$(%+H9c%Ns<`znQh#<_#{$2*8*Gj!PhR#C*8WpDmHjI@5@t46f*y8h-g@Tw`^Tii zUDE`?mmh9ztke>7G?)NX_mxs20B{W|^gD6rkEM(SUe_FXYV$X_3t8p3u{m;TT7I+1 zfboXG>8*<{J2A`rUSdp_6*Yz*G12)vid1t1fYo#Y65JWEvuCLEU=Wrso&7l_a(r@{ z@6D6zD{_~Ed|9(m`F;^EV05ZX(v4V}(IuRTxU1)ueCJy5oXH~^qg`72x!r;T;2+aR zO5PM}O5;AG@^-*ERBui6`3Z^yI+`j>?FtYZP<70GW=2m;l8@MJ9FHo!^{47;_`?Jm zU7=GAJ3x_vEV~YUyOR|WR#Eky1<0Sw)GiW-J=K+aVqJ%>j;&*Pw-Je6!AC@PZmk&u zb$_>iL2d`bL!ZVQm&JYiu&x9-mhPs(ixu2mft9#4blsnkB!13WIurkAV)Q1_;arvI z-R}>B86e4X(c!)?TZNz;M-!dD0OgieRkpjSOzMnSk3~D<7CHs>&w?m}1FTO< z2=x?jL^>TiJy}A@$@{a{Rc>$R<-kYVC#69I@Wac0;0>h0;bRr(`(}L^!^cdJRK|&A zD=6YAwFg663I-;BOj-g(yTtY$iBpZ9&BsEzPkdl=2aXKU0UdQp4jg(OU(cUZxKi=q zgG%!Ag+S$~@ZL7mQ+7z24Ao~{OJLI5Kh?RemT?{-4Lo`|12`~m)mfIU*!nzJ%YAj? zBuo0FPTSqEX`D?zhZX(;oE4#@r;~yxY|9&v4}hw#=`N$+A)8!^TyvZ0$CINRl~X50 zepA55-8G2`=*BRk1I{ifhpp_rz~(%Y71raY`!w@ljEh0&ZK2VO129V_f@bb|U zN&SW^p|%xt5F&?--WnID2??@yOq58$z-|}Wk~rfl63uYVVoW)ncWU-=Yj3RrF@SU? z-*?GB7)|HR>~8g^m?N%9Bz z#^BDnW8+>~jt^@^aXj<)iy^csZ;T2nY%2mcrsqe$5XRZrSjP{fw(e zCu076AV8^fm?MH?K^?#MV0;$MNhzFwBrilI3TNM9!yvsBfm4oD7 zV6WV@RXb4@Bic|t|Cp5*{xSM@1NMB)hT$hG|7c<~k9)*!NOS;r7`+|!4;z+{)J&74;>^Wz@Z4+<0dPi0=_G^q7CWbR z%t7mE!;13It`WG*I4|4GgGZ3otou=S9FX>uRI=7glpta?RiO5#w{%;K?#4I!5rujl zp8L_Qr4Zz|$o7VhAL{2gTf~>Kqk=ksIp8*AE}@}g6&;hZEMjuG3I2VS-vRx+`M zapm;Ii`hc$9LNv+-epXf`RR3N5eiZFO$jf*j9lO7ec=syp$a z)b$d#Ss~>s9&J0K8tMaED6;HcU~xozVjWNY$d`17hRvw{8lTp zg3q9aQO!>1$!qUWnb<7V%;}f~)@Thgy1e-rLEH1mE;dD5lF z$j|$M6PUkA)E^964I zP8KEqNft|J4y?h$F)l{^M<(d_2VclcO+9IBq>)*t&3IW)Ku3Ok zUcUcHOqw&hiI-8drBcsuaL&K1ujqH#aeB(pBz_@`$PQ`aCY!H zRdThjE6Za4OrQVPi5u5D&T}wm3QiHMP~S*)<^!eTe$1>=TjsfMI9pN;dBw)?>R{pR z9*e`RU9Ns&7kRcQAgleX*`#mQ{^gj>yBmb8-?n%=&1smlfxB^a)30++PqI)loX@VCA)lkZ+rYk2k%qw zx#-WPoDFTTRGnv#;we;Ez_mW|!_3>N@Z9m7iW}b2E+VO7#zGuzojO{(0=YX^aSP?I zRHgG*sM5Vo5?4EN#1t+L$;3g#kLig8LKyP{J_7LWpO{{Ry;HyJJ!^zRqtlwtHs=~# zI?t!_TPW;`;)(1$2!v+CH%9M56)YHbs>Y7JX_DO>JEL}oI(R_jyR^Q!0T{O#F*omN zJbBTm#B#-ZM?{dZY2q~Qs-@@Qo&J*Qih}j|OXuHqfVKlahbA3o1D%NhUaRr!U?u@f z9oqCce|D0du1QIh&E+?2%j?vuUxJ;~;7^3?N{>{xQZMw@pxEAjTRl_d6jA%*HvPvg zW)*d}haZD{UnrZrZ6!ck{@jWYdr#J?@IqgwlT39sr|+%3SJQl zuPlkYxDerWY&E6bwP#Bi7jpCKO6;xZ<}9nLk=K8`s+(k|due$og@nON|Zyz@FcycS19-UyZ`@kj_PP6U&g#c?kgXCD@$LsCS_ly)7%NA$>;@*SnJe%;w zBL!%ssEo^QiGSj6J=Q~h6<}Pj>~qe3eB|6-VC!xZwS7qHcXav z7ISDPuCS}+t|yXU#3%TPLRUhtc5T$1%G2=-dd^Q@UM|du=y!AdE%HKXbs6FP_d#i~ z*JNO|sDT%IEAPk}w9($;__n-Yej;19Z9`W4OM%^t^JkAdvMNC^DeLCKgyr&fBa08M zdl3f%99TV1F-N@%P)v0Yb>Z=TEm!#Dlwll$9u{&1;ztukL;$`gW0(i*ZB)jyg8^ce zwTJVT?Bt#8B3YK8@TRyu0aoPRwf8?XuSOeWS&0-D>KMOzb&u`1s!2saTYvzl>!)o6 z3`-8*u1QLM)B7IndAT_J;$n_^Kg~Nr^^8k++Kks*NhhRM&XAeu_-{M9(+aX0uQ=b* zg$PUd#`usSv@w6w#Yv0015W5!f_9->kNW2Mt*wXCAG}P!St3jj&}3vN4NxIjoApt} zy`wiuMl&Nfw#+L8tR;$WY~1=O9EBGO?yVSS|CP8=ojbCeXqDh0Ki~0o7A}!3)*o9B zv5E190Cm}WR2=mhf$kne=^yc1%lO^DMsGeC6 zgl9pO&}`B(Sr-552SRUp{a$;!!cwusm#Vf3$Dy{=FY!*c(jiV;xkNR(i zv^Pl6Gn$9u6QrDtKf9xbMEi{t1?cW7hZ{S6{qs4(2U%8RHAqWNwKP)g*g5|Nnd(M? zk;K>@GB?mej&|9R?-QdAR{RTcE37X{rVp#9$h%sq$K1cuVt`De#ATzfA(NDS7koN< z7R&}N=pAcLP)C1tOe`JDvXx|=k)O-I8eQ@7ws@hK18d)R4PjoTgYR`WDB(3~xtcTM zcBU1DsRHuPBeGJ?uQS{iaVeqwz5gIK@-y#>_$p}L%p*p&TT?~;sG5IUN|eg?`abL* z;$kt}-t$`|Lgd}H@`qc2!L;A?#8jwWppPyg>>$GXdXxv7C~3zKz? zdH9AlH7i|yPyX*w;C7@5PFhs~zla#vDZ5!)QYL*$dE>+|t>WfjRn=^rjDtxPQ03rU zu6n@g>hEgb!F5YR%8iY|kDvji*9QlB!CGsJ;CZ^_oR2Q&``R*7QpKS&%z-LhiTX{oX(&XgKT?*&5b_=4@bmyKByDF21}r71Xk!D{ML& z9T#9EEa3{~I}abRAQ%B3wk&DJ|Jpnfo@Ys)Vw7G}6XX5u-U}^Z8O9+0R>NRDLdLIy z!6brC)1+gc^GA!7Q{%rncpr=0)9nmD-ZlrVpA)Kw96{>PtA8p_Fa(_yI*o?xoy>=F5%E*<&9za zS@fH{fL|6&5!IMaXDTg{?N+VyLG?z}mwpdh#e>y5Uvm%$d*C$xa4Paa_b`_t)`Bz9 z->4Tgcg7CL3Ec;>f85Xh%LD5KAu4K;caG%XmE|zvoFuT3;lW#B{h~=b+9tb2NkaDk zqOureCl5$ZrJx@~4FNF);KdB9Kv6I>i6%fP%p*FKJuwmxWMiu_ahXNYFQ=X>4$}8V z8@>y>&ZKQ(^EkTS`F`-2b=EC%HyQ(Q;Qey}JitsHO}QK&sAl#L)XjdJv`QF6y-5`= z|8q6vGG7PgB3v~=;yPsVrBq*xw^El+9-=f9vyhcEJucB2T_F)e8%bKscSn^mlH++{ ztH0POi8a>)VelHU0(0PmYvx>P8>8dmf2{kR*u>pNMaBGQ(^N*(?{oMlO7K^z(b!$O zvmyPS*Ul)oGsu`X^;Q_hhR`Jk7;PD69Ga{`ohzu_cw!1sZSnW^c*oj$H6!nVNk0{o zR?#LPqR^KxI}g#FoWNtYjU<1lW$E45QZ@k{EOGQRu3?d!FR>L;&UwNwvmBxtxw~T- zfTVV%ZGn~r@IHGA3NBfbrN58FBmR^aygU9wYVy7R&brkI%+1CVOnB#<8NMNriqQ5e z6Ax?{JZ+C>h^0$3iv9fEu#Av`j#M|L?MN??{sr+xP%qbFiK$>q@y?+3z(uw5{W6)7 z&R(SsAv+|@7-wWJCw@=jPR(#SGh&$kTc+lrkFRl3S|zW1N+*ktmtPc%Ig6bAfx!+@T&! zcf);`>bR)AXX-8y@h~jj0_F92>Pwmf znWH*2mF!Fk$FL&LS0z5A6m88sc5ynze}Q}|Ud=Vo*942f4^Txw*V#`Obe-pbSknf^ z2@U#u(*N%BK>|XNETm7EQ>t>Vev%18oB5&~W9Ru>^mys@7PInwZz6Gr9Ds6=JA*i& zL;-3WI`bk8WGAy8Ow&Yuyh~&Ckgz%Pll1tTLzmMdX%ESpV;V*LH-02}#S1OMlmi)a zlx7+GSLYFBF$V=6a4Y3IyoUEOGSE>risZTD4`~etN@c3DOr_H@ken z=zpXfQXA)T)yYIZtedE4&;)ZrMPaz273l(ciq9* zC-p->z8GonJwP=gTS0t-_7)k}$wjmGrk*|AjeOhx7S^!Ox~Ttozy2pC8C9Dp4<**L zUp0+QlcjgEoX`l(ulCm@vu>W#HCs;We5Banq{yqJxW4%S_C$K0uLfZCRsh<{5MOjM zpgcZrG4Z9fH9@ zt@{hLe>ng!$bNREQKN2AF8z0-IPCwDXjT0m2+hF%1JkMi;6IFH=9yZfT}emHF!N>D zpgvdcbsP2k!qC|bpMrxhV1P)nOAAHqNW-X%7AV3?RHG_c4g@r4)Zo=YuQt*o53n#m z!p^b{Br-x40UCA~MOD%v%VB{C&zUsP)!2ZM=6##FXh8Vmu`m`8J60$%JIJ(W-|EmWx76Q%j=#d_HwtTU=xR96+tKTDvHbA&Hi9L z+{Dapc27sVT;N1>&^O<3W1v7aH;KnWkKPXv3tqbbMylFjXC}F8;{!qjlfIbUw9e9C zcA`sj>uj1!b91z_u=XiDDPcRP<7Kg2Qv^ z8w6j!a7@Q5G)o^REgbm4SfAy(dmLjrEB0>YN!<^A)_SiN*XY#J{YF6P66Z&{^k7y~ zeuL=txk$Ul&I|H)M0lxTj`y3J`bq0Sa@Hv^Zety$9CyPi5Ajs*xT`=-=V(%#_K zPj84sbyX%Bu_&L+n}ar)WU+yf9SEd;>PTXgb-w?!ajd140sm=yj*m!m(2t8NdTQeB z*mZV%zYL>gLU5!+7VJ>eF`%ag_}G9uC(97TN`8PBkcrfJrGDPqy44ZvzvC_6%Uz!9 zB=KqgsS55{f5aq65r;OTcx#xaEp45|O$UPzT2Zo!=`ohCiK1MA{#CdxSm8SuzH}3{ zc1nR75n6+ualnH9UMhxg4HR{MfL^Pcx7RGtqS4ATUrzL_V4ZImJ=x_Va;s~(@*_)X zqiR2be{{N)Xq${V_1XZMXpoJN>+V*6?t@UD_`H6kT+KU;+2<)i9~^>1L5mxhx?#yt zRc9!{Fk)^G!*>9CW>9@VzZ(QGNE-sqDnIPGx;+g@_9z=4CL*HPx?_v5wcg6p9AJf; z1k}bw@&_~sg;5+e&Ok#j_votlygJEGh1#3d4kn=rP?_dRk%p`_McI%0osEQ|dw_@s zx8pA;51D;t{h6BBjlmlUTbJx-;+f8G%k*FJfTWW6r=YjUyR{Hl_W;A|7|PNp4oEdp+lvwVbS_dXF3#`takeoc z)(q(KID*t*eA?w;RT@weGbvf2>Po<UY9PmR_zX}4DRZs@GOhxFp2{axh^>}8!7 zKzX7GovqFY_E;`i8&u1Cz%2U-_>?qa2+4^B%jekL%#&kN{Q2V)@!S=i+Lse_q|eiE zfia7Q=@dg}yYjo(Q!NDbzA;8f^oFvH$pXbsXDu8yvo(UkB?E0opc40^<}u+-gCNJ? zq#jN_>M~WHUBp#Wq*|^==BAJ!Np&j9Eb}YU0|!Qv3O=hDjdyGy67Q1cTg3X}N4WeDY(zjb5PV(>!$VpU-ke!jU_ zwYO6<G+)P?g97wq- z-E~CMLE;Es%nidlC*d;Xlpjp3nA_2ohUlZa5x##}8SxJZx#x%o+jxU454d=1lO$Rg zn3T7Am+hUP-A7hDpbTa>Cn`5D`e&=pJrxeBSWT@5dg&*C=4%xBbJX9Oug^R1WT&IMwf4 zT@4lE<#kwH;-1e^j$EK9MA~M?JMEe8@7} zd2&&-xTOnoZEa_?3%QLU+W!lZ>IDOR3;6-+C~Qk;2}L{wE=D-w>|Mqs(AV-&<*8#1 zse>lj^z;{sGBdEI-VUKY2Rby}G9#6vm=X5r($fAO^_otC$~pV>3mzi;Hdib-mvk6v z{D>d}jNu~yW9Ka3VsHD`7wT7!;i@?|o3y`HqmV1vJa$pU-sYd1X=pR@vta3{Nj;(n zOHP2ZQlwBjiDrkyz!Ot-a%r^eOf&=$4bXhsf4sM!YUd(thfTIq@v~F4tsdDoCoU)o zo&aUnB&9t4L=p(AwEacbzrxC&d+MU9xZjitABw5jK_T>8u#M~24dZ+2kEScfah@CQ z$$+I%O59un2a-tqD)m<}YN;ZdHXSv*Ih|20gWo>Zl+3&u$k@X;Q-z^Ls^BrN0(z0s znW9N!vt1wBwyJ4*oZWN<8^cxsvs;zm(gLUThMOhn4VFd=kcE7ro9SZK#0oEdqg@}& zCHc)pHBwNfWxYG=?h0O#MWTrlrg2F>$J!a-npm+g(wiC3WRvCrBt+Y7fH%}WwoltS z+XuG942*IdXs9tpsxnVw`G*^vW+LRJtzYdmjgBZ>*Jk6@Ra9$v@j^{J9Gp!)B=9QN z$!R1g*vZYfygwTJ)WXNpU1AMh*OW7B@?R~7HjTptADO6DuD84t%?tg z_>t^5)M{oq$R|l>17Vs}Muo`&klBC@|^$Lo@_Y)6n1v27G*} zVE&gSN475~<02o5#XEgod7#3*?_}~R<{+nC%74_s>6}yYQb*|~GSbFa?LpScD(0o z?hlv?XmaneCaha$P$HpaB=@^#%o&sBZ-ppWr&KQT?xH_rr?dYAfO#GgoC4Pua)E-c zFsR31|M?5jRv6@voqG02R%1u%8D?(_M#%Wz|B8+RG4cQLXJzIG44(DrlVW&3J=)xV zV&+0sG_q7_#dFIW%e`g5MdN2a;38jZwRyjP(Pz{7n{0=0a!l)i=Yi2#@NEA(3bi0T zgt}e%MweifbN)&1ild)*l4`NGh-7Qlrb!w)(_aNZb0&?Gr3r84A5tYJ9MC9NBH!^R z=HA?8d499T?yve|e8f^AOY{)`#+w0_W~*o8GMny?tekn#b?Qw-uPr?fAh~!8;RawO zV(&e$q`Rrdt1YkVZpdd#rc!2USeo;j-fX-IG*4z>>!;b7H^h}x$k(#p?L{-cH(&LU z%)IzwRNz(_yLFJ`_NhbdJ!-%PYI7A8;ctGZLMjHgatPBWmxb6=m0$k-@pC8p9|F z2%8F{qF8i`@`-)TU2)u#E}s{AN+RuJ)DjvZuHNO<7E)o*r8ENmAd*P6Hz?6R78hYH z`tTiAv?v3AfD<4v6RoRps*iJ4Mg*&!=<|2F-2PY!pVkw2e}!KA`0ts@w4qkS0JJqR zHA-}6%b{pUtxsdU5;@@Ae+8=*DjZp3myVVsVP*{M?vp;x=+yN)+t^Dlmc3;<@j6n8 z*GPjO0-@UFq_aEW6V{6TAJ3>TcR5;>#J5Zsinl%w7jdcyWa8m}Wfei#o@zwiuIejg zh27T|&>n;p|NN}4O|U6<);fmbMDUYNHC1&-&$0?`!}|-qROo-|SNL>C>k1vP_zBWx z^~+k%L_hPSp6e;`mcH=*&b2hhD;Z7*aX$CUc_Kyg%~nB(M*tOIIq;TF)S zo(z$!;hgG{YaG0aHv>A8UAgd90N+?|I%<#<9Qa3kf3lG-yeUjD4f zD;7n+xneF?fklaG3)DUiB+#^$&>;7FE{S>owow#AjC7Wz@sauDJ)rkvUvU*O;t%n~X{H)r?Gu!X=w?#~3pxGq>II@>rk>Gy5>i$lw4CkQdg6BI-aST0_Qe$=ACqn} zMv{5IEfE#Ep1OXc*Vhx}`%o}&4@#@X<)(uBbxnC9=dlm{cfk$@TA68DO z^x`jb7&BR-9|`sHlf)9!KTcqw9oxFE*y?>DKV*`3ut+d{GGYRTJwP&nGABE(rwucj z_m`%H;Fy0{cD$7EU@R$@<^T9rhmRh6kH$TGhabG4YKzM-%>3lt2CNLlK+g4N0+%#*w*qB8IAaMBMoPsGT%d!8lbWxM}|`pH^}<4PWtA5lkpZ#Q&a8o_J$=r!qF;;6IMt=dr>2*g#YN6(U6ec}Yz=%L+!$EBab#If45;4)<3L6B4fKU4 zDWx7=cRES#`X#lA%*=7|K3~JvCsrO@5^-;tZAKOo53&1XfWlxRs8?zBCO+fe!^=qW z(t);h74~)pz2g`8URO6I1yYwDDQ>9XLO?Yk<_HSU=qPGbPncmNn=X+}tL*#)jipZ*j zJQbSb&_w?^fRGNUyec3!{1T-P-uj+NrFZZ&cW5TwbF}^g0?zOG_Uy!by+}LL^PkjZ z32y-!A{vesrsR0!YuzB}`AEfiN#+%g={O0uylA=BJ&!d^_FM>5b)@pb5J#VR>#xr& zK^`aFQ0HAv{8ZgfzwsqI?$lNNGWi?hB^Ol}7nCm4u~BE zn6p86uu3`vQS6TmK{{F&IGPRA_IpY$YH=6I#(#J@27h*N?{Ci1_GSczEq2D_b;9j4 zw!Iq@k6m3Z@fu8Vh6=a_0hk^@3BLg$jl$yl*ZvVBHV*DeLZ=5_n>`&X`|XDms@Upg zZY@l5_I(v6po|07iwcC1BpTheP{F6%-^C-Q3D%3Ayr_ zADNZ2X(j1(DCP)#mb8{C($`f++^T7_P#+^y^VO?*DL%g(U6I(1?8ZcY4*or6g9!V}APzeN^vK*RB8z=;5CvG=JK zL(&`5!Sgk#1H4X8%C6^D4n)5P#E7SnkPY|IwDjC*)G)lsw-(bPAZ-wJx8jiPYpjk3 z?X~$2BC^VOUyk-GI;znUvg&0}JoLkT}bW-QgWhjCJV zK25NWOS+j=?2fq~!8*sGdx7zTb40vBbt6r8z)hhjc!2t;-d&2he!t{q2}OH_x!UXG z>(;c`Dn8JJcaTk%9FZv8ift$K}Um{j>3o%~8yYvJ#?hI*Rl|v)^C1*4HyI{d#hIahkT9)IY4^uo7&E17W~N`J8jv#thkUdFOa2m#bGox1YDX>!-?BXf^q}$ z5vMH5%FFvN_+^DH^Azc>J*>a1zHvtfCs6!w;xr|0GhbEH+C6`jSY)at6mz3g`{Xqx z$Y~z_*+fEGgrLZvJSjY%g4L^c&RXWa`bPm6D8PfCVIE`@fQq0;}`xSpe@YWC|w$`tx?#v`Jlb=#at zJ?yX0cK_>Eu&3QX1)><)8!z;`NSAWe^%wMCM}9EA8qVP>=w(|QR%b{F+g-o6YJ}TK zN*ey&H~Y%@9xvNe(diSWr`p=ksWs*xGc+SeWoAF~jqBWVztcQcDZmE#CyfD(b-Aw? z6?G<3tZ6UxGRb0SMM36${`HA&59vyqP(35d`n;5*7E|7yiz{a{lo**4gWgu-fp7}2SN6N#|_K9#spS5FPWB-q!4pO-9 zDMWh7UB-oSvUn5#sd|!T;AHM2Oy>`{DrQ?i!FPca3v8Xi+Y{su?gOcPN7Szh0*1m= zCvs$00{GwtP+(x<0xC!sfJd8vQt}ic^XmV9KN9DF-=1O*uQBvvUh#c-AQ%rl*@X;M zdU3o>H>wk^dJ?jSuC-4GoDpwF17Lsne8=)M^g{RWi}d7pv6!Ykk-5PEz`8{61@xmp zC*7Z*K5GUh9@pV;OejopO~|L6XZFF<7wK_Iyu4Gv4Q)dHv0zc1M#!>+fv(OgI#Zh* zjB&p%#lnwxK&+kZ+&VcTG!u1+Bm#c3Nnw(fESl*MpK%w{bFN(flat%pp%7e5I|`0XZTR0I6un$aaRZv(vbXIEQ8P9BUO5nQzApMUA$K0|{XHQk7$4@rB zpmtWZhHg0Bn=~6x?fB&M-9PV38Mg^_mE%otpRl4*YX=rT*|M~j)i5F4rZ{{1Qxw)I z;3FUpiv_e%j}flDo0ZhJ7~FYZEBpI->Ytly1}rfu$_nk_G(vPrYDzp<#ch}O2xhVa zBT%RV25>(RqoAYX1stBfC2$t$318WdyK}r7*gLCTflq>UT1zpF{?*a!bewz6NUIZ3Nx1mk_c*Bg6;ZBJ zCBgl!(ZS*26yO5*f9|Q9{-b#5M&GE{bzDnlrAp7<%+`phDa>#lvWc5J0lytM#qavz z1>9`&?{Qi7?zEHSi-f^AG{O`UQu@A_ppQcqy~pJ~w5NYy^HvjYK1Xt#{rprH*D!IOWsU1}pRHl+%B?AOcf_}T z;O+oDT_@ljwnucsF9cgZEB7r6>w&vE(Vnqm;(Gf_UEP+xp%wTpO+gb-AY>>1J7oX* z=95vjP>A;%9X#WESXmyzAmJhW!XR_0{XE~jn-WXf4^EQpVrUdwnD)yJNtUy@%!lvw zR7DHg@s(PlwXcg>hSZD(bRTbs?*xH?&kUjy$z^8MC9KLSH^*K%6m-;G;vHPpC&sQV z4IleHrNCeA9OynOP5G|Q?_IPyQVSp{D%I8ojlhnt7S?ogs1BZN3e#(OIILl~afjYt@=(v4xe0(~aHhtL;cH)9 zziJnbH9TX9xn_N1ER4bQSZ(MF_sEKaYhdaVT+ksNv#5hpM~(8J$|Lc@!!tx<{P)6X z>CoOr^oV>)*Y4_s$VnSLep|(UM#Qwshhz^(_|zQx)35)<0aACOpvjywP)DUBqa1_W8ry@?lTyI^(*mS+Jc zNe=;mXzu*PeyV=x8zq@HN;I{60y@ujsz9h=)YZ?CB?z@q5gs4mEGxLe-|ixRuumOC z2+Gx`>11;R*oCu!Z6F|q9Rh{H&9md);8xg1%KsQ@q#rZ+Uo$#io2eqGq}*n2tRZ;- zc$!IF!$sizflv2uncw;~UhI$Vo{bk4{Pulfta^SD|5>GFMIz;S6|(=fz5T;Z_P~#9bM5BJ{%o^Q-09Yr{B-)AqZ};!bCPWfsuQkX1TG1I zt%L9SclDm&wVKEe|1M;R^;g@r-q<Wryk(ZLw4tF>BPLx4Q8!cw;gZY zl;wXIpAYO(&anzVbY3M)=FseR|#m$?-bv0oGc#@69KW*?8_ov4w0WEAq??d3G( z4~hlZcm6Y^tY@g0@BVS`x1DP+%Sd-OmpVV>H2jEm|>M4 z^B?AyJPbVWGdQ}UIOK|Me*Ut(O^*&;^L(7~Wa12_Q_FdboA?#_oj8wu$Uih+CM!Ll&4vAC!e{gv@`IX)0}Ex{fD(NbEbY1dt;6J!?(^cSHHg1d+xh!p6IqN>m9c= zW1HF3lNrh$?|fC2KI_ALu|Jw0k4=9R>vrKse8e8XGe`4upat=+l`IMUg%=teMSrOR z2l_>UJLT$o)ql)>c=DZ%<;s`ewwYhJ?_XG-t-2>$CP-U=TjR7$g2r?84J`e8ffr$L kX$&81|Nmy)-C$1Oz#W5()IxP7xmh_~sWlN=)81_Z0BWea7ytkO literal 126134 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 Date: Wed, 25 Feb 2026 15:43:08 +0800 Subject: [PATCH 10/10] [Doc] Refine Diffusion Tutorial Documents (#1305) Signed-off-by: Didan Deng <33117903+wtomin@users.noreply.github.com> --- docs/.nav.yml | 11 +++++------ docs/contributing/model/adding_diffusion_model.md | 12 ++++++------ .../features => design/feature}/cache_dit.md | 2 +- .../features => design/feature}/cfg_parallel.md | 2 +- .../features => design/feature}/sequence_parallel.md | 2 +- .../features => design/feature}/teacache.md | 2 +- .../features => design/feature}/tensor_parallel.md | 2 +- 7 files changed, 16 insertions(+), 17 deletions(-) rename docs/{contributing/features => design/feature}/cache_dit.md (99%) rename docs/{contributing/features => design/feature}/cfg_parallel.md (99%) rename docs/{contributing/features => design/feature}/sequence_parallel.md (99%) rename docs/{contributing/features => design/feature}/teacache.md (99%) rename docs/{contributing/features => design/feature}/tensor_parallel.md (99%) diff --git a/docs/.nav.yml b/docs/.nav.yml index 07db1b465..fa06b53ec 100644 --- a/docs/.nav.yml +++ b/docs/.nav.yml @@ -58,12 +58,6 @@ nav: - contributing/model/README.md - contributing/model/adding_omni_model.md - contributing/model/adding_diffusion_model.md - - Advanced Features: - - contributing/features/cfg_parallel.md - - contributing/features/sequence_parallel.md - - contributing/features/tensor_parallel.md - - contributing/features/cache_dit.md - - contributing/features/teacache.md - CI: contributing/ci - Design Documents: - design/index.md @@ -72,6 +66,11 @@ nav: - design/feature/disaggregated_inference.md - design/feature/ray_based_execution.md - design/feature/omni_connectors/ + - design/feature/cfg_parallel.md + - design/feature/sequence_parallel.md + - design/feature/tensor_parallel.md + - design/feature/cache_dit.md + - design/feature/teacache.md - Module Design: - design/module/ar_module.md - design/module/dit_module.md diff --git a/docs/contributing/model/adding_diffusion_model.md b/docs/contributing/model/adding_diffusion_model.md index 10755c6c6..c1094802c 100644 --- a/docs/contributing/model/adding_diffusion_model.md +++ b/docs/contributing/model/adding_diffusion_model.md @@ -1,4 +1,4 @@ -# Adding a Diffusion Model to vLLM-Omni +# Adding a Diffusion Model This guide walks you through adding a new diffusion model to vLLM-Omni. We use **Qwen-Image** as the primary example, with references to other models (LongCat, Flux, Wan2.2) to illustrate different patterns. @@ -680,7 +680,7 @@ vLLM-Omni automatically compiles blocks in `_repeated_blocks` when `torch.compil ### Tensor Parallelism -See detailed guide: [How to add Tensor Parallel support](../features/tensor_parallel.md) +See detailed guide: [How to add Tensor Parallel support](../../design/feature/tensor_parallel.md) **Quick setup:** @@ -694,7 +694,7 @@ omni = Omni(model="your-model", tensor_parallel_size=2) ### CFG Parallelism -See detailed guide: [How to add CFG-Parallel support](../features/cfg_parallel.md) +See detailed guide: [How to add CFG-Parallel support](../../design/feature/cfg_parallel.md) **Quick setup:** @@ -708,7 +708,7 @@ omni = Omni(model="your-model", cfg_parallel_size=2) ### Sequence Parallelism -See detailed guide: [How to add Sequence Parallel support](../features/sequence_parallel.md) +See detailed guide: [How to add Sequence Parallel support](../../design/feature/sequence_parallel.md) **Quick setup:** @@ -724,7 +724,7 @@ omni = Omni(model="your-model", ulysses_degree=2, ring_degree=2) #### TeaCache -See detailed guide: [How to add TeaCache support](../features/teacache.md) +See detailed guide: [How to add TeaCache support](../../design/feature/teacache.md) **Quick setup:** @@ -744,7 +744,7 @@ omni = Omni(model="your-model", #### Cache-DiT -See detailed guide: [How to add Cache-DiT support](../features/cache_dit.md) +See detailed guide: [How to add Cache-DiT support](../../design/feature/cache_dit.md) **Quick setup:** diff --git a/docs/contributing/features/cache_dit.md b/docs/design/feature/cache_dit.md similarity index 99% rename from docs/contributing/features/cache_dit.md rename to docs/design/feature/cache_dit.md index f39b93578..3112bb591 100644 --- a/docs/contributing/features/cache_dit.md +++ b/docs/design/feature/cache_dit.md @@ -1,4 +1,4 @@ -# Support Cache-DiT +# Cache-DiT This section describes how to add cache-dit acceleration to a new diffusion pipeline. We use the Qwen-Image pipeline and LongCat-Image pipeline as reference implementations. diff --git a/docs/contributing/features/cfg_parallel.md b/docs/design/feature/cfg_parallel.md similarity index 99% rename from docs/contributing/features/cfg_parallel.md rename to docs/design/feature/cfg_parallel.md index b169f13d0..e31e64edd 100644 --- a/docs/contributing/features/cfg_parallel.md +++ b/docs/design/feature/cfg_parallel.md @@ -1,4 +1,4 @@ -# Support CFG-Parallel +# CFG-Parallel This section describes how to add CFG-Parallel (Classifier-Free Guidance Parallel) to a diffusion pipeline. We use the Qwen-Image pipeline as the reference implementation. diff --git a/docs/contributing/features/sequence_parallel.md b/docs/design/feature/sequence_parallel.md similarity index 99% rename from docs/contributing/features/sequence_parallel.md rename to docs/design/feature/sequence_parallel.md index f90ac817f..c5c667885 100644 --- a/docs/contributing/features/sequence_parallel.md +++ b/docs/design/feature/sequence_parallel.md @@ -1,4 +1,4 @@ -# Support Sequence Parallel +# Sequence Parallel This section describes how to add Sequence Parallel (SP) to a diffusion transformer model. We use the Qwen-Image transformer and Wan2.2 transformer as reference implementations. diff --git a/docs/contributing/features/teacache.md b/docs/design/feature/teacache.md similarity index 99% rename from docs/contributing/features/teacache.md rename to docs/design/feature/teacache.md index 6c29c0f99..775fb635b 100644 --- a/docs/contributing/features/teacache.md +++ b/docs/design/feature/teacache.md @@ -1,4 +1,4 @@ -# Support TeaCache +# TeaCache This section describes how to add TeaCache to a diffusion transformer model. We use the Qwen-Image transformer as the reference implementation. diff --git a/docs/contributing/features/tensor_parallel.md b/docs/design/feature/tensor_parallel.md similarity index 99% rename from docs/contributing/features/tensor_parallel.md rename to docs/design/feature/tensor_parallel.md index 9d25a6e2d..bcafde7e7 100644 --- a/docs/contributing/features/tensor_parallel.md +++ b/docs/design/feature/tensor_parallel.md @@ -1,4 +1,4 @@ -# Support Tensor Parallel +# Tensor Parallel This section describes how to add Tensor Parallel (TP) to a diffusion transformer model. We use the Z-Image transformer as the reference implementation.