From 672eec2a2616cc20adb49e36116dad378cc98b53 Mon Sep 17 00:00:00 2001 From: ali-88123 <1940747290@qq.com> Date: Mon, 12 Jan 2026 20:05:20 +0800 Subject: [PATCH 1/2] add Fun-CosyVoice3 eagle3 speculative decoding --- .../speculative/benchmark/pytorch/__init__.py | 9 +- .../benchmark/pytorch/benchmark_engine.py | 126 ++ .../pytorch/generate_baseline_answer.py | 230 ++- .../pytorch/generate_eagle_answer.py | 232 ++- .../speculative/inference/models/__init__.py | 4 +- .../inference/models/eagle3/__init__.py | 4 +- .../inference/models/eagle3/draft/__init__.py | 4 +- .../models/eagle3/draft/llama3_eagle3.py | 301 +++- .../inference/models/eagle3/eagle3_model.py | 440 +++++- .../models/eagle3/target/__init__.py | 3 +- .../eagle3/target/modeling_cosyvoice3_kv.py | 984 +++++++++++++ .../models/eagle3/target/modeling_qwen2_kv.py | 1255 +++++++++++++++++ .../train/configs/cosyvoice3-llm-eagle3.json | 28 + .../speculative/train/data/data_utils.py | 71 + .../speculative/train/data/dataset.py | 2 + .../train/data/dataset_builder/__init__.py | 2 + .../dataset_builder/online_dataset_builder.py | 381 +++++ .../train/models/draft/__init__.py | 9 +- .../train/models/draft/llama_eagle3.py | 77 + .../train/models/target/cosyvoice3_llm.py | 283 ++++ .../models/target/target_model_wrapper.py | 75 + .../speculative/train/trainer/__init__.py | 7 +- .../train/trainer/online_eagle3_trainer.py | 227 ++- .../compressor/speculative/utils/__init__.py | 6 + .../compressor/speculative/utils/util.py | 150 ++ angelslim/engine.py | 4 + angelslim/utils/lazy_imports.py | 6 + dataset/tts_fake_data/question.jsonl | 100 ++ dataset/tts_fake_data/train.jsonl | 2 + dataset/tts_fake_data/train_regenerate.jsonl | 2 + dataset/tts_fake_data/zero_shot_prompt.wav | Bin 0 -> 334138 bytes .../speculative_decoding/audio_eagle.md | 140 ++ requirements/requirements_multimodal.txt | 8 +- .../speculative/train_eagle3_tts_online.sh | 32 + tools/spec_benchmark.py | 8 +- tools/train_eagle3_online.py | 2 +- 36 files changed, 5184 insertions(+), 30 deletions(-) create mode 100644 angelslim/compressor/speculative/inference/models/eagle3/target/modeling_cosyvoice3_kv.py create mode 100644 angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py create mode 100644 angelslim/compressor/speculative/train/configs/cosyvoice3-llm-eagle3.json create mode 100644 angelslim/compressor/speculative/train/models/target/cosyvoice3_llm.py create mode 100644 dataset/tts_fake_data/question.jsonl create mode 100644 dataset/tts_fake_data/train.jsonl create mode 100644 dataset/tts_fake_data/train_regenerate.jsonl create mode 100644 dataset/tts_fake_data/zero_shot_prompt.wav create mode 100644 docs/source/features/speculative_decoding/audio_eagle.md create mode 100644 scripts/speculative/train_eagle3_tts_online.sh diff --git a/angelslim/compressor/speculative/benchmark/pytorch/__init__.py b/angelslim/compressor/speculative/benchmark/pytorch/__init__.py index ac8a4b0f..0db6e4f5 100644 --- a/angelslim/compressor/speculative/benchmark/pytorch/__init__.py +++ b/angelslim/compressor/speculative/benchmark/pytorch/__init__.py @@ -12,6 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .benchmark_engine import BenchmarkConfig, BenchmarkEngine, BenchmarkMode +from .benchmark_engine import ( + BenchmarkConfig, + BenchmarkEngine, + BenchmarkMode, + TTSBenchmarkEngine, +) -__all__ = ["BenchmarkEngine", "BenchmarkConfig", "BenchmarkMode"] +__all__ = ["BenchmarkEngine", "TTSBenchmarkEngine", "BenchmarkConfig", "BenchmarkMode"] diff --git a/angelslim/compressor/speculative/benchmark/pytorch/benchmark_engine.py b/angelslim/compressor/speculative/benchmark/pytorch/benchmark_engine.py index 4444996a..a54dfaab 100644 --- a/angelslim/compressor/speculative/benchmark/pytorch/benchmark_engine.py +++ b/angelslim/compressor/speculative/benchmark/pytorch/benchmark_engine.py @@ -25,7 +25,11 @@ from angelslim.utils.lazy_imports import fastchat, ray from .generate_baseline_answer import get_model_answers as get_baseline_answers +from .generate_baseline_answer import get_tts_answers as get_tts_baseline_answers +from .generate_baseline_answer import get_tts_audios as get_tts_baseline_audios from .generate_eagle_answer import get_model_answers as get_eagle_answers +from .generate_eagle_answer import get_tts_answers as get_tts_eagle_answers +from .generate_eagle_answer import get_tts_audios as get_tts_eagle_audios class BenchmarkMode(Enum): @@ -77,6 +81,9 @@ class BenchmarkConfig: # Batch settings batch_size: int = 1 + # TTS settings + generate_audio: bool = False + class BenchmarkEngine: """Core benchmark engine for speculative decoding evaluation""" @@ -343,6 +350,9 @@ def _create_args_namespace(self, mode: str) -> argparse.Namespace: args.early_stop_method = self.config.early_stop_method + # TTS settings + args.generate_audio = self.config.generate_audio + return args def _get_question_file_path(self) -> str: @@ -397,3 +407,119 @@ def get_performance_summary(self) -> str: summary.append(f"Analysis Report: {self.analysis_file}") return "\n".join(summary) + + +class TTSBenchmarkEngine(BenchmarkEngine): + """Core benchmark engine for speculative decoding evaluation""" + + def _run_eagle_benchmark(self): + """Run Eagle speculative decoding benchmark""" + args = self._create_args_namespace("eagle") + + questions = fastchat.llm_judge.common.load_questions( + self._get_question_file_path(), + self.config.question_begin, + self.config.question_end, + ) + + use_ray = self.config.num_gpus_total // self.config.num_gpus_per_model > 1 + get_answers_func = ( + ray.remote(num_gpus=self.config.num_gpus_per_model)( + get_tts_eagle_answers + ).remote + if use_ray + else get_tts_eagle_answers + ) + + chunk_size = len(questions) // ( + self.config.num_gpus_total // self.config.num_gpus_per_model + ) + ans_handles = [ + get_answers_func( + f"{self.config.model_id}-temperature-{self.config.temperature}", + questions[i : i + chunk_size], + self.eagle_file, + self.config.num_choices, + self.config.temperature, + args, + ) + for i in range(0, len(questions), chunk_size) + ] + + if use_ray: + ray.get(ans_handles) + + self._reorg_answer_file(self.eagle_file) + self.results["eagle_file"] = self.eagle_file + + if self.config.generate_audio: + self._generate_audio("eagle") + + def _run_baseline_benchmark(self): + """Run baseline benchmark""" + args = self._create_args_namespace("baseline") + + questions = fastchat.llm_judge.common.load_questions( + self._get_question_file_path(), + self.config.question_begin, + self.config.question_end, + ) + + use_ray = self.config.num_gpus_total // self.config.num_gpus_per_model > 1 + get_answers_func = ( + ray.remote(num_gpus=self.config.num_gpus_per_model)( + get_tts_baseline_answers + ).remote + if use_ray + else get_tts_baseline_answers + ) + + chunk_size = len(questions) // ( + self.config.num_gpus_total // self.config.num_gpus_per_model + ) + ans_handles = [ + get_answers_func( + f"{self.config.model_id}-temperature-{self.config.temperature}", + questions[i : i + chunk_size], + self.baseline_file, + self.config.num_choices, + self.config.temperature, + args, + ) + for i in range(0, len(questions), chunk_size) + ] + + if use_ray: + ray.get(ans_handles) + + self._reorg_answer_file(self.baseline_file) + self.results["baseline_file"] = self.baseline_file + + if self.config.generate_audio: + self._generate_audio("baseline") + + def _calculate_metrics(self) -> Dict[str, Any]: + """Calculate acceptance length and speedup ratio""" + metrics = {} + + # Calculate acceptance length from Eagle results + if os.path.exists(self.eagle_file): + metrics["acceptance_length"] = self._calculate_acceptance_length( + self.eagle_file + ) + + return metrics + + def _generate_audio(self, mode): + args = self._create_args_namespace(mode) + + answers = fastchat.llm_judge.common.load_questions( + args.answer_file, + self.config.question_begin, + self.config.question_end, + ) + + if mode == "baseline": + get_tts_baseline_audios(answers, args.answer_file, args) + else: + get_tts_eagle_audios(answers, args.answer_file, args) diff --git a/angelslim/compressor/speculative/benchmark/pytorch/generate_baseline_answer.py b/angelslim/compressor/speculative/benchmark/pytorch/generate_baseline_answer.py index a0c72a50..bc28c28d 100644 --- a/angelslim/compressor/speculative/benchmark/pytorch/generate_baseline_answer.py +++ b/angelslim/compressor/speculative/benchmark/pytorch/generate_baseline_answer.py @@ -17,15 +17,18 @@ import os import random import time -from typing import Any, Dict, List +from typing import Any, Dict, Generator, List import numpy as np import shortuuid import torch from tqdm import tqdm -from angelslim.compressor.speculative.inference.models import Eagle3Model -from angelslim.utils.lazy_imports import fastchat, ray +from angelslim.compressor.speculative.inference.models import ( + CosyVoice3Eagle3Model, + Eagle3Model, +) +from angelslim.utils.lazy_imports import fastchat, ray, torchaudio SYSTEM_PROMPT = { "role": "system", @@ -56,6 +59,7 @@ def __init__(self, args: argparse.Namespace): self.total_token = args.total_token self.depth = args.depth self.top_k = args.top_k + self.generate_audio = args.generate_audio def _get_question_file_path(self, args: argparse.Namespace) -> str: script_dir = os.path.dirname(__file__) @@ -99,6 +103,24 @@ def initialize_model(config: EvaluationConfig) -> Eagle3Model: return model +def initialize_cosycoice3_model(config: EvaluationConfig) -> CosyVoice3Eagle3Model: + """Initialize and return the Eagle3 model""" + model = CosyVoice3Eagle3Model.from_pretrained( + base_model_path=config.base_model_path, + eagle_model_path=config.eagle_model_path, + total_token=config.total_token, + depth=config.depth, + top_k=config.top_k, + device_map="auto", + torch_dtype="auto", + generate_audio=config.generate_audio, + ) + model.eval() + print(f"Model training state: {model.training}") + print(f'CUDA VISIBLE DEVICES: {os.environ.get("CUDA_VISIBLE_DEVICES")}') + return model + + def process_conversation_turn( model: Eagle3Model, tokenizer: Any, @@ -146,6 +168,60 @@ def process_conversation_turn( } +def process_tts_conversation_turn( + model: Eagle3Model, + model_id: str, + qs: str, + temperature: float, + path: str, +) -> Dict[str, Any]: + """Process a single question""" + if "cosyvoice3" in model_id: + prompt_text = model.base_model.frontend.text_normalize( + qs["prompt_text"], split=False, text_frontend=True + ) + prompt_wav = os.path.normpath(os.path.join(path, qs["prompt_wav"])) + for i in tqdm( + model.base_model.frontend.text_normalize( + qs["tts_text"], split=True, text_frontend=True + ) + ): + if (not isinstance(i, Generator)) and len(i) < 0.5 * len(prompt_text): + print( + "synthesis text {} too short than prompt text {}, this may lead to bad performance".format( # noqa: E501 + i, prompt_text + ) + ) + model_input = model.base_model.frontend.frontend_zero_shot( + i, prompt_text, prompt_wav, model.base_model.sample_rate, "" + ) + + torch.cuda.synchronize() + start_time = time.time() + + output_ids, new_token, idx = model.naive_generate( + text=model_input["text"], + prompt_text=model_input["prompt_text"], + llm_prompt_speech_token=model_input["llm_prompt_speech_token"], + temperature=temperature, + log=True, + ) + + torch.cuda.synchronize() + total_time = time.time() - start_time + output_ids = output_ids[0][-new_token:] + + return { + "tts_text": qs["tts_text"], + "prompt_text": qs["prompt_text"], + "prompt_wav": prompt_wav, + "output_audio_tokens": output_ids, + "idx": int(idx), + "new_token": int(new_token), + "wall_time": total_time, + } + + def generate_answer_for_question( model: Eagle3Model, tokenizer: Any, @@ -183,6 +259,39 @@ def generate_answer_for_question( return choices +def generate_answer_for_question_tts( + model: Eagle3Model, + model_id: str, + question: Dict[str, Any], + num_choices: int, + temperature: float, + path: str, +) -> List[Dict[str, Any]]: + """Generate answers for a single question with multiple choices""" + choices = [] + for i in range(num_choices): + torch.manual_seed(i) + + result = process_tts_conversation_turn( + model, model_id, question, temperature, path + ) + + choices.append( + { + "index": i, + "tts_text": result["tts_text"], + "prompt_text": result["prompt_text"], + "prompt_wav": result["prompt_wav"], + "output_audio_tokens": result["output_audio_tokens"].tolist(), + "idxs": result["idx"], + "new_tokens": result["new_token"], + "wall_time": result["wall_time"], + } + ) + + return choices + + def warmup_model( model: Eagle3Model, tokenizer: Any, question: Dict[str, Any], temperature: float ) -> None: @@ -195,6 +304,20 @@ def warmup_model( print("Warmup done") +def warmup_tts_lm( + model: Eagle3Model, + model_id: str, + question: Dict[str, Any], + temperature: float, + path: str, +) -> None: + """Warm up the model before actual evaluation""" + for _ in range(3): + torch.manual_seed(0) + process_tts_conversation_turn(model, model_id, question, temperature, path) + print("Warmup done") + + @torch.inference_mode() def get_model_answers( model_id: str, @@ -230,6 +353,107 @@ def get_model_answers( fout.write(json.dumps(ans_json) + "\n") +@torch.inference_mode() +def get_tts_answers( + model_id: str, + questions: List[Dict[str, Any]], + answer_file: str, + num_choices: int, + temperature: float, + args: argparse.Namespace, +) -> None: + """Generate answers for a batch of questions""" + config = EvaluationConfig(args) + if os.path.exists(os.path.join(args.base_model_path, "cosyvoice3.yaml")): + model = initialize_cosycoice3_model(config) + + if questions: + current_file = os.path.abspath(__file__) + project_root = current_file.split("/AngelSlim/")[0] + "/AngelSlim" + warmup_tts_lm( + model, + model_id, + questions[0], + temperature, + os.path.join(project_root, "dataset", args.bench_name), + ) + + os.makedirs(os.path.dirname(answer_file), exist_ok=True) + + i = 0 + for question in tqdm(questions): + choices = generate_answer_for_question_tts( + model, + model_id, + question, + num_choices, + temperature, + os.path.join(project_root, "dataset", args.bench_name), + ) + + with open(os.path.expanduser(answer_file), "a") as fout: + ans_json = { + "question_id": i, + "answer_id": shortuuid.uuid(), + "model_id": model_id, + "choices": choices, + "tstamp": time.time(), + } + fout.write(json.dumps(ans_json) + "\n") + + i += 1 + + +def get_tts_audios( + answers: List[Dict[str, Any]], + answer_file: str, + args: argparse.Namespace, +) -> None: + """Generate audios for a batch of audio tokens""" + config = EvaluationConfig(args) + if os.path.exists(os.path.join(args.base_model_path, "cosyvoice3.yaml")): + model = initialize_cosycoice3_model(config) + + for answer in tqdm(answers): + prompt_text = model.base_model.frontend.text_normalize( + answer["choices"][0]["prompt_text"], split=False, text_frontend=True + ) + prompt_wav = answer["choices"][0]["prompt_wav"] + for i in tqdm( + model.base_model.frontend.text_normalize( + answer["choices"][0]["tts_text"], split=True, text_frontend=True + ) + ): + model_input = model.base_model.frontend.frontend_zero_shot( + i, prompt_text, prompt_wav, model.base_model.sample_rate, "" + ) + + tts_speech_token = answer["choices"][0]["output_audio_tokens"] + while tts_speech_token[-1] == model.base_model.model.llm.eos_token: + del tts_speech_token[-1] + this_tts_speech_token = torch.tensor(tts_speech_token).unsqueeze(dim=0) + this_tts_speech = model.base_model.model.token2wav( + token=this_tts_speech_token, + prompt_token=model_input["flow_prompt_speech_token"], + prompt_feat=model_input["prompt_speech_feat"], + embedding=model_input["flow_embedding"], + token_offset=0, + uuid="", + finalize=True, + speed=1.0, + ) + this_tts_speech = this_tts_speech.cpu() + directory = os.path.dirname(answer_file) + os.makedirs(f"{directory}/baseline", exist_ok=True) + torchaudio.save( + f"{directory}/baseline/eval_{answer['question_id']}.wav", + this_tts_speech, + model.base_model.sample_rate, + ) + else: + raise NotImplementedError("Model not supported") + + def run_evaluation(config: EvaluationConfig, args: argparse.Namespace) -> None: """Run the evaluation with optional distributed processing""" questions = fastchat.llm_judge.common.load_questions( diff --git a/angelslim/compressor/speculative/benchmark/pytorch/generate_eagle_answer.py b/angelslim/compressor/speculative/benchmark/pytorch/generate_eagle_answer.py index 9451b742..f688ce90 100644 --- a/angelslim/compressor/speculative/benchmark/pytorch/generate_eagle_answer.py +++ b/angelslim/compressor/speculative/benchmark/pytorch/generate_eagle_answer.py @@ -17,15 +17,18 @@ import os import random import time -from typing import Any, Dict, List +from typing import Any, Dict, Generator, List import numpy as np import shortuuid import torch from tqdm import tqdm -from angelslim.compressor.speculative.inference.models import Eagle3Model -from angelslim.utils.lazy_imports import fastchat, ray +from angelslim.compressor.speculative.inference.models import ( + CosyVoice3Eagle3Model, + Eagle3Model, +) +from angelslim.utils.lazy_imports import fastchat, ray, torchaudio SYSTEM_PROMPT = { "role": "system", @@ -57,6 +60,7 @@ def __init__(self, args: argparse.Namespace): self.depth = args.depth self.top_k = args.top_k self.early_stop_method = args.early_stop_method + self.generate_audio = args.generate_audio def _get_question_file_path(self, args: argparse.Namespace) -> str: script_dir = os.path.dirname(__file__) @@ -99,6 +103,24 @@ def initialize_model(config: EvaluationConfig) -> Eagle3Model: return model +def initialize_cosycoice3_model(config: EvaluationConfig) -> CosyVoice3Eagle3Model: + """Initialize and return the Eagle3 model""" + model = CosyVoice3Eagle3Model.from_pretrained( + base_model_path=config.base_model_path, + eagle_model_path=config.eagle_model_path, + total_token=config.total_token, + depth=config.depth, + top_k=config.top_k, + device_map="auto", + torch_dtype="auto", + generate_audio=config.generate_audio, + ) + model.eval() + print(f"Model training state: {model.training}") + print(f'CUDA VISIBLE DEVICES: {os.environ.get("CUDA_VISIBLE_DEVICES")}') + return model + + def process_conversation_turn( model: Eagle3Model, tokenizer: Any, @@ -147,6 +169,61 @@ def process_conversation_turn( } +def process_tts_conversation_turn( + model: Eagle3Model, + model_id: str, + qs: str, + temperature: float, + path: str, +) -> Dict[str, Any]: + """Process a single question""" + if "cosyvoice3" in model_id: + prompt_text = model.base_model.frontend.text_normalize( + qs["prompt_text"], split=False, text_frontend=True + ) + prompt_wav = os.path.normpath(os.path.join(path, qs["prompt_wav"])) + for i in tqdm( + model.base_model.frontend.text_normalize( + qs["tts_text"], split=True, text_frontend=True + ) + ): + if (not isinstance(i, Generator)) and len(i) < 0.5 * len(prompt_text): + print( + "synthesis text {} too short than prompt text {}, this may lead to bad performance".format( # noqa: E501 + i, prompt_text + ) + ) + model_input = model.base_model.frontend.frontend_zero_shot( + i, prompt_text, prompt_wav, model.base_model.sample_rate, "" + ) + + torch.cuda.synchronize() + start_time = time.time() + + output_ids, new_token, idx, accept_length_list = model.eagle_generate( + text=model_input["text"], + prompt_text=model_input["prompt_text"], + llm_prompt_speech_token=model_input["llm_prompt_speech_token"], + temperature=temperature, + log=True, + ) + + torch.cuda.synchronize() + total_time = time.time() - start_time + output_ids = output_ids[0][-new_token:] + + return { + "tts_text": qs["tts_text"], + "prompt_text": qs["prompt_text"], + "prompt_wav": prompt_wav, + "output_audio_tokens": output_ids, + "idx": int(idx), + "new_token": int(new_token), + "wall_time": total_time, + "accept_length_list": accept_length_list, + } + + def generate_answer_for_question( model: Eagle3Model, tokenizer: Any, @@ -187,6 +264,40 @@ def generate_answer_for_question( return choices +def generate_answer_for_question_tts( + model: Eagle3Model, + model_id: str, + question: Dict[str, Any], + num_choices: int, + temperature: float, + path: str, +) -> List[Dict[str, Any]]: + """Generate answers for a single question with multiple choices""" + choices = [] + for i in range(num_choices): + torch.manual_seed(i) + + result = process_tts_conversation_turn( + model, model_id, question, temperature, path + ) + + choices.append( + { + "index": i, + "tts_text": result["tts_text"], + "prompt_text": result["prompt_text"], + "prompt_wav": result["prompt_wav"], + "output_audio_tokens": result["output_audio_tokens"].tolist(), + "idxs": result["idx"], + "new_tokens": result["new_token"], + "wall_time": result["wall_time"], + "accept_length": result["accept_length_list"], + } + ) + + return choices + + def warmup_model( model: Eagle3Model, tokenizer: Any, question: Dict[str, Any], temperature: float ) -> None: @@ -199,6 +310,20 @@ def warmup_model( print("Warmup done") +def warmup_tts_lm( + model: Eagle3Model, + model_id: str, + question: Dict[str, Any], + temperature: float, + path: str, +) -> None: + """Warm up the model before actual evaluation""" + for _ in range(3): + torch.manual_seed(0) + process_tts_conversation_turn(model, model_id, question, temperature, path) + print("Warmup done") + + @torch.inference_mode() def get_model_answers( model_id: str, @@ -234,6 +359,107 @@ def get_model_answers( fout.write(json.dumps(ans_json) + "\n") +@torch.inference_mode() +def get_tts_answers( + model_id: str, + questions: List[Dict[str, Any]], + answer_file: str, + num_choices: int, + temperature: float, + args: argparse.Namespace, +) -> None: + """Generate answers for a batch of questions""" + config = EvaluationConfig(args) + if os.path.exists(os.path.join(args.base_model_path, "cosyvoice3.yaml")): + model = initialize_cosycoice3_model(config) + + if questions: + current_file = os.path.abspath(__file__) + project_root = current_file.split("/AngelSlim/")[0] + "/AngelSlim" + warmup_tts_lm( + model, + model_id, + questions[0], + temperature, + os.path.join(project_root, "dataset", args.bench_name), + ) + + os.makedirs(os.path.dirname(answer_file), exist_ok=True) + + i = 0 + for question in tqdm(questions): + choices = generate_answer_for_question_tts( + model, + model_id, + question, + num_choices, + temperature, + os.path.join(project_root, "dataset", args.bench_name), + ) + + with open(os.path.expanduser(answer_file), "a") as fout: + ans_json = { + "question_id": i, + "answer_id": shortuuid.uuid(), + "model_id": model_id, + "choices": choices, + "tstamp": time.time(), + } + fout.write(json.dumps(ans_json) + "\n") + + i += 1 + + +def get_tts_audios( + answers: List[Dict[str, Any]], + answer_file: str, + args: argparse.Namespace, +) -> None: + """Generate audios for a batch of audio tokens""" + config = EvaluationConfig(args) + if os.path.exists(os.path.join(args.base_model_path, "cosyvoice3.yaml")): + model = initialize_cosycoice3_model(config) + + for answer in tqdm(answers): + prompt_text = model.base_model.frontend.text_normalize( + answer["choices"][0]["prompt_text"], split=False, text_frontend=True + ) + prompt_wav = answer["choices"][0]["prompt_wav"] + for i in tqdm( + model.base_model.frontend.text_normalize( + answer["choices"][0]["tts_text"], split=True, text_frontend=True + ) + ): + model_input = model.base_model.frontend.frontend_zero_shot( + i, prompt_text, prompt_wav, model.base_model.sample_rate, "" + ) + + tts_speech_token = answer["choices"][0]["output_audio_tokens"] + while tts_speech_token[-1] == model.base_model.model.llm.eos_token: + del tts_speech_token[-1] + this_tts_speech_token = torch.tensor(tts_speech_token).unsqueeze(dim=0) + this_tts_speech = model.base_model.model.token2wav( + token=this_tts_speech_token, + prompt_token=model_input["flow_prompt_speech_token"], + prompt_feat=model_input["prompt_speech_feat"], + embedding=model_input["flow_embedding"], + token_offset=0, + uuid="", + finalize=True, + speed=1.0, + ) + this_tts_speech = this_tts_speech.cpu() + directory = os.path.dirname(answer_file) + os.makedirs(f"{directory}/eagle", exist_ok=True) + torchaudio.save( + f"{directory}/eagle/eval_{answer['question_id']}.wav", + this_tts_speech, + model.base_model.sample_rate, + ) + else: + raise NotImplementedError("Model not supported") + + def run_evaluation(config: EvaluationConfig, args: argparse.Namespace) -> None: """Run the evaluation with optional distributed processing""" questions = fastchat.llm_judge.common.load_questions( diff --git a/angelslim/compressor/speculative/inference/models/__init__.py b/angelslim/compressor/speculative/inference/models/__init__.py index a6696119..c03e73d7 100644 --- a/angelslim/compressor/speculative/inference/models/__init__.py +++ b/angelslim/compressor/speculative/inference/models/__init__.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .eagle3 import Eagle3Model +from .eagle3 import CosyVoice3Eagle3Model, Eagle3Model -__all__ = ["Eagle3Model"] +__all__ = ["Eagle3Model", "CosyVoice3Eagle3Model"] diff --git a/angelslim/compressor/speculative/inference/models/eagle3/__init__.py b/angelslim/compressor/speculative/inference/models/eagle3/__init__.py index 291fcd7a..37604579 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/__init__.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/__init__.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .eagle3_model import Eagle3Model +from .eagle3_model import CosyVoice3Eagle3Model, Eagle3Model -__all__ = ["Eagle3Model"] +__all__ = ["Eagle3Model", "CosyVoice3Eagle3Model"] diff --git a/angelslim/compressor/speculative/inference/models/eagle3/draft/__init__.py b/angelslim/compressor/speculative/inference/models/eagle3/draft/__init__.py index a4bc5eda..9055c65e 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/draft/__init__.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/draft/__init__.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .llama3_eagle3 import Llama3Eagle3Drafter +from .llama3_eagle3 import CosyVoice3Llama3Eagle3Drafter, Llama3Eagle3Drafter -__all__ = ["Llama3Eagle3Drafter"] +__all__ = ["Llama3Eagle3Drafter", "CosyVoice3Llama3Eagle3Drafter"] diff --git a/angelslim/compressor/speculative/inference/models/eagle3/draft/llama3_eagle3.py b/angelslim/compressor/speculative/inference/models/eagle3/draft/llama3_eagle3.py index a05fb5ec..8f3d3453 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/draft/llama3_eagle3.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/draft/llama3_eagle3.py @@ -13,11 +13,13 @@ # limitations under the License. import math -from typing import List, Optional, Tuple +import os +from typing import Any, List, Optional, Tuple import torch import torch.nn.functional as F -from torch import nn +from huggingface_hub import snapshot_download +from torch import Tensor, nn from transformers.activations import ACT2FN from .base_model import BaseEagle3Drafter @@ -710,3 +712,298 @@ def forward( hidden_states = layer_outputs[0] return hidden_states, next_decoder_cache, early_stop_signal + + +class CosyVoice3Llama3Eagle3Drafter(Llama3Eagle3Drafter): + + def load_embed(self, path: str) -> None: + # Handle HuggingFace model identifier + if not os.path.exists(path): + path = snapshot_download(repo_id=path) + + # Try loading embedding weights + tensor = torch.load("{}/llm.pt".format(path)) + embed_tokens_weight = tensor["speech_embedding.weight"] + + with torch.no_grad(): + self.embed_tokens.weight.copy_(embed_tokens_weight) + + def _get_initial_hidden( + self, hidden_states: Tensor, input_ids: Tensor, inputs_embeds: Tensor + ) -> Tuple[Tensor, Any]: + """Get initial hidden states and past key values.""" + if hasattr(self, "stable_kv") and self.stable_kv is not None: + kv_len = self.stable_kv[0][0].shape[2] + outputs = self( + hidden_states, + input_ids=input_ids[:, kv_len:], + inputs_embeds=inputs_embeds[:, kv_len:], + past_key_values=self.stable_kv, + use_cache=True, + ) + else: + outputs = self( + hidden_states, + input_ids=input_ids, + inputs_embeds=inputs_embeds, + use_cache=True, + ) + out_hidden, past_key_values = outputs + + return out_hidden[:, -1], past_key_values + + @torch.no_grad() + def topK_genrate( + self, + hidden_states: Tensor, + input_ids: Tensor, + inputs_embeddings: Tensor, + logits_processor: Optional[Any] = None, + ) -> Tuple[Tensor, Tensor, Tensor, Tensor]: + """ + Generate tokens using top-K speculative decoding. + + Args: + hidden_states: Hidden states from the target model + input_ids: Input token IDs + logits_processor: Optional logits processor + + Returns: + Tuple containing: + - draft_tokens: Generated draft tokens + - retrieve_indices: Indices for retrieving tokens from tree + - tree_mask: Mask for tree attention + - tree_position_ids: Position IDs in the tree + """ + # Initialize data structures + scores_list = [] + parents_list = [] + ss_token = [] + + # Prepare input and initialize tree + input_ids = input_ids.to(hidden_states.device) + sample_token = input_ids[:, -1] + input_ids = input_ids[:, 1:] + inputs_embeddings = inputs_embeddings[:, 1:] + self.initial_position_id = input_ids.shape[1] + + assert input_ids.shape[1] == inputs_embeddings.shape[1] + + self.reset() + + # Generate initial hidden states and tokens + last_hidden, past_key_values = self._get_initial_hidden( + hidden_states, input_ids, inputs_embeddings + ) + self.stable_kv = past_key_values + + # Generate first level of tokens + topk_index, scores = self._get_topk_tokens(last_hidden) + scores_list.append(scores) + parents_list.append(torch.zeros(1, dtype=torch.long, device=scores.device)) + + # Handle vocabulary mapping if needed + if self.config.vocab_size == self.config.draft_vocab_size: + ss_token.append(topk_index) + input_ids = topk_index + else: + mapped_tokens = topk_index + self.d2t[topk_index] + ss_token.append(mapped_tokens) + input_ids = mapped_tokens + + # Prepare for tree traversal + input_hidden = last_hidden[None].repeat(1, self.top_k, 1) + tree_mask = self.tree_mask_init + topk_cs_index = torch.arange(self.top_k, device=self.embed_tokens.weight.device) + + # Traverse the tree depth levels + for i in range(self.depth): + ( + tree_mask, + input_hidden, + input_ids, + scores, + topk_cs_index, + past_key_values, + ) = self._process_tree_level( + i, + tree_mask, + input_hidden, + input_ids, + scores, + topk_cs_index, + scores_list, + parents_list, + ss_token, + past_key_values, + ) + # Process the final results + draft_tokens, retrieve_indices, tree_mask, tree_position_ids = ( + self._finalize_results( + scores_list, ss_token, sample_token, parents_list, logits_processor + ) + ) + + # Delete some used lists and variables to free memory + del scores_list, parents_list, ss_token + + return ( + draft_tokens, + retrieve_indices, + tree_mask, + tree_position_ids, + ) + + def forward( + self, + hidden_states, + input_ids, + inputs_embeds: Optional[torch.Tensor], + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[List[torch.FloatTensor]] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + ): + batch_size, seq_length, _ = hidden_states.shape + seq_length_with_past = seq_length + past_key_values_length = 0 + + if inputs_embeds is None: + with torch.no_grad(): + inputs_embeds = self.embed_tokens.weight[ + input_ids.squeeze(0).tolist() + ].unsqueeze(0) + + if past_key_values is not None: + past_key_values_length = past_key_values[0][0].shape[2] + seq_length_with_past = seq_length_with_past + past_key_values_length + if position_ids is None: + device = ( + hidden_states.device + if hidden_states is not None + else inputs_embeds.device + ) + position_ids = torch.arange( + past_key_values_length, + seq_length + past_key_values_length, + dtype=torch.long, + device=device, + ) + position_ids = position_ids.unsqueeze(0).view(-1, seq_length) + else: + position_ids = position_ids.view(-1, seq_length).long() + + if attention_mask is None: + attention_mask = torch.ones( + (batch_size, seq_length_with_past), + dtype=torch.bool, + device=hidden_states.device, + ) + attention_mask = self._prepare_decoder_attention_mask( + attention_mask, + (batch_size, seq_length), + hidden_states, + past_key_values_length, + ) + + dtype = self.fc.weight.dtype + inputs_embeds = inputs_embeds.to(dtype) + hidden_states = hidden_states.to(dtype) + if hidden_states.shape[-1] != inputs_embeds.shape[-1]: + hidden_states = self.fc(hidden_states) + + next_decoder_cache = () if use_cache else None + + past_key_value = past_key_values[0] if past_key_values is not None else None + layer_outputs = self.midlayer( + input_emb=inputs_embeds, + hidden_states=hidden_states, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_value=past_key_value, + output_attentions=output_attentions, + use_cache=True, + ) + if use_cache: + next_decoder_cache += (layer_outputs[2 if output_attentions else 1],) + hidden_states = layer_outputs[0] + + return hidden_states, next_decoder_cache + + def _get_topk_tokens(self, hidden: Tensor) -> Tuple[Tensor, Tensor]: + """Get top-k tokens from hidden states.""" + logits = self.lm_head(self.norm(hidden)) + probs = self.logsoftmax(logits) + topk = torch.topk(probs, self.top_k, dim=-1) + return topk.indices, topk.values + + def _process_tree_level( + self, + level: int, + tree_mask: Tensor, + input_hidden: Tensor, + input_ids: Tensor, + scores: Tensor, + topk_cs_index: Tensor, + scores_list: list, + parents_list: list, + ss_token: list, + past_key_values, + ) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: + """Process one level of the speculative decoding tree.""" + self.tree_mask = tree_mask + position_ids = self.position_ids + self.initial_position_id + + # Get next level hidden states + out_hidden, past_key_values = self( + input_hidden, + input_ids=input_ids, + inputs_embeds=None, + past_key_values=past_key_values, + position_ids=position_ids, + use_cache=True, + ) + self.initial_position_id += 1 + + # Calculate parent indices + bias1 = self.top_k if level > 0 else 0 + bias2 = max(0, level - 1) + bias = 1 + self.top_k**2 * bias2 + bias1 + parents = topk_cs_index + bias + parents_list.append(parents) + + # Get top-k tokens for this level + topk_index, topk_p = self._get_topk_tokens(out_hidden[0]) + if len(scores.shape) == 2: + scores = scores.squeeze(0) + cu_scores = topk_p + scores[:, None] + + # Select best candidates + topk_cs = torch.topk(cu_scores.view(-1), self.top_k, dim=-1) + topk_cs_index, scores = topk_cs.indices, topk_cs.values + + # Update data structures + out_ids = topk_cs_index // self.top_k + input_hidden = out_hidden[:, out_ids] + input_ids = topk_index.view(-1)[topk_cs_index][None] + + # Handle vocabulary mapping if needed + if self.config.vocab_size == self.config.draft_vocab_size: + ss_token.append(topk_index) + else: + input_ids = input_ids + self.d2t[input_ids] + ss_token.append(topk_index + self.d2t[topk_index]) + + scores_list.append(cu_scores) + tree_mask = torch.cat((tree_mask[:, :, out_ids], self.tree_mask_init), dim=3) + + return ( + tree_mask, + input_hidden, + input_ids, + scores, + topk_cs_index, + past_key_values, + ) diff --git a/angelslim/compressor/speculative/inference/models/eagle3/eagle3_model.py b/angelslim/compressor/speculative/inference/models/eagle3/eagle3_model.py index 113ec450..dfdd97fb 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/eagle3_model.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/eagle3_model.py @@ -30,13 +30,17 @@ evaluate_posterior, initialize_past_key_values, initialize_tree, + initialize_tree_cosyvoice3, prepare_logits_processor, reset_tree_mode, tree_decoding, + tree_decoding_cosyvoice3, update_inference_inputs, + update_inference_inputs_cosyvoice3, ) from .configuration_eagle3_model import Eagle3Config -from .draft import Llama3Eagle3Drafter +from .draft import CosyVoice3Llama3Eagle3Drafter, Llama3Eagle3Drafter +from .target import CosyVoice3 as KVCosyVoice3 from .target import LlamaForCausalLM as KVLlamaForCausalLM from .target import Qwen3ForCausalLM as KVQwen3ForCausalLM @@ -72,20 +76,30 @@ class ModelLoader: SUPPORTED_ARCHITECTURES = { "LlamaForCausalLM": KVLlamaForCausalLM, "Qwen3ForCausalLM": KVQwen3ForCausalLM, + "CosyVoice3": KVCosyVoice3, } @classmethod def load_base_model(cls, base_model_path: str, **kwargs) -> nn.Module: """Load base model based on architecture""" - config = AutoConfig.from_pretrained(base_model_path) - if not getattr(config, "architectures", None): - raise ValueError("Base model config missing 'architectures' field") + try: + config = AutoConfig.from_pretrained(base_model_path) + if not getattr(config, "architectures", None): + raise ValueError("Base model config missing 'architectures' field") + arch = config.architectures[0] + except ValueError: + if os.path.exists(os.path.join(base_model_path, "cosyvoice3.yaml")): + arch = "CosyVoice3" + else: + raise ValueError - arch = config.architectures[0] if arch not in cls.SUPPORTED_ARCHITECTURES: raise NotImplementedError(f"Model {arch} not supported") model_class = cls.SUPPORTED_ARCHITECTURES[arch] + if arch == "CosyVoice3": + model = model_class(base_model_path, kwargs["generate_audio"]) + return model return model_class.from_pretrained(base_model_path, **kwargs) @classmethod @@ -194,6 +208,48 @@ def prepare_generation( input_len=input_ids.shape[1], ) + def prepare_generation_cosyvoice3( + self, model: "Eagle3Model", input_ids: torch.Tensor, config: GenerationConfig + ) -> GenerationState: + """Prepare all necessary components for generation""" + stop_token_id = model.base_model.model.llm.stop_token_ids + + logits_processor = ( + prepare_logits_processor( + temperature=config.temperature, top_p=config.top_p, top_k=config.top_k + ) + if config.temperature > 1e-5 + else None + ) + + input_ids = input_ids.clone() + model.eagle_layer.reset_kv() + + if hasattr(model, "past_key_values"): + past_key_values = model.past_key_values + model.current_length_data.zero_() + else: + past_key_values, past_key_values_data, current_length_data = ( + initialize_past_key_values( + model.base_model.model.llm.llm.model, max_length=config.max_length + ) + ) + model.past_key_values = past_key_values + model.past_key_values_data = past_key_values_data + model.current_length_data = current_length_data + + # reset_tree_mode(model) + model.base_model.model.llm.llm.model.model.tree_mask = None + model.base_model.model.llm.llm.model.model.tree_mode = None + + return GenerationState( + stop_token_id=stop_token_id, + logits_processor=logits_processor, + input_ids=input_ids, + past_key_values=past_key_values, + input_len=input_ids.shape[1], + ) + def should_stop( self, input_ids: torch.Tensor, @@ -218,6 +274,28 @@ def should_stop( return False + def should_stop_cosyvoice3( + self, + input_ids: torch.Tensor, + input_len: int, + new_token: int, + config: GenerationConfig, + stop_token_id: Optional[int], + ) -> bool: + """Check if generation should stop""" + if stop_token_id is not None: + stop_tensor = torch.tensor(stop_token_id, device=input_ids.device) + if torch.any(torch.isin(input_ids[0, input_len:], stop_tensor)): + return True + + if new_token > config.max_new_tokens: + return True + + if input_ids.shape[1] > config.max_length: + return True + + return False + def get_padding_token(self, device: torch.device) -> torch.Tensor: """Get or create padding token""" if self._padding_token is None or self._padding_token.device != device: @@ -562,3 +640,355 @@ def naive_generate( break return (state.input_ids, state.new_token, step) if log else state.input_ids + + +class CosyVoice3Eagle3Model(nn.Module): + """ + CosyVoice3 EAGLE3 Model for speculative decoding + """ + + def __init__( + self, + base_model: nn.Module, + eagle_layer: nn.Module, + ): + super().__init__() + self.base_model = base_model + self.eagle_layer = eagle_layer + self.generation_manager = GenerationManager(None) + + @classmethod + def from_pretrained( + cls, + base_model_path: Optional[str] = None, + eagle_model_path: Optional[str] = None, + total_token: int = 60, + depth: int = 7, + top_k: int = 10, + threshold: float = 1.0, + enable_benchmark: bool = False, + **kwargs, + ) -> "CosyVoice3Eagle3Model": + """Create CosyVoice3Eagle3Model from pretrained components""" + # Load base model and tokenizer + base_model = ModelLoader.load_base_model(base_model_path, **kwargs) + # Load configuration + config_path = ModelLoader.ensure_config_path(eagle_model_path) + config = Eagle3Config.from_pretrained(config_path) + + # Initialize EAGLE layer + device = next(base_model.model.llm.parameters()).device + eagle_state_dict = ModelLoader.load_eagle_state_dict(eagle_model_path, device) + + # TODO: Implement factory pattern for different drafter types + eagle_layer = CosyVoice3Llama3Eagle3Drafter( + config, + total_tokens=total_token, + depth=depth, + top_k=top_k, + threshold=threshold, + path=base_model_path, + load_emb=True, + ) + + # Clean up unused components + if config.vocab_size == config.draft_vocab_size: + del eagle_layer.d2t + del eagle_layer.t2d + + eagle_layer.load_state_dict(eagle_state_dict, strict=False) + eagle_layer.to(device=device, dtype=base_model.dtype) + eagle_layer.init_tree() + + # Auto-select optimal token count if needed + if total_token == -1 and enable_benchmark: + total_token = PerformanceBenchmark.auto_select_total_token( + base_model, config.vocab_size + ) + eagle_layer.total_tokens = total_token - 1 + + return cls(base_model, eagle_layer) + + def forward( + self, + text: torch.Tensor, + prompt_text: torch.Tensor, + llm_prompt_speech_token: torch.Tensor, + past_key_values: Optional[Any] = None, + ) -> Union[Tuple[Any, torch.Tensor], Tuple[Any, torch.Tensor, torch.Tensor]]: + """Forward pass through the model""" + device = self.base_model.model.device + first_token, hidden_states, inputs_embeddings = self.base_model.model.llm( + text=text.to(device), + text_len=torch.tensor([text.shape[1]], dtype=torch.int32).to(device), + prompt_text=prompt_text.to(device), + prompt_text_len=torch.tensor([prompt_text.shape[1]], dtype=torch.int32).to( + device + ), + prompt_speech_token=llm_prompt_speech_token.to(device), + prompt_speech_token_len=torch.tensor( + [llm_prompt_speech_token.shape[1]], dtype=torch.int32 + ).to(device), + past_key_values=past_key_values, + ) + first_token = torch.tensor(first_token).unsqueeze(dim=0) + + return first_token, hidden_states, inputs_embeddings + + def tree_decoding_forward( + self, + input_ids: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + past_key_values: Optional[Any] = None, + position_ids: Optional[torch.Tensor] = None, + ) -> Union[Tuple[Any, torch.Tensor], Tuple[Any, torch.Tensor, torch.Tensor]]: + """Forward pass through the model""" + logits, hidden_states = self.base_model.model.llm.inference_one_step( + input_ids=input_ids, + attention_mask=attention_mask, + past_key_values=past_key_values, + position_ids=position_ids, + ) + + return logits, hidden_states + + @torch.no_grad() + def eagle_generate( + self, + text: Optional[torch.Tensor] = None, + prompt_text: Optional[torch.Tensor] = None, + llm_prompt_speech_token: Optional[torch.Tensor] = None, + temperature: float = 0.0, + top_p: float = 0.0, + top_k: float = 0.0, + max_new_tokens: int = 512, + max_length: int = 2048, + log: bool = False, + is_llama3: bool = False, + **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, int, int, List[int]]]: + """Generate text using EAGLE speculative decoding""" + config = GenerationConfig( + temperature=temperature, + top_p=top_p, + top_k=top_k, + max_new_tokens=max_new_tokens, + max_length=max_length, + log=log, + is_llama3=is_llama3, + ) + + input_ids = torch.concat( + [ + self.base_model.model.llm.sos_id.unsqueeze(dim=0) + .to(text.dtype) + .to(text.device), + prompt_text, + text, + self.base_model.model.llm.task_token.unsqueeze(dim=0) + .to(text.dtype) + .to(text.device), + llm_prompt_speech_token, + ], + dim=1, + ) + state = self.generation_manager.prepare_generation_cosyvoice3( + self, input_ids, config + ) + padding = self.generation_manager.get_padding_token(text.device) + + # Prefill phase + ( + draft_tokens, + retrieve_indices, + tree_mask, + tree_position_ids, + logits, + inputs_embeddings, + first_token, + ) = initialize_tree_cosyvoice3( + text, + prompt_text, + llm_prompt_speech_token, + input_ids, + self, + state.past_key_values, + state.logits_processor, + ) + + accept_length_list = [] + max_decode_steps = config.max_length - self.eagle_layer.total_tokens - 10 + + out_tokens = [] + out_tokens.append(first_token.item()) + + for step in range(max_decode_steps): # noqa: B007 + # Ensure tensors are on correct device + draft_tokens = draft_tokens.to(input_ids.device) + tree_position_ids = tree_position_ids.to(input_ids.device) + + self.base_model.model.llm.llm.model.model.tree_mask = tree_mask + + # Target model forward pass + logits, hidden_state_new = tree_decoding_cosyvoice3( + self, + draft_tokens, + state.past_key_values, + tree_position_ids, + state.input_ids, + retrieve_indices, + ) + + draft_tokens = torch.cat((draft_tokens, padding), dim=1) + candidates = draft_tokens[0, retrieve_indices] + + # Verification phase + best_candidate, accept_length, sample_token = evaluate_posterior( + logits, candidates, state.logits_processor + ) + + accept_length_list.append( + accept_length.item() + if torch.is_tensor(accept_length) + else accept_length + ) + + out_tokens.extend(candidates[best_candidate, : accept_length + 1].tolist()) + out_tokens.append(sample_token.item()) + + # Update inference inputs + ( + state.input_ids, + inputs_embeddings, + draft_tokens, + retrieve_indices, + tree_mask, + tree_position_ids, + state.new_token, + ) = update_inference_inputs_cosyvoice3( + input_ids=state.input_ids, + inputs_embeddings=inputs_embeddings, + candidates=candidates, + best_candidate=best_candidate, + accept_length=accept_length, + retrieve_indices=retrieve_indices, + logits_processor=state.logits_processor, + new_token=state.new_token, + past_key_values_data_list=self.past_key_values_data, + current_length_data=self.current_length_data, + model=self, + hidden_state_new=hidden_state_new, + sample_token=sample_token, + ) + + if self.generation_manager.should_stop_cosyvoice3( + state.input_ids, + state.input_len, + state.new_token, + config, + state.stop_token_id, + ): + break + + return ( + (state.input_ids, state.new_token, step, accept_length_list) + if log + else state.input_ids + ) + + @torch.no_grad() + def naive_generate( + self, + text: Optional[torch.Tensor] = None, + prompt_text: Optional[torch.Tensor] = None, + llm_prompt_speech_token: Optional[torch.Tensor] = None, + temperature: float = 0.0, + top_p: float = 0.0, + top_k: float = 0.0, + max_new_tokens: int = 512, + max_length: int = 2048, + log: bool = False, + is_llama3: bool = False, + max_token_text_ratio: float = 20.0, + min_token_text_ratio: float = 2.0, + **kwargs, + ) -> Union[torch.Tensor, Tuple[torch.Tensor, int, int]]: + """Generate text using naive (non-speculative) decoding""" + config = GenerationConfig( + temperature=temperature, + top_p=top_p, + top_k=top_k, + max_new_tokens=max_new_tokens, + max_length=max_length, + log=log, + is_llama3=is_llama3, + ) + + input_ids = torch.concat( + [ + self.base_model.model.llm.sos_id.unsqueeze(dim=0) + .to(text.dtype) + .to(text.device), + prompt_text, + text, + self.base_model.model.llm.task_token.unsqueeze(dim=0) + .to(text.dtype) + .to(text.device), + llm_prompt_speech_token, + ], + dim=1, + ) + state = self.generation_manager.prepare_generation_cosyvoice3( + self, input_ids, config + ) + + device = self.base_model.model.device + logits = self.base_model.model.llm( + text=text.to(device), + text_len=torch.tensor([text.shape[1]], dtype=torch.int32).to(device), + prompt_text=prompt_text.to(device), + prompt_text_len=torch.tensor([prompt_text.shape[1]], dtype=torch.int32).to( + device + ), + prompt_speech_token=llm_prompt_speech_token.to(device), + prompt_speech_token_len=torch.tensor( + [llm_prompt_speech_token.shape[1]], dtype=torch.int32 + ).to(device), + past_key_values=state.past_key_values, + return_first_token=False, + ) + + out_tokens = [] + min_len = int(text.shape[1] * min_token_text_ratio) + max_decode_steps = int(text.shape[1] * max_token_text_ratio) + + for step in range(max_decode_steps): # noqa: B007 + input_id = self.base_model.model.llm.sampling_ids( + logits.squeeze(dim=0), + out_tokens, + ignore_eos=True if step < min_len else False, + ) + out_tokens.append(input_id) + input_id = ( + torch.tensor(input_id, device=state.input_ids.device) + .unsqueeze(0) + .unsqueeze(0) + ) + + logits, _ = self.base_model.model.llm.inference_one_step( + input_id, past_key_values=state.past_key_values + ) + logits = logits.squeeze(0) + state.input_ids = torch.cat([state.input_ids, input_id], dim=-1) + state.new_token += 1 + + if self.generation_manager.should_stop_cosyvoice3( + state.input_ids, + state.input_len, + state.new_token, + config, + state.stop_token_id, + ): + break + + return (state.input_ids, state.new_token, step) if log else state.input_ids diff --git a/angelslim/compressor/speculative/inference/models/eagle3/target/__init__.py b/angelslim/compressor/speculative/inference/models/eagle3/target/__init__.py index 530d0478..733a55e5 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/target/__init__.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/target/__init__.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from .modeling_cosyvoice3_kv import CosyVoice3 from .modeling_llama_kv import LlamaForCausalLM from .modeling_qwen3_kv import Qwen3ForCausalLM -__all__ = ["LlamaForCausalLM", "Qwen3ForCausalLM"] +__all__ = ["LlamaForCausalLM", "Qwen3ForCausalLM", "CosyVoice3"] diff --git a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_cosyvoice3_kv.py b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_cosyvoice3_kv.py new file mode 100644 index 00000000..bb00e12e --- /dev/null +++ b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_cosyvoice3_kv.py @@ -0,0 +1,984 @@ +# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu, Zhihao Du) +# 2025 Alibaba Inc (authors: Xiang Lyu, Yabin Li, Qihua, Shengqiang Li) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from https://github.com/FunAudioLLM/CosyVoice for AngelSlim project + +import functools +import os +import re +from functools import partial +from typing import Any, Callable, Generator, List, Optional + +import numpy as np +import regex +import torch +from torch import nn +from torch.nn import functional as F +from transformers import AutoTokenizer +from transformers.configuration_utils import PretrainedConfig + +from .......utils.lazy_imports import ( + inflect, + librosa, + onnxruntime, + torchaudio, + wetext, + whisper, +) +from .modeling_qwen2_kv import Qwen2ForCausalLM + +IGNORE_ID = -1 +# cosyvoice3 fixed params +use_ttsfrd = False +sample_rate = 24000 +llm_input_size = 896 +llm_output_size = 896 +spk_embed_dim = 192 +token_frame_rate = 25 +token_mel_ratio = 2 +# stream related params +chunk_size = 25 # streaming inference chunk size, in token +num_decoding_left_chunks = ( + -1 +) # streaming inference flow decoder left chunk size, <0 means use all left chunks + + +# Repetition Aware Sampling in VALL-E 2 +def ras_sampling( + weighted_scores, + decoded_tokens, + sampling, + top_p=0.8, + top_k=25, + win_size=10, + tau_r=0.1, +): + top_ids = nucleus_sampling(weighted_scores, top_p=top_p, top_k=top_k) + rep_num = ( + (torch.tensor(decoded_tokens[-win_size:]).to(weighted_scores.device) == top_ids) + .sum() + .item() + ) + if rep_num >= win_size * tau_r: + top_ids = random_sampling(weighted_scores, decoded_tokens, sampling) + return top_ids + + +def nucleus_sampling(weighted_scores, top_p=0.8, top_k=25): + prob, indices = [], [] + cum_prob = 0.0 + sorted_value, sorted_idx = weighted_scores.softmax(dim=0).sort( + descending=True, stable=True + ) + for i in range(len(sorted_idx)): + # sampling both top-p and numbers. + if cum_prob < top_p and len(prob) < top_k: + cum_prob += sorted_value[i] + prob.append(sorted_value[i]) + indices.append(sorted_idx[i]) + else: + break + prob = torch.tensor(prob).to(weighted_scores) + indices = torch.tensor(indices, dtype=torch.long).to(weighted_scores.device) + top_ids = indices[prob.multinomial(1, replacement=True)].item() + return top_ids + + +def random_sampling(weighted_scores, decoded_tokens, sampling): + top_ids = weighted_scores.softmax(dim=0).multinomial(1, replacement=True).item() + return top_ids + + +def make_pad_mask(lengths: torch.Tensor, max_len: int = 0) -> torch.Tensor: + """Make mask tensor containing indices of padded part. + + See description of make_non_pad_mask. + + Args: + lengths (torch.Tensor): Batch of lengths (B,). + Returns: + torch.Tensor: Mask tensor containing indices of padded part. + + Examples: + >>> lengths = [5, 3, 2] + >>> make_pad_mask(lengths) + masks = [[0, 0, 0, 0 ,0], + [0, 0, 0, 1, 1], + [0, 0, 1, 1, 1]] + """ + batch_size = lengths.size(0) + max_len = max_len if max_len > 0 else lengths.max().item() + seq_range = torch.arange(0, max_len, dtype=torch.int64, device=lengths.device) + seq_range_expand = seq_range.unsqueeze(0).expand(batch_size, max_len) + seq_length_expand = lengths.unsqueeze(-1) + mask = seq_range_expand >= seq_length_expand + return mask + + +def get_qwen_tokenizer( + token_path: str, skip_special_tokens: bool, version: str = "cosyvoice3" +): + if version == "cosyvoice3": + return CosyVoice3Tokenizer( + token_path=token_path, skip_special_tokens=skip_special_tokens + ) + else: + raise ValueError + + +mel_basis = {} +hann_window = {} + + +def dynamic_range_compression_torch(x, C=1, clip_val=1e-5): + return torch.log(torch.clamp(x, min=clip_val) * C) + + +def spectral_normalize_torch(magnitudes): + output = dynamic_range_compression_torch(magnitudes) + return output + + +def mel_spectrogram( + y, n_fft, num_mels, sampling_rate, hop_size, win_size, fmin, fmax, center=False +): + if torch.min(y) < -1.0: + print("min value is ", torch.min(y)) + if torch.max(y) > 1.0: + print("max value is ", torch.max(y)) + + global mel_basis, hann_window # pylint: disable=global-statement + if f"{str(fmax)}_{str(y.device)}" not in mel_basis: + mel = librosa.filters.mel( + sr=sampling_rate, n_fft=n_fft, n_mels=num_mels, fmin=fmin, fmax=fmax + ) + mel_basis[str(fmax) + "_" + str(y.device)] = ( + torch.from_numpy(mel).float().to(y.device) + ) + hann_window[str(y.device)] = torch.hann_window(win_size).to(y.device) + + y = torch.nn.functional.pad( + y.unsqueeze(1), + (int((n_fft - hop_size) / 2), int((n_fft - hop_size) / 2)), + mode="reflect", + ) + y = y.squeeze(1) + + spec = torch.view_as_real( + torch.stft( + y, + n_fft, + hop_length=hop_size, + win_length=win_size, + window=hann_window[str(y.device)], + center=center, + pad_mode="reflect", + normalized=False, + onesided=True, + return_complex=True, + ) + ) + + spec = torch.sqrt(spec.pow(2).sum(-1) + (1e-9)) + + spec = torch.matmul(mel_basis[str(fmax) + "_" + str(y.device)], spec) + spec = spectral_normalize_torch(spec) + + return spec + + +chinese_char_pattern = re.compile(r"[\u4e00-\u9fff]+") + + +# whether contain chinese character +def contains_chinese(text): + return bool(chinese_char_pattern.search(text)) + + +# replace special symbol +def replace_corner_mark(text): + text = text.replace("²", "平方") + text = text.replace("³", "立方") + return text + + +# remove meaningless symbol +def remove_bracket(text): + text = text.replace("(", "").replace(")", "") + text = text.replace("【", "").replace("】", "") + text = text.replace("`", "").replace("`", "") + text = text.replace("——", " ") + return text + + +# spell Arabic numerals +def spell_out_number(text: str, inflect_parser): + new_text = [] + st = None + for i, c in enumerate(text): + if not c.isdigit(): + if st is not None: + num_str = inflect_parser.number_to_words(text[st:i]) + new_text.append(num_str) + st = None + new_text.append(c) + else: + if st is None: + st = i + if st is not None and st < len(text): + num_str = inflect_parser.number_to_words(text[st:]) + new_text.append(num_str) + return "".join(new_text) + + +def split_paragraph( + text: str, + tokenize, + lang="zh", + token_max_n=80, + token_min_n=60, + merge_len=20, + comma_split=False, +): + def calc_utt_length(_text: str): + if lang == "zh": + return len(_text) + else: + return len(tokenize(_text)) + + def should_merge(_text: str): + if lang == "zh": + return len(_text) < merge_len + else: + return len(tokenize(_text)) < merge_len + + if lang == "zh": + pounc = ["。", "?", "!", ";", ":", "、", ".", "?", "!", ";"] + else: + pounc = [".", "?", "!", ";", ":"] + if comma_split: + pounc.extend([",", ","]) + + if text[-1] not in pounc: + if lang == "zh": + text += "。" + else: + text += "." + + st = 0 + utts = [] + for i, c in enumerate(text): + if c in pounc: + if len(text[st:i]) > 0: + utts.append(text[st:i] + c) + if i + 1 < len(text) and text[i + 1] in ['"', "”"]: + tmp = utts.pop(-1) + utts.append(tmp + text[i + 1]) + st = i + 2 + else: + st = i + 1 + + final_utts = [] + cur_utt = "" + for utt in utts: + if ( + calc_utt_length(cur_utt + utt) > token_max_n + and calc_utt_length(cur_utt) > token_min_n + ): + final_utts.append(cur_utt) + cur_utt = "" + cur_utt = cur_utt + utt + if len(cur_utt) > 0: + if should_merge(cur_utt) and len(final_utts) != 0: + final_utts[-1] = final_utts[-1] + cur_utt + else: + final_utts.append(cur_utt) + + return final_utts + + +# remove blank between chinese character +def replace_blank(text: str): + out_str = [] + for i, c in enumerate(text): + if c == " ": + if (text[i + 1].isascii() and text[i + 1] != " ") and ( + text[i - 1].isascii() and text[i - 1] != " " + ): + out_str.append(c) + else: + out_str.append(c) + return "".join(out_str) + + +def is_only_punctuation(text): + # Regular expression: Match strings that consist only of punctuation marks or are empty. + punctuation_pattern = r"^[\p{P}\p{S}]*$" + return bool(regex.fullmatch(punctuation_pattern, text)) + + +def load_wav(wav, target_sr, min_sr=16000): + speech, sample_rate = torchaudio.load(wav, backend="soundfile") + speech = speech.mean(dim=0, keepdim=True) + if sample_rate != target_sr: + assert ( + sample_rate >= min_sr + ), "wav sample rate {} must be greater than {}".format(sample_rate, target_sr) + speech = torchaudio.transforms.Resample( + orig_freq=sample_rate, new_freq=target_sr + )(speech) + return speech + + +class Qwen2Encoder(torch.nn.Module): + def __init__(self, pretrain_path): + super().__init__() + self.model = Qwen2ForCausalLM.from_pretrained( + pretrain_path, attn_implementation="eager" + ) + + def forward_one_step( + self, + xs, + masks=None, + past_key_values=None, + position_ids=None, + output_hidden_states=False, + return_hidden_states=False, + ): + if masks is not None: + input_masks = masks[:, -1, :] + else: + input_masks = None + outs = self.model( + inputs_embeds=xs, + attention_mask=input_masks, + output_hidden_states=output_hidden_states, + past_key_values=past_key_values, + position_ids=position_ids, + ) + xs = outs.hidden_states[-1] + + if return_hidden_states: + return xs, outs["hidden_states"][:-1] + return xs + + +class CosyVoice3Tokenizer: + def __init__(self, token_path, skip_special_tokens=True): + # NOTE: non-chat model, all these special tokens keep randomly initialized. + # fmt: off + # flake8: noqa + special_tokens = { + 'eos_token': '<|endoftext|>', + 'pad_token': '<|endoftext|>', + 'additional_special_tokens': [ + '<|im_start|>', '<|im_end|>', '<|endofprompt|>', + '[breath]', '', '', '[noise]', + '[laughter]', '[cough]', '[clucking]', '[accent]', + '[quick_breath]', + "", "", + "[hissing]", "[sigh]", "[vocalized-noise]", + "[lipsmack]", "[mn]", "<|endofsystem|>", + "[AA]", "[AA0]", "[AA1]", "[AA2]", "[AE]", "[AE0]", "[AE1]", "[AE2]", "[AH]", "[AH0]", "[AH1]", "[AH2]", + "[AO]", "[AO0]", "[AO1]", "[AO2]", "[AW]", "[AW0]", "[AW1]", "[AW2]", "[AY]", "[AY0]", "[AY1]", "[AY2]", + "[B]", "[CH]", "[D]", "[DH]", "[EH]", "[EH0]", "[EH1]", "[EH2]", "[ER]", "[ER0]", "[ER1]", "[ER2]", "[EY]", + "[EY0]", "[EY1]", "[EY2]", "[F]", "[G]", "[HH]", "[IH]", "[IH0]", "[IH1]", "[IH2]", "[IY]", "[IY0]", "[IY1]", + "[IY2]", "[JH]", "[K]", "[L]", "[M]", "[N]", "[NG]", "[OW]", "[OW0]", "[OW1]", "[OW2]", "[OY]", "[OY0]", + "[OY1]", "[OY2]", "[P]", "[R]", "[S]", "[SH]", "[T]", "[TH]", "[UH]", "[UH0]", "[UH1]", "[UH2]", "[UW]", + "[UW0]", "[UW1]", "[UW2]", "[V]", "[W]", "[Y]", "[Z]", "[ZH]", + "[a]", "[ai]", "[an]", "[ang]", "[ao]", "[b]", "[c]", "[ch]", "[d]", "[e]", "[ei]", "[en]", "[eng]", "[f]", + "[g]", "[h]", "[i]", "[ian]", "[in]", "[ing]", "[iu]", "[ià]", "[iàn]", "[iàng]", "[iào]", "[iá]", "[ián]", + "[iáng]", "[iáo]", "[iè]", "[ié]", "[iòng]", "[ióng]", "[iù]", "[iú]", "[iā]", "[iān]", "[iāng]", "[iāo]", + "[iē]", "[iě]", "[iōng]", "[iū]", "[iǎ]", "[iǎn]", "[iǎng]", "[iǎo]", "[iǒng]", "[iǔ]", "[j]", "[k]", "[l]", + "[m]", "[n]", "[o]", "[ong]", "[ou]", "[p]", "[q]", "[r]", "[s]", "[sh]", "[t]", "[u]", "[uang]", "[ue]", + "[un]", "[uo]", "[uà]", "[uài]", "[uàn]", "[uàng]", "[uá]", "[uái]", "[uán]", "[uáng]", "[uè]", "[ué]", "[uì]", + "[uí]", "[uò]", "[uó]", "[uā]", "[uāi]", "[uān]", "[uāng]", "[uē]", "[uě]", "[uī]", "[uō]", "[uǎ]", "[uǎi]", + "[uǎn]", "[uǎng]", "[uǐ]", "[uǒ]", "[vè]", "[w]", "[x]", "[y]", "[z]", "[zh]", "[à]", "[ài]", "[àn]", "[àng]", + "[ào]", "[á]", "[ái]", "[án]", "[áng]", "[áo]", "[è]", "[èi]", "[èn]", "[èng]", "[èr]", "[é]", "[éi]", "[én]", + "[éng]", "[ér]", "[ì]", "[ìn]", "[ìng]", "[í]", "[ín]", "[íng]", "[ò]", "[òng]", "[òu]", "[ó]", "[óng]", "[óu]", + "[ù]", "[ùn]", "[ú]", "[ún]", "[ā]", "[āi]", "[ān]", "[āng]", "[āo]", "[ē]", "[ēi]", "[ēn]", "[ēng]", "[ě]", + "[ěi]", "[ěn]", "[ěng]", "[ěr]", "[ī]", "[īn]", "[īng]", "[ō]", "[ōng]", "[ōu]", "[ū]", "[ūn]", "[ǎ]", "[ǎi]", + "[ǎn]", "[ǎng]", "[ǎo]", "[ǐ]", "[ǐn]", "[ǐng]", "[ǒ]", "[ǒng]", "[ǒu]", "[ǔ]", "[ǔn]", "[ǘ]", "[ǚ]", "[ǜ]" + ] + } + # fmt: on + self.special_tokens = special_tokens + self.tokenizer = AutoTokenizer.from_pretrained(token_path) + self.tokenizer.add_special_tokens(special_tokens) + self.skip_special_tokens = skip_special_tokens + + def encode(self, text, **kwargs): + tokens = self.tokenizer([text], return_tensors="pt") + tokens = tokens["input_ids"][0].cpu().tolist() + return tokens + + def decode(self, tokens): + tokens = torch.tensor(tokens, dtype=torch.int64) + text = self.tokenizer.batch_decode( + [tokens], skip_special_tokens=self.skip_special_tokens + )[0] + return text + + +class CosyVoiceFrontEnd: + + def __init__( + self, + get_tokenizer: Callable, + feat_extractor: Callable, + campplus_model: str, + speech_tokenizer_model: str, + spk2info: str = "", + allowed_special: str = "all", + ): + self.tokenizer = get_tokenizer() + self.feat_extractor = feat_extractor + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + option = onnxruntime.SessionOptions() + option.graph_optimization_level = ( + onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL + ) + option.intra_op_num_threads = 1 + self.campplus_session = onnxruntime.InferenceSession( + campplus_model, sess_options=option, providers=["CPUExecutionProvider"] + ) + self.speech_tokenizer_session = onnxruntime.InferenceSession( + speech_tokenizer_model, + sess_options=option, + providers=[ + ( + "CUDAExecutionProvider" + if torch.cuda.is_available() + else "CPUExecutionProvider" + ) + ], + ) + if os.path.exists(spk2info): + self.spk2info = torch.load(spk2info, map_location=self.device) + else: + self.spk2info = {} + self.allowed_special = allowed_special + self.zh_tn_model = wetext.Normalizer(remove_erhua=False) + self.en_tn_model = wetext.Normalizer() + self.inflect_parser = inflect.engine() + + def text_normalize(self, text, split=True, text_frontend=True): + if isinstance(text, Generator): + print("get tts_text generator, will skip text_normalize!") + return [text] + # NOTE skip text_frontend when ssml symbol in text + if "<|" in text and "|>" in text: + text_frontend = False + if text_frontend is False or text == "": + return [text] if split is True else text + text = text.strip() + if contains_chinese(text): + text = self.zh_tn_model.normalize(text) + text = text.replace("\n", "") + text = replace_blank(text) + text = replace_corner_mark(text) + text = text.replace(".", "。") + text = text.replace(" - ", ",") + text = remove_bracket(text) + text = re.sub(r"[,,、]+$", "。", text) + texts = list( + split_paragraph( + text, + partial( + self.tokenizer.encode, allowed_special=self.allowed_special + ), + "zh", + token_max_n=80, + token_min_n=60, + merge_len=20, + comma_split=False, + ) + ) + else: + text = self.en_tn_model.normalize(text) + text = spell_out_number(text, self.inflect_parser) + texts = list( + split_paragraph( + text, + partial( + self.tokenizer.encode, allowed_special=self.allowed_special + ), + "en", + token_max_n=80, + token_min_n=60, + merge_len=20, + comma_split=False, + ) + ) + texts = [i for i in texts if not is_only_punctuation(i)] + return texts if split is True else text + + def _extract_text_token(self, text): + if isinstance(text, Generator): + print("get tts_text generator, will return _extract_text_token_generator!") + # NOTE add a dummy text_token_len for compatibility + return self._extract_text_token_generator(text), torch.tensor( + [0], dtype=torch.int32 + ).to(self.device) + else: + text_token = self.tokenizer.encode( + text, allowed_special=self.allowed_special + ) + text_token = torch.tensor([text_token], dtype=torch.int32).to(self.device) + text_token_len = torch.tensor([text_token.shape[1]], dtype=torch.int32).to( + self.device + ) + return text_token, text_token_len + + def _extract_text_token_generator(self, text_generator): + for text in text_generator: + text_token, _ = self._extract_text_token(text) + for i in range(text_token.shape[1]): + yield text_token[:, i : i + 1] + + def _extract_speech_token(self, prompt_wav): + speech = load_wav(prompt_wav, 16000) + assert ( + speech.shape[1] / 16000 <= 30 + ), "do not support extract speech token for audio longer than 30s" + feat = whisper.log_mel_spectrogram(speech, n_mels=128) + speech_token = ( + self.speech_tokenizer_session.run( + None, + { + self.speech_tokenizer_session.get_inputs()[0] + .name: feat.detach() + .cpu() + .numpy(), + self.speech_tokenizer_session.get_inputs()[1].name: np.array( + [feat.shape[2]], dtype=np.int32 + ), + }, + )[0] + .flatten() + .tolist() + ) + speech_token = torch.tensor([speech_token], dtype=torch.int32).to(self.device) + speech_token_len = torch.tensor([speech_token.shape[1]], dtype=torch.int32).to( + self.device + ) + return speech_token, speech_token_len + + def _extract_spk_embedding(self, prompt_wav): + speech = load_wav(prompt_wav, 16000) + feat = torchaudio.compliance.kaldi.fbank( + speech, num_mel_bins=80, dither=0, sample_frequency=16000 + ) + feat = feat - feat.mean(dim=0, keepdim=True) + embedding = ( + self.campplus_session.run( + None, + { + self.campplus_session.get_inputs()[0] + .name: feat.unsqueeze(dim=0) + .cpu() + .numpy() + }, + )[0] + .flatten() + .tolist() + ) + embedding = torch.tensor([embedding]).to(self.device) + return embedding + + def _extract_speech_feat(self, prompt_wav): + speech = load_wav(prompt_wav, sample_rate) + speech_feat = ( + self.feat_extractor(speech).squeeze(dim=0).transpose(0, 1).to(self.device) + ) + speech_feat = speech_feat.unsqueeze(dim=0) + speech_feat_len = torch.tensor([speech_feat.shape[1]], dtype=torch.int32).to( + self.device + ) + return speech_feat, speech_feat_len + + def frontend_zero_shot( + self, tts_text, prompt_text, prompt_wav, resample_rate, zero_shot_spk_id + ): + tts_text_token, tts_text_token_len = self._extract_text_token(tts_text) + if zero_shot_spk_id == "": + prompt_text_token, prompt_text_token_len = self._extract_text_token( + prompt_text + ) + speech_feat, speech_feat_len = self._extract_speech_feat(prompt_wav) + speech_token, speech_token_len = self._extract_speech_token(prompt_wav) + if resample_rate == 24000: + # cosyvoice2, force speech_feat % speech_token = 2 + token_len = min(int(speech_feat.shape[1] / 2), speech_token.shape[1]) + speech_feat, speech_feat_len[:] = ( + speech_feat[:, : 2 * token_len], + 2 * token_len, + ) + speech_token, speech_token_len[:] = ( + speech_token[:, :token_len], + token_len, + ) + embedding = self._extract_spk_embedding(prompt_wav) + model_input = { + "prompt_text": prompt_text_token, + "prompt_text_len": prompt_text_token_len, + "llm_prompt_speech_token": speech_token, + "llm_prompt_speech_token_len": speech_token_len, + "flow_prompt_speech_token": speech_token, + "flow_prompt_speech_token_len": speech_token_len, + "prompt_speech_feat": speech_feat, + "prompt_speech_feat_len": speech_feat_len, + "llm_embedding": embedding, + "flow_embedding": embedding, + } + else: + model_input = self.spk2info[zero_shot_spk_id] + model_input["text"] = tts_text_token + model_input["text_len"] = tts_text_token_len + return model_input + + +class CosyVoice3LM(torch.nn.Module): + def __init__( + self, + model_path, + llm_input_size: int, + llm_output_size: int, + speech_token_size: int, + ): + super().__init__() + self.llm_input_size = llm_input_size + self.llm_output_size = llm_output_size + self.speech_token_size = speech_token_size + # 2. build speech token language model related modules + self.sos = speech_token_size + 0 + self.sos_id = torch.tensor([self.sos]) + self.eos_token = speech_token_size + 1 + self.task_id = speech_token_size + 2 + self.task_token = torch.tensor([self.task_id]) + self.fill_token = speech_token_size + 3 + + self.llm = Qwen2Encoder(os.path.join(model_path, "CosyVoice-BlankEN")) + self.llm_decoder = nn.Linear( + llm_output_size, speech_token_size + 200, bias=False + ) + + # 3. [Optional] build speech token related modules + self.speech_embedding = torch.nn.Embedding( + speech_token_size + 200, llm_input_size + ) + + # 4. sampling method + self.sampling = functools.partial( + ras_sampling, top_p=0.8, top_k=25, win_size=10, tau_r=0.1 + ) + + self.stop_token_ids = [speech_token_size + i for i in range(200)] + + def sampling_ids( + self, + weighted_scores: torch.Tensor, + decoded_tokens: List, + sampling: int = 25, + ignore_eos: bool = True, + ): + num_trials, max_trials = 0, 100 + while True: + top_ids = self.sampling(weighted_scores, decoded_tokens, sampling) + if (not ignore_eos) or (top_ids < self.speech_token_size): + break + num_trials += 1 + if num_trials > max_trials: + raise RuntimeError( + "sampling reaches max_trials {} and still get eos when ignore_eos is True, check your input!".format( + max_trials + ) + ) + return top_ids + + @torch.inference_mode() + def inference_one_step( + self, + input_ids: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + past_key_values: Optional[Any] = None, + position_ids: Optional[torch.Tensor] = None, + pre_tokens: Optional[List] = None, + ) -> List[int]: + inputs_embeds = self.speech_embedding.weight[ + input_ids.squeeze(0).tolist() + ].unsqueeze(0) + + out_tokens = [] + if pre_tokens is not None: + out_tokens.extend(pre_tokens) + y_pred, hidden_states = self.llm.forward_one_step( + inputs_embeds, + past_key_values=past_key_values, + position_ids=position_ids, + output_hidden_states=True, + return_hidden_states=True, + ) + logp = self.llm_decoder(y_pred).log_softmax(dim=-1) + return logp, hidden_states + + @torch.inference_mode() + def forward( + self, + text: torch.Tensor, + text_len: torch.Tensor, + prompt_text: torch.Tensor, + prompt_text_len: torch.Tensor, + prompt_speech_token: torch.Tensor, + prompt_speech_token_len: torch.Tensor, + sampling: int = 25, + past_key_values=None, + return_first_token=True, + ) -> List[int]: + device = text.device + text = torch.concat([prompt_text, text], dim=1) + text_len += prompt_text_len + text = self.llm.model.model.embed_tokens(text) + + # concat llm_input + sos_emb = self.speech_embedding.weight[self.sos].reshape(1, 1, -1) + task_id_emb = self.speech_embedding.weight[self.task_id].reshape(1, 1, -1) + if prompt_speech_token_len != 0: + prompt_speech_token_emb = self.speech_embedding(prompt_speech_token) + else: + prompt_speech_token_emb = torch.zeros( + 1, 0, self.llm_input_size, dtype=text.dtype + ).to(device) + lm_input = torch.concat( + [sos_emb, text, task_id_emb, prompt_speech_token_emb], dim=1 + ) + + # prefill + out_tokens = [] + y_pred, hidden_states = self.llm.forward_one_step( + lm_input, + past_key_values=past_key_values, + output_hidden_states=True, + return_hidden_states=True, + ) + logp = self.llm_decoder(y_pred[:, -1]).log_softmax(dim=-1) + + if return_first_token: + probs = torch.nn.functional.softmax(logp.squeeze(dim=0), dim=-1) + input_id = torch.multinomial(probs, 1) + out_tokens.append(input_id.item()) + return out_tokens, hidden_states, lm_input + + return logp + + +class CosyVoice3Model: + def __init__( + self, + llm: torch.nn.Module, + flow: Optional[torch.nn.Module], + hift: Optional[torch.nn.Module], + ): + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + self.llm = llm + self.flow = flow + self.hift = hift + + def load(self, llm_model, flow_model, hift_model): + self.llm.load_state_dict( + torch.load(llm_model, map_location=self.device), strict=True + ) + self.llm.to(self.device).eval() + if self.flow is not None: + self.flow.load_state_dict( + torch.load(flow_model, map_location=self.device), strict=True + ) + self.flow.to(self.device).eval() + if self.hift is not None: + # in case hift_model is a hifigan model + hift_state_dict = { + k.replace("generator.", ""): v + for k, v in torch.load(hift_model, map_location=self.device).items() + } + self.hift.load_state_dict(hift_state_dict, strict=True) + self.hift.to(self.device).eval() + + def token2wav( + self, + token, + prompt_token, + prompt_feat, + embedding, + token_offset, + uuid, + stream=False, + finalize=False, + speed=1.0, + ): + tts_mel, _ = self.flow.inference( + token=token.to(self.device, dtype=torch.int32), + token_len=torch.tensor([token.shape[1]], dtype=torch.int32).to(self.device), + prompt_token=prompt_token.to(self.device), + prompt_token_len=torch.tensor( + [prompt_token.shape[1]], dtype=torch.int32 + ).to(self.device), + prompt_feat=prompt_feat.to(self.device), + prompt_feat_len=torch.tensor([prompt_feat.shape[1]], dtype=torch.int32).to( + self.device + ), + embedding=embedding.to(self.device), + streaming=stream, + finalize=finalize, + ) + tts_mel = tts_mel[:, :, token_offset * self.flow.token_mel_ratio :] + if speed != 1.0: + assert ( + token_offset == 0 and finalize is True + ), "speed change only support non-stream inference mode" + tts_mel = F.interpolate( + tts_mel, size=int(tts_mel.shape[2] / speed), mode="linear" + ) + tts_speech, _ = self.hift.inference(speech_feat=tts_mel, finalize=finalize) + return tts_speech + + +class CosyVoice3: + + def __init__(self, model_dir, generate_audio=False): + self.config = PretrainedConfig.from_pretrained( + os.path.join(model_dir, "CosyVoice-BlankEN") + ) + self.config.model_type = "cosyvoice3" + self.config.txt_tokenizer_path = os.path.join(model_dir, "CosyVoice-BlankEN") + self.dtype = self.config.torch_dtype + + self.model_dir = model_dir + self.frontend = CosyVoiceFrontEnd( + partial( + get_qwen_tokenizer, + token_path=self.config.txt_tokenizer_path, + skip_special_tokens=True, + ), + partial( + mel_spectrogram, + n_fft=1920, + num_mels=80, + sampling_rate=sample_rate, + hop_size=480, + win_size=1920, + fmin=0, + fmax=None, + center=False, + ), + os.path.join(model_dir, "campplus.onnx"), + os.path.join(model_dir, "speech_tokenizer_v3.onnx"), + os.path.join(model_dir, "spk2info.pt"), + allowed_special="all", + ) + self.sample_rate = sample_rate + llm = CosyVoice3LM( + model_dir, + llm_input_size=llm_input_size, + llm_output_size=llm_output_size, + speech_token_size=6561, + ) + + llm_path, flow_path, hift_path = os.path.join(model_dir, "llm.pt"), "", "" + flow, hift = None, None + self.generate_audio = generate_audio + if self.generate_audio: + from cosyvoice.flow.DiT.dit import DiT + from cosyvoice.flow.flow import CausalMaskedDiffWithDiT + from cosyvoice.flow.flow_matching import CausalConditionalCFM + from cosyvoice.hifigan.f0_predictor import CausalConvRNNF0Predictor + from cosyvoice.hifigan.generator import CausalHiFTGenerator + from cosyvoice.transformer.upsample_encoder import PreLookaheadLayer + from omegaconf import DictConfig + + pre_lookahead_layer = PreLookaheadLayer( + in_channels=80, channels=1024, pre_lookahead_len=3 + ) + config_dict = { + "sigma_min": 1e-06, + "solver": "euler", + "t_scheduler": "cosine", + "training_cfg_rate": 0.2, + "inference_cfg_rate": 0.7, + "reg_loss_type": "l1", + } + cfm_params = DictConfig(content=config_dict) + estimator = DiT( + dim=1024, + depth=22, + heads=16, + dim_head=64, + ff_mult=2, + mel_dim=80, + mu_dim=80, + spk_dim=80, + out_channels=80, + static_chunk_size=chunk_size * token_mel_ratio, + num_decoding_left_chunks=num_decoding_left_chunks, + ) + decoder = CausalConditionalCFM( + in_channels=240, + n_spks=1, + spk_emb_dim=80, + cfm_params=cfm_params, + estimator=estimator, + ) + flow = CausalMaskedDiffWithDiT( + input_size=80, + output_size=80, + spk_embed_dim=spk_embed_dim, + output_type="mel", + vocab_size=6561, + input_frame_rate=token_frame_rate, + only_mask_loss=True, + token_mel_ratio=token_mel_ratio, + pre_lookahead_len=3, + pre_lookahead_layer=pre_lookahead_layer, + decoder=decoder, + ) + f0_predictor = CausalConvRNNF0Predictor( + num_class=1, in_channels=80, cond_channels=512 + ) + hift = CausalHiFTGenerator( + in_channels=80, + base_channels=512, + nb_harmonics=8, + sampling_rate=sample_rate, + nsf_alpha=0.1, + nsf_sigma=0.003, + nsf_voiced_threshold=10, + upsample_rates=[8, 5, 3], + upsample_kernel_sizes=[16, 11, 7], + istft_params={"n_fft": 16, "hop_len": 4}, + resblock_kernel_sizes=[3, 7, 11], + resblock_dilation_sizes=[[1, 3, 5], [1, 3, 5], [1, 3, 5]], + source_resblock_kernel_sizes=[7, 7, 11], + source_resblock_dilation_sizes=[[1, 3, 5], [1, 3, 5], [1, 3, 5]], + lrelu_slope=0.1, + audio_limit=0.99, + conv_pre_look_right=4, + f0_predictor=f0_predictor, + ) + + flow_path, hift_path = os.path.join(model_dir, "flow.pt"), os.path.join( + model_dir, "hift.pt" + ) + + self.model = CosyVoice3Model(llm, flow, hift) + self.model.load(llm_path, flow_path, hift_path) diff --git a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py new file mode 100644 index 00000000..93f6e850 --- /dev/null +++ b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_qwen2_kv.py @@ -0,0 +1,1255 @@ +# This file is adapted from the Hugging Face Transformers library: +# Source: https://github.com/huggingface/transformers/blob/v4.51.3/src/transformers/models/qwen2/modeling_qwen2.py # noqa: E501 +# Original Copyright: Copyright 2024 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved. # noqa: E501 +# +# Modifications made for AngelSlim project: +# - Modified KV cache mechanism for preallocated GPU memory optimization +# - Added support for speculative decoding in EAGLE3 target model +# - Customized attention mask handling with tree_mask support for tree-based inference +# - Modified forward pass to support custom cache position handling +# - Other modifications are denoted by the symbol: [MODIFIED] +# flake8: noqa: E501 +from functools import partial +from typing import Callable, Optional, Tuple, Union + +import torch +from torch import nn +from transformers.activations import ACT2FN +from transformers.cache_utils import Cache, SlidingWindowCache, StaticCache +from transformers.generation import GenerationMixin +from transformers.modeling_attn_mask_utils import AttentionMaskConverter +from transformers.modeling_flash_attention_utils import FlashAttentionKwargs +from transformers.modeling_outputs import ( + BaseModelOutputWithPast, + CausalLMOutputWithPast, + QuestionAnsweringModelOutput, + SequenceClassifierOutputWithPast, + TokenClassifierOutput, +) +from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update +from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel +from transformers.models.qwen2.configuration_qwen2 import Qwen2Config +from transformers.processing_utils import Unpack +from transformers.utils import TransformersKwargs # [MODIFIED] +from transformers.utils import ( + add_code_sample_docstrings, + add_start_docstrings, + add_start_docstrings_to_model_forward, + can_return_tuple, + logging, + replace_return_docstrings, +) +from transformers.utils.deprecation import deprecate_kwarg + +logger = logging.get_logger(__name__) + +_CHECKPOINT_FOR_DOC = "meta-qwen2/Qwen2-2-7b-hf" +_CONFIG_FOR_DOC = "Qwen2Config" + + +class Qwen2MLP(nn.Module): + def __init__(self, config): + super().__init__() + self.config = config + self.hidden_size = config.hidden_size + self.intermediate_size = config.intermediate_size + self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False) + self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False) + self.act_fn = ACT2FN[config.hidden_act] + + def forward(self, x): + down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x)) + return down_proj + + +def rotate_half(x): + """Rotates half the hidden dims of the input.""" + x1 = x[..., : x.shape[-1] // 2] + x2 = x[..., x.shape[-1] // 2 :] + return torch.cat((-x2, x1), dim=-1) + + +def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1): + """Applies Rotary Position Embedding to the query and key tensors. + + Args: + q (`torch.Tensor`): The query tensor. + k (`torch.Tensor`): The key tensor. + cos (`torch.Tensor`): The cosine part of the rotary embedding. + sin (`torch.Tensor`): The sine part of the rotary embedding. + position_ids (`torch.Tensor`, *optional*): + Deprecated and unused. + unsqueeze_dim (`int`, *optional*, defaults to 1): + The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and + sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note + that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and + k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes + cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have + the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2. + Returns: + `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding. + """ + cos = cos.unsqueeze(unsqueeze_dim) + sin = sin.unsqueeze(unsqueeze_dim) + q_embed = (q * cos) + (rotate_half(q) * sin) + k_embed = (k * cos) + (rotate_half(k) * sin) + return q_embed, k_embed + + +def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor: + """ + This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch, + num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim) + """ + batch, num_key_value_heads, slen, head_dim = hidden_states.shape + if n_rep == 1: + return hidden_states + hidden_states = hidden_states[:, :, None, :, :].expand( + batch, num_key_value_heads, n_rep, slen, head_dim + ) + return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim) + + +def eager_attention_forward( + module: nn.Module, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + attention_mask: Optional[torch.Tensor], + scaling: float, + dropout: float = 0.0, + **kwargs, +): + key_states = repeat_kv(key, module.num_key_value_groups) + value_states = repeat_kv(value, module.num_key_value_groups) + + attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling + if attention_mask is not None: + causal_mask = attention_mask[:, :, :, : key_states.shape[-2]] + attn_weights = attn_weights + causal_mask + + attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to( + query.dtype + ) + attn_weights = nn.functional.dropout( + attn_weights, p=dropout, training=module.training + ) + attn_output = torch.matmul(attn_weights, value_states) + attn_output = attn_output.transpose(1, 2).contiguous() + + return attn_output, attn_weights + + +class Qwen2Attention(nn.Module): + """Multi-headed attention from 'Attention Is All You Need' paper""" + + def __init__(self, config: Qwen2Config, layer_idx: int): + super().__init__() + self.config = config + self.layer_idx = layer_idx + self.head_dim = getattr( + config, "head_dim", config.hidden_size // config.num_attention_heads + ) + self.num_key_value_groups = ( + config.num_attention_heads // config.num_key_value_heads + ) + self.scaling = self.head_dim**-0.5 + self.attention_dropout = config.attention_dropout + self.is_causal = True + self.q_proj = nn.Linear( + config.hidden_size, config.num_attention_heads * self.head_dim, bias=True + ) + self.k_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=True + ) + self.v_proj = nn.Linear( + config.hidden_size, config.num_key_value_heads * self.head_dim, bias=True + ) + self.o_proj = nn.Linear( + config.num_attention_heads * self.head_dim, config.hidden_size, bias=False + ) + + def forward( + self, + hidden_states: torch.Tensor, + position_embeddings: Tuple[torch.Tensor, torch.Tensor], + attention_mask: Optional[torch.Tensor], + past_key_value: Optional[Cache] = None, + cache_position: Optional[torch.LongTensor] = None, + **kwargs: Unpack[FlashAttentionKwargs], + ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]: + input_shape = hidden_states.shape[:-1] + hidden_shape = (*input_shape, -1, self.head_dim) + + query_states = self.q_proj(hidden_states).view(hidden_shape).transpose(1, 2) + key_states = self.k_proj(hidden_states).view(hidden_shape).transpose(1, 2) + value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) + + cos, sin = position_embeddings + query_states, key_states = apply_rotary_pos_emb( + query_states, key_states, cos, sin + ) + + # [MODIFIED] + if past_key_value is not None: + # sin and cos are specific to RoPE models; cache_position needed for the static cache + past_key, past_value = past_key_value[self.layer_idx] + key_states = past_key.cat(key_states) + value_states = past_value.cat(value_states) + + sliding_window = None + if ( + self.config.use_sliding_window + and getattr(self.config, "sliding_window", None) is not None + and self.layer_idx >= self.config.max_window_layers + ): + sliding_window = self.config.sliding_window + + attention_interface: Callable = eager_attention_forward + if self.config._attn_implementation != "eager": + if self.config._attn_implementation == "sdpa" and kwargs.get( + "output_attentions", False + ): + logger.warning_once( + "`torch.nn.functional.scaled_dot_product_attention` does not support `output_attentions=True`. Falling back to " + 'eager attention. This warning can be removed using the argument `attn_implementation="eager"` when loading the model.' + ) + else: + attention_interface = ALL_ATTENTION_FUNCTIONS[ + self.config._attn_implementation + ] + + attn_output, attn_weights = attention_interface( + self, + query_states, + key_states, + value_states, + attention_mask, + dropout=0.0 if not self.training else self.attention_dropout, + scaling=self.scaling, + sliding_window=sliding_window, # main diff with Llama + **kwargs, + ) + + attn_output = attn_output.reshape(*input_shape, -1).contiguous() + attn_output = self.o_proj(attn_output) + return attn_output, attn_weights + + +class Qwen2RMSNorm(nn.Module): + def __init__(self, hidden_size, eps=1e-6): + """ + Qwen2RMSNorm is equivalent to T5LayerNorm + """ + super().__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.variance_epsilon = eps + + def forward(self, hidden_states): + input_dtype = hidden_states.dtype + hidden_states = hidden_states.to(torch.float32) + variance = hidden_states.pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon) + return self.weight * hidden_states.to(input_dtype) + + def extra_repr(self): + return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}" + + +class Qwen2DecoderLayer(nn.Module): + def __init__(self, config: Qwen2Config, layer_idx: int): + super().__init__() + self.hidden_size = config.hidden_size + self.self_attn = Qwen2Attention(config=config, layer_idx=layer_idx) + self.mlp = Qwen2MLP(config) + self.input_layernorm = Qwen2RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.post_attention_layernorm = Qwen2RMSNorm( + config.hidden_size, eps=config.rms_norm_eps + ) + if config.sliding_window and config._attn_implementation != "flash_attention_2": + logger.warning_once( + f"Sliding Window Attention is enabled but not implemented for `{config._attn_implementation}`; " + "unexpected results may be encountered." + ) + + def forward( + self, + hidden_states: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_value: Optional[Cache] = None, + output_attentions: Optional[bool] = False, + use_cache: Optional[bool] = False, + cache_position: Optional[torch.LongTensor] = None, + position_embeddings: Optional[ + Tuple[torch.Tensor, torch.Tensor] + ] = None, # necessary, but kept here for BC + **kwargs: Unpack[FlashAttentionKwargs], + ) -> Tuple[ + torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]] + ]: + residual = hidden_states + + hidden_states = self.input_layernorm(hidden_states) + + # Self Attention + hidden_states, self_attn_weights = self.self_attn( + hidden_states=hidden_states, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_value=past_key_value, + output_attentions=output_attentions, + use_cache=use_cache, + cache_position=cache_position, + position_embeddings=position_embeddings, + **kwargs, + ) + hidden_states = residual + hidden_states + + # Fully Connected + residual = hidden_states + hidden_states = self.post_attention_layernorm(hidden_states) + hidden_states = self.mlp(hidden_states) + hidden_states = residual + hidden_states + + outputs = (hidden_states,) + if output_attentions: + outputs += (self_attn_weights,) + + return outputs + + +class Qwen2RotaryEmbedding(nn.Module): + def __init__(self, config: Qwen2Config, device=None): + super().__init__() + # BC: "rope_type" was originally "type" + if hasattr(config, "rope_scaling") and config.rope_scaling is not None: + self.rope_type = config.rope_scaling.get( + "rope_type", config.rope_scaling.get("type") + ) + else: + self.rope_type = "default" + self.max_seq_len_cached = config.max_position_embeddings + self.original_max_seq_len = config.max_position_embeddings + + self.config = config + self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type] + + inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device) + self.register_buffer("inv_freq", inv_freq, persistent=False) + self.original_inv_freq = self.inv_freq + + @torch.no_grad() + @dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope) + def forward(self, x, position_ids): + inv_freq_expanded = ( + self.inv_freq[None, :, None] + .float() + .expand(position_ids.shape[0], -1, 1) + .to(x.device) + ) + position_ids_expanded = position_ids[:, None, :].float() + + device_type = ( + x.device.type + if isinstance(x.device.type, str) and x.device.type != "mps" + else "cpu" + ) + with torch.autocast(device_type=device_type, enabled=False): # Force float32 + freqs = ( + inv_freq_expanded.float() @ position_ids_expanded.float() + ).transpose(1, 2) + emb = torch.cat((freqs, freqs), dim=-1) + cos = emb.cos() * self.attention_scaling + sin = emb.sin() * self.attention_scaling + + return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype) + + +QWEN2_START_DOCSTRING = r""" + This model inherits from [`PreTrainedModel`]. Check the superclass documentation for the generic methods the + library implements for all its model (such as downloading or saving, resizing the input embeddings, pruning heads + etc.) + + This model is also a PyTorch [torch.nn.Module](https://pytorch.org/docs/stable/nn.html#torch.nn.Module) subclass. + Use it as a regular PyTorch Module and refer to the PyTorch documentation for all matter related to general usage + and behavior. + + Parameters: + config ([`Qwen2Config`]): + Model configuration class with all the parameters of the model. Initializing with a config file does not + load the weights associated with the model, only the configuration. Check out the + [`~PreTrainedModel.from_pretrained`] method to load the model weights. +""" + + +@add_start_docstrings( + "The bare Qwen2 Model outputting raw hidden-states without any specific head on top.", + QWEN2_START_DOCSTRING, +) +class Qwen2PreTrainedModel(PreTrainedModel): + config_class = Qwen2Config + base_model_prefix = "model" + supports_gradient_checkpointing = True + _no_split_modules = ["Qwen2DecoderLayer"] + _skip_keys_device_placement = ["past_key_values"] + _supports_flash_attn_2 = True + _supports_sdpa = True + _supports_flex_attn = True + _supports_cache_class = True + _supports_quantized_cache = True + _supports_static_cache = True + _supports_attention_backend = True + + def _init_weights(self, module): + std = self.config.initializer_range + if isinstance(module, nn.Linear): + module.weight.data.normal_(mean=0.0, std=std) + if module.bias is not None: + module.bias.data.zero_() + elif isinstance(module, nn.Embedding): + module.weight.data.normal_(mean=0.0, std=std) + if module.padding_idx is not None: + module.weight.data[module.padding_idx].zero_() + + +QWEN2_INPUTS_DOCSTRING = r""" + Args: + input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`): + Indices of input sequence tokens in the vocabulary. Padding will be ignored by default should you provide + it. + + Indices can be obtained using [`AutoTokenizer`]. See [`PreTrainedTokenizer.encode`] and + [`PreTrainedTokenizer.__call__`] for details. + + [What are input IDs?](../glossary#input-ids) + attention_mask (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*): + Mask to avoid performing attention on padding token indices. Mask values selected in `[0, 1]`: + + - 1 for tokens that are **not masked**, + - 0 for tokens that are **masked**. + + [What are attention masks?](../glossary#attention-mask) + + Indices can be obtained using [`AutoTokenizer`]. See [`PreTrainedTokenizer.encode`] and + [`PreTrainedTokenizer.__call__`] for details. + + If `past_key_values` is used, optionally only the last `input_ids` have to be input (see + `past_key_values`). + + If you want to change padding behavior, you should read [`modeling_opt._prepare_decoder_attention_mask`] + and modify to your needs. See diagram 1 in [the paper](https://arxiv.org/abs/1910.13461) for more + information on the default strategy. + + - 1 indicates the head is **not masked**, + - 0 indicates the head is **masked**. + position_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Indices of positions of each input sequence tokens in the position embeddings. Selected in the range `[0, + config.n_positions - 1]`. + + [What are position IDs?](../glossary#position-ids) + past_key_values (`Cache`, *optional*): + Pre-computed hidden-states (key and values in the self-attention blocks and in the cross-attention + blocks) that can be used to speed up sequential decoding. This typically consists in the `past_key_values` + returned by the model at a previous stage of decoding, when `use_cache=True` or `config.use_cache=True`. + + It is a [`~cache_utils.Cache`] instance. For more details, see our [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache). + + If `past_key_values` are used, the user can optionally input only the last `input_ids` (those that don't + have their past key value states given to this model) of shape `(batch_size, 1)` instead of all `input_ids` + of shape `(batch_size, sequence_length)`. + inputs_embeds (`torch.FloatTensor` of shape `(batch_size, sequence_length, hidden_size)`, *optional*): + Optionally, instead of passing `input_ids` you can choose to directly pass an embedded representation. This + is useful if you want more control over how to convert `input_ids` indices into associated vectors than the + model's internal embedding lookup matrix. + use_cache (`bool`, *optional*): + If set to `True`, `past_key_values` key value states are returned and can be used to speed up decoding (see + `past_key_values`). + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under returned + tensors for more detail. + output_hidden_states (`bool`, *optional*): + Whether or not to return the hidden states of all layers. See `hidden_states` under returned tensors for + more detail. + return_dict (`bool`, *optional*): + Whether or not to return a [`~utils.ModelOutput`] instead of a plain tuple. + cache_position (`torch.LongTensor` of shape `(sequence_length)`, *optional*): + Indices depicting the position of the input sequence tokens in the sequence. Contrarily to `position_ids`, + this tensor is not affected by padding. It is used to update the cache in the correct position and to infer + the complete sequence length. +""" + + +@add_start_docstrings( + "The bare Qwen2 Model outputting raw hidden-states without any specific head on top.", + QWEN2_START_DOCSTRING, +) +class Qwen2Model(Qwen2PreTrainedModel): + """ + Transformer decoder consisting of *config.num_hidden_layers* layers. Each layer is a [`Qwen2DecoderLayer`] + + Args: + config: Qwen2Config + """ + + def __init__(self, config: Qwen2Config): + super().__init__(config) + self.padding_idx = config.pad_token_id + self.vocab_size = config.vocab_size + + self.embed_tokens = nn.Embedding( + config.vocab_size, config.hidden_size, self.padding_idx + ) + self.layers = nn.ModuleList( + [ + Qwen2DecoderLayer(config, layer_idx) + for layer_idx in range(config.num_hidden_layers) + ] + ) + self.norm = Qwen2RMSNorm(config.hidden_size, eps=config.rms_norm_eps) + self.rotary_emb = Qwen2RotaryEmbedding(config=config) + self.gradient_checkpointing = False + + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self): + return self.embed_tokens + + def set_input_embeddings(self, value): + self.embed_tokens = value + + @can_return_tuple + @add_start_docstrings_to_model_forward(QWEN2_INPUTS_DOCSTRING) + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + **flash_attn_kwargs: Unpack[FlashAttentionKwargs], + ) -> BaseModelOutputWithPast: + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + use_cache = use_cache if use_cache is not None else self.config.use_cache + + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError( + "You must specify exactly one of input_ids or inputs_embeds" + ) + + if self.gradient_checkpointing and self.training and use_cache: + logger.warning_once( + "`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`." + ) + use_cache = False + + if inputs_embeds is None: + inputs_embeds = self.embed_tokens(input_ids) + + # [MODIFIED] + if cache_position is None: + past_seen_tokens = ( + past_key_values[0][0].current_length.item() + if past_key_values is not None + else 0 + ) + cache_position = torch.arange( + past_seen_tokens, + past_seen_tokens + inputs_embeds.shape[1], + device=inputs_embeds.device, + ) + + if position_ids is None: + position_ids = cache_position.unsqueeze(0) + + causal_mask = self._update_causal_mask( + attention_mask, + inputs_embeds, + cache_position, + past_key_values, + output_attentions, + ) + + hidden_states = inputs_embeds + + # create position embeddings to be shared across the decoder layers + position_embeddings = self.rotary_emb(hidden_states, position_ids) + + # decoder layers + all_hidden_states = () if output_hidden_states else None + all_self_attns = () if output_attentions else None + + for idx, decoder_layer in enumerate( + self.layers[: self.config.num_hidden_layers] + ): + if output_hidden_states: + # [MODIFIED] + if ( + idx == len(self.layers) - 3 + or idx == len(self.layers) // 2 + or idx == 2 + ): + all_hidden_states += (hidden_states,) + + if self.gradient_checkpointing and self.training: + layer_outputs = self._gradient_checkpointing_func( + partial(decoder_layer.__call__, **flash_attn_kwargs), + hidden_states, + causal_mask, + position_ids, + past_key_values, + output_attentions, + use_cache, + cache_position, + position_embeddings, + ) + else: + layer_outputs = decoder_layer( + hidden_states, + attention_mask=causal_mask, + position_ids=position_ids, + past_key_value=past_key_values, + output_attentions=output_attentions, + use_cache=use_cache, + cache_position=cache_position, + position_embeddings=position_embeddings, + **flash_attn_kwargs, + ) + + hidden_states = layer_outputs[0] + + if output_attentions: + all_self_attns += (layer_outputs[1],) + + hidden_states = self.norm(hidden_states) + + # add hidden states from the last decoder layer + if output_hidden_states: + all_hidden_states += (hidden_states,) + + return BaseModelOutputWithPast( + last_hidden_state=hidden_states, + past_key_values=past_key_values if use_cache else None, + hidden_states=all_hidden_states, + attentions=all_self_attns, + ) + + def _update_causal_mask( + self, + attention_mask: torch.Tensor, + input_tensor: torch.Tensor, + cache_position: torch.Tensor, + past_key_values: Cache, + output_attentions: bool = False, + ): + if self.config._attn_implementation == "flash_attention_2": + if attention_mask is not None and past_key_values is not None: + is_padding_right = ( + attention_mask[:, -1].sum().item() != input_tensor.size()[0] + ) + if is_padding_right: + raise ValueError( + "You are attempting to perform batched generation with padding_side='right'" + " this may lead to unexpected behaviour for Flash Attention version of Qwen2. Make sure to " + " call `tokenizer.padding_side = 'left'` before tokenizing the input. " + ) + if attention_mask is not None and 0.0 in attention_mask: + return attention_mask + return None + + # For SDPA, when possible, we will rely on its `is_causal` argument instead of its `attn_mask` argument, in + # order to dispatch on Flash Attention 2. This feature is not compatible with static cache, as SDPA will fail + # to infer the attention mask. + # past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0 + past_seen_tokens = ( + past_key_values[0][0].current_length.item() + if past_key_values is not None + else 0 + ) + + using_static_cache = isinstance(past_key_values, StaticCache) + using_sliding_window_cache = isinstance(past_key_values, SlidingWindowCache) + + # When output attentions is True, sdpa implementation's forward method calls the eager implementation's forward + if ( + self.config._attn_implementation == "sdpa" + and not (using_static_cache or using_sliding_window_cache) + and not output_attentions + ): + if AttentionMaskConverter._ignore_causal_mask_sdpa( + attention_mask, + inputs_embeds=input_tensor, + past_key_values_length=past_seen_tokens, + sliding_window=self.config.sliding_window, + is_training=self.training, + ): + return None + + dtype, device = input_tensor.dtype, input_tensor.device + min_dtype = torch.finfo(dtype).min + sequence_length = input_tensor.shape[1] + # SlidingWindowCache or StaticCache + if using_sliding_window_cache or using_static_cache: + target_length = past_key_values.get_max_cache_shape() + # DynamicCache or no cache + else: + target_length = ( + attention_mask.shape[-1] + if isinstance(attention_mask, torch.Tensor) + else past_seen_tokens + sequence_length # [MODIFIED] + ) + + # In case the provided `attention` mask is 2D, we generate a causal mask here (4D). + causal_mask = self._prepare_4d_causal_attention_mask_with_cache_position( + attention_mask, + sequence_length=sequence_length, + target_length=target_length, + dtype=dtype, + device=device, + cache_position=cache_position, + batch_size=input_tensor.shape[0], + config=self.config, + past_key_values=past_key_values, + ) + + if ( + self.config._attn_implementation == "sdpa" + and attention_mask is not None + and attention_mask.device.type in ["cuda", "xpu"] + and not output_attentions + ): + # Attend to all tokens in fully masked rows in the causal_mask, for example the relevant first rows when + # using left padding. This is required by F.scaled_dot_product_attention memory-efficient attention path. + # Details: https://github.com/pytorch/pytorch/issues/110213 + causal_mask = AttentionMaskConverter._unmask_unattended( + causal_mask, min_dtype + ) + + return causal_mask + + # @staticmethod + def _prepare_4d_causal_attention_mask_with_cache_position( + self, + attention_mask: torch.Tensor, + sequence_length: int, + target_length: int, + dtype: torch.dtype, + device: torch.device, + cache_position: torch.Tensor, + batch_size: int, + config: Qwen2Config, + past_key_values: Cache, + ): + """ + Creates a causal 4D mask of shape `(batch_size, 1, query_length, key_value_length)` from a 2D mask of shape + `(batch_size, key_value_length)`, or if the input `attention_mask` is already 4D, do nothing. + + Args: + attention_mask (`torch.Tensor`): + A 2D attention mask of shape `(batch_size, key_value_length)` or a 4D attention mask of shape `(batch_size, 1, query_length, key_value_length)`. + sequence_length (`int`): + The sequence length being processed. + target_length (`int`): + The target length: when generating with static cache, the mask should be as long as the static cache, to account for the 0 padding, the part of the cache that is not filled yet. + dtype (`torch.dtype`): + The dtype to use for the 4D attention mask. + device (`torch.device`): + The device to place the 4D attention mask on. + cache_position (`torch.Tensor`): + Indices depicting the position of the input sequence tokens in the sequence. + batch_size (`torch.Tensor`): + Batch size. + config (`Qwen2Config`): + The model's configuration class + past_key_values (`Cache`): + The cache class that is being used currently to generate + """ + if attention_mask is not None and attention_mask.dim() == 4: + # In this case we assume that the mask comes already in inverted form and requires no inversion or slicing. + causal_mask = attention_mask + else: + min_dtype = torch.finfo(dtype).min + if sequence_length == target_length: + causal_mask = torch.full( + (sequence_length, target_length), + fill_value=min_dtype, + dtype=dtype, + device=device, + ) + diagonal_attend_mask = torch.arange( + target_length, device=device + ) > cache_position.reshape(-1, 1) + if config.sliding_window is not None: + # if we have sliding window, we should not attend to tokens beyond sliding window length, so we mask them out also + # the check is needed to verify is current checkpoint was trained with sliding window or not + if ( + not isinstance(past_key_values, SlidingWindowCache) + or sequence_length > target_length + ): + sliding_attend_mask = torch.arange( + target_length, device=device + ) <= (cache_position.reshape(-1, 1) - config.sliding_window) + diagonal_attend_mask.bitwise_or_(sliding_attend_mask) + causal_mask *= diagonal_attend_mask + causal_mask = causal_mask[None, None, :, :].expand( + batch_size, 1, -1, -1 + ) + if attention_mask is not None: + causal_mask = ( + causal_mask.clone() + ) # copy to contiguous memory for in-place edit + if attention_mask.shape[-1] > target_length: + attention_mask = attention_mask[:, :target_length] + mask_length = attention_mask.shape[-1] + padding_mask = causal_mask[:, :, :, :mask_length] + attention_mask[ + :, None, None, : + ].to(causal_mask.device) + padding_mask = padding_mask == 0 + causal_mask[:, :, :, :mask_length] = causal_mask[ + :, :, :, :mask_length + ].masked_fill(padding_mask, min_dtype) + else: + # [MODIFIED] + causal_mask = torch.zeros( + (sequence_length, target_length), dtype=dtype, device=device + ) + causal_mask = causal_mask[None, None, :, :].expand( + batch_size, 1, -1, -1 + ) + + if hasattr(self, "tree_mask") and self.tree_mask is not None: + tree_mask = self.tree_mask + tree_len = tree_mask.size(-1) + causal_mask[:, :, -tree_len:, -tree_len:][ + tree_mask == 0 + ] = min_dtype + return causal_mask + + +class Qwen2ForCausalLM(Qwen2PreTrainedModel, GenerationMixin): + _tied_weights_keys = ["lm_head.weight"] + _tp_plan = {"lm_head": "colwise_rep"} + _pp_plan = {"lm_head": (["hidden_states"], ["logits"])} + + def __init__(self, config): + super().__init__(config) + self.model = Qwen2Model(config) + self.vocab_size = config.vocab_size + self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) + + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self): + return self.model.embed_tokens + + def set_input_embeddings(self, value): + self.model.embed_tokens = value + + def get_output_embeddings(self): + return self.lm_head + + def set_output_embeddings(self, new_embeddings): + self.lm_head = new_embeddings + + def set_decoder(self, decoder): + self.model = decoder + + def get_decoder(self): + return self.model + + @can_return_tuple + @deprecate_kwarg("num_logits_to_keep", version="4.50", new_name="logits_to_keep") + @add_start_docstrings_to_model_forward(QWEN2_INPUTS_DOCSTRING) + @replace_return_docstrings( + output_type=CausalLMOutputWithPast, config_class=_CONFIG_FOR_DOC + ) + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + cache_position: Optional[torch.LongTensor] = None, + logits_to_keep: Union[int, torch.Tensor] = 0, + **kwargs: Unpack[TransformersKwargs], # [MODIFIED] + ) -> CausalLMOutputWithPast: + r""" + labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*): + Labels for computing the masked language modeling loss. Indices should either be in `[0, ..., + config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored + (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`. + + logits_to_keep (`int` or `torch.Tensor`, *optional*): + If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all + `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that + token can save memory, which becomes pretty significant for long sequences or large vocabulary size. + If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension. + This is useful when using packed tensor format (single dimension for batch and sequence length). + + Returns: + + Example: + + ```python + >>> from transformers import AutoTokenizer, Qwen2ForCausalLM + + >>> model = Qwen2ForCausalLM.from_pretrained("meta-qwen2/Qwen2-2-7b-hf") + >>> tokenizer = AutoTokenizer.from_pretrained("meta-qwen2/Qwen2-2-7b-hf") + + >>> prompt = "Hey, are you conscious? Can you talk to me?" + >>> inputs = tokenizer(prompt, return_tensors="pt") + + >>> # Generate + >>> generate_ids = model.generate(inputs.input_ids, max_length=30) + >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0] + "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you." + ```""" + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + + # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn) + outputs: BaseModelOutputWithPast = self.model( + input_ids=input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + cache_position=cache_position, + **kwargs, + ) + + hidden_states = outputs.last_hidden_state + # Only compute necessary logits, and do not upcast them to float if we are not computing the loss + slice_indices = ( + slice(-logits_to_keep, None) + if isinstance(logits_to_keep, int) + else logits_to_keep + ) + logits = self.lm_head(hidden_states[:, slice_indices, :]) + + loss = None + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + vocab_size=self.config.vocab_size, + **kwargs, + ) + + return CausalLMOutputWithPast( + loss=loss, + logits=logits, + past_key_values=outputs.past_key_values, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +@add_start_docstrings( + """ + The Qwen2 Model transformer with a sequence classification head on top (linear layer). + + [`Qwen2ForSequenceClassification`] uses the last token in order to do the classification, as other causal models + (e.g. GPT-2) do. + + Since it does classification on the last token, it requires to know the position of the last token. If a + `pad_token_id` is defined in the configuration, it finds the last token that is not a padding token in each row. If + no `pad_token_id` is defined, it simply takes the last value in each row of the batch. Since it cannot guess the + padding tokens when `inputs_embeds` are passed instead of `input_ids`, it does the same (take the last value in + each row of the batch). + """, + QWEN2_START_DOCSTRING, +) +class Qwen2ForSequenceClassification(Qwen2PreTrainedModel): + def __init__(self, config): + super().__init__(config) + self.num_labels = config.num_labels + self.model = Qwen2Model(config) + self.score = nn.Linear(config.hidden_size, self.num_labels, bias=False) + + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self): + return self.model.embed_tokens + + def set_input_embeddings(self, value): + self.model.embed_tokens = value + + @can_return_tuple + @add_start_docstrings_to_model_forward(QWEN2_INPUTS_DOCSTRING) + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + ) -> SequenceClassifierOutputWithPast: + r""" + labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): + Labels for computing the sequence classification/regression loss. Indices should be in `[0, ..., + config.num_labels - 1]`. If `config.num_labels == 1` a regression loss is computed (Mean-Square loss), If + `config.num_labels > 1` a classification loss is computed (Cross-Entropy). + """ + + transformer_outputs: BaseModelOutputWithPast = self.model( + input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + ) + hidden_states = transformer_outputs.last_hidden_state + logits = self.score(hidden_states) + + if input_ids is not None: + batch_size = input_ids.shape[0] + else: + batch_size = inputs_embeds.shape[0] + + if self.config.pad_token_id is None and batch_size != 1: + raise ValueError( + "Cannot handle batch sizes > 1 if no padding token is defined." + ) + if self.config.pad_token_id is None: + last_non_pad_token = -1 + elif input_ids is not None: + # To handle both left- and right- padding, we take the rightmost token that is not equal to pad_token_id + non_pad_mask = (input_ids != self.config.pad_token_id).to( + logits.device, torch.int32 + ) + token_indices = torch.arange( + input_ids.shape[-1], device=logits.device, dtype=torch.int32 + ) + last_non_pad_token = (token_indices * non_pad_mask).argmax(-1) + else: + last_non_pad_token = -1 + logger.warning_once( + f"{self.__class__.__name__} will not detect padding tokens in `inputs_embeds`. Results may be " + "unexpected if using padding tokens in conjunction with `inputs_embeds.`" + ) + + pooled_logits = logits[ + torch.arange(batch_size, device=logits.device), last_non_pad_token + ] + + loss = None + if labels is not None: + loss = self.loss_function( + logits=logits, + labels=labels, + pooled_logits=pooled_logits, + config=self.config, + ) + + return SequenceClassifierOutputWithPast( + loss=loss, + logits=pooled_logits, + past_key_values=transformer_outputs.past_key_values, + hidden_states=transformer_outputs.hidden_states, + attentions=transformer_outputs.attentions, + ) + + +@add_start_docstrings( + """ + The Qwen2 Model transformer with a token classification head on top (a linear layer on top of the hidden-states + output) e.g. for Named-Entity-Recognition (NER) tasks. + """, + QWEN2_START_DOCSTRING, +) +class Qwen2ForTokenClassification(Qwen2PreTrainedModel): + def __init__(self, config): + super().__init__(config) + self.num_labels = config.num_labels + self.model = Qwen2Model(config) + if getattr(config, "classifier_dropout", None) is not None: + classifier_dropout = config.classifier_dropout + elif getattr(config, "hidden_dropout", None) is not None: + classifier_dropout = config.hidden_dropout + else: + classifier_dropout = 0.1 + self.dropout = nn.Dropout(classifier_dropout) + self.score = nn.Linear(config.hidden_size, config.num_labels) + + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self): + return self.model.embed_tokens + + def set_input_embeddings(self, value): + self.model.embed_tokens = value + + @can_return_tuple + @add_start_docstrings_to_model_forward(QWEN2_INPUTS_DOCSTRING) + @add_code_sample_docstrings( + checkpoint=_CHECKPOINT_FOR_DOC, + output_type=TokenClassifierOutput, + config_class=_CONFIG_FOR_DOC, + ) + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + ) -> TokenClassifierOutput: + r""" + labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): + Labels for computing the sequence classification/regression loss. Indices should be in `[0, ..., + config.num_labels - 1]`. If `config.num_labels == 1` a regression loss is computed (Mean-Square loss), If + `config.num_labels > 1` a classification loss is computed (Cross-Entropy). + """ + + outputs: BaseModelOutputWithPast = self.model( + input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + ) + sequence_output = outputs.last_hidden_state + sequence_output = self.dropout(sequence_output) + logits = self.score(sequence_output) + + loss = None + if labels is not None: + loss = self.loss_function(logits, labels, self.config) + + return TokenClassifierOutput( + loss=loss, + logits=logits, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +@add_start_docstrings( + """ +The Qwen2 Model transformer with a span classification head on top for extractive question-answering tasks like +SQuAD (a linear layer on top of the hidden-states output to compute `span start logits` and `span end logits`). + """, + QWEN2_START_DOCSTRING, +) +class Qwen2ForQuestionAnswering(Qwen2PreTrainedModel): + base_model_prefix = "transformer" + + def __init__(self, config): + super().__init__(config) + self.transformer = Qwen2Model(config) + self.qa_outputs = nn.Linear(config.hidden_size, 2) + + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self): + return self.transformer.embed_tokens + + def set_input_embeddings(self, value): + self.transformer.embed_tokens = value + + @can_return_tuple + @add_start_docstrings_to_model_forward(QWEN2_INPUTS_DOCSTRING) + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + position_ids: Optional[torch.LongTensor] = None, + past_key_values: Optional[Cache] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + start_positions: Optional[torch.LongTensor] = None, + end_positions: Optional[torch.LongTensor] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + **kwargs, + ) -> QuestionAnsweringModelOutput: + r""" + start_positions (`torch.LongTensor` of shape `(batch_size,)`, *optional*): + Labels for position (index) of the start of the labelled span for computing the token classification loss. + Positions are clamped to the length of the sequence (`sequence_length`). Position outside of the sequence + are not taken into account for computing the loss. + end_positions (`torch.LongTensor` of shape `(batch_size,)`, *optional*): + Labels for position (index) of the end of the labelled span for computing the token classification loss. + Positions are clamped to the length of the sequence (`sequence_length`). Position outside of the sequence + are not taken into account for computing the loss. + """ + + outputs: BaseModelOutputWithPast = self.transformer( + input_ids, + attention_mask=attention_mask, + position_ids=position_ids, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + ) + + sequence_output = outputs.last_hidden_state + + logits = self.qa_outputs(sequence_output) + start_logits, end_logits = logits.split(1, dim=-1) + start_logits = start_logits.squeeze(-1).contiguous() + end_logits = end_logits.squeeze(-1).contiguous() + + loss = None + if start_positions is not None and end_positions is not None: + loss = self.loss_function( + start_logits, end_logits, start_positions, end_positions, **kwargs + ) + + return QuestionAnsweringModelOutput( + loss=loss, + start_logits=start_logits, + end_logits=end_logits, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) diff --git a/angelslim/compressor/speculative/train/configs/cosyvoice3-llm-eagle3.json b/angelslim/compressor/speculative/train/configs/cosyvoice3-llm-eagle3.json new file mode 100644 index 00000000..e0cba072 --- /dev/null +++ b/angelslim/compressor/speculative/train/configs/cosyvoice3-llm-eagle3.json @@ -0,0 +1,28 @@ +{ + "architectures": [ + "CosyVoice3Eagle3LlamaForCausalLM" + ], + "attention_dropout": 0.0, + "bos_token_id": 151643, + "eos_token_id": 151645, + "hidden_act": "silu", + "hidden_size": 896, + "initializer_range": 0.02, + "intermediate_size": 4864, + "max_position_embeddings": 32768, + "max_window_layers": 24, + "model_type": "llama", + "num_attention_heads": 14, + "num_hidden_layers": 24, + "num_key_value_heads": 2, + "rms_norm_eps": 1e-06, + "rope_theta": 1000000.0, + "sliding_window": 32768, + "tie_word_embeddings": true, + "torch_dtype": "bfloat16", + "transformers_version": "4.40.1", + "use_cache": true, + "use_sliding_window": false, + "vocab_size": 6761, + "draft_vocab_size": 6761 +} \ No newline at end of file diff --git a/angelslim/compressor/speculative/train/data/data_utils.py b/angelslim/compressor/speculative/train/data/data_utils.py index 591a309d..41514a36 100644 --- a/angelslim/compressor/speculative/train/data/data_utils.py +++ b/angelslim/compressor/speculative/train/data/data_utils.py @@ -22,6 +22,9 @@ "convert_ultrachat_data", "DataCollatorWithPadding", "VLMDataCollatorWithPadding", + "VLMHunyuanDataCollatorWithPadding", + "AudioDataCollatorWithPadding", + "CosyVoice3DataCollatorWithPadding", ] @@ -409,3 +412,71 @@ def __call__(self, features: List[Dict[str, Any]]) -> Dict[str, Any]: [(item["input_features"]) for item in features] ) return batch + + +class CosyVoice3DataCollatorWithPadding: + + def __call__(self, features: List[Dict[str, Any]]) -> Dict[str, Any]: + max_length = max(item["text"].shape[-1] for item in features) + batch_text_tokens = torch.cat( + [ + paddingtensor2D(item["text"].unsqueeze(0), max_length) + for item in features + ] + ) + max_length = max(item["speech_token"].shape[-1] for item in features) + batch_speech_tokens = torch.cat( + [ + paddingtensor2D(item["speech_token"].unsqueeze(0), max_length) + for item in features + ] + ) + max_length = max(item["prompt_text"].shape[-1] for item in features) + batch_prompt_text = torch.cat( + [ + paddingtensor2D(item["prompt_text"].unsqueeze(0), max_length) + for item in features + ] + ) + max_length = max(item["prompt_speech_token"].shape[-1] for item in features) + batch_prompt_speech_tokens = torch.cat( + [ + paddingtensor2D(item["prompt_speech_token"].unsqueeze(0), max_length) + for item in features + ] + ) + batch_text_token_lens = torch.stack([item["text_len"] for item in features]) + batch_speech_token_lens = torch.stack( + [item["speech_token_len"] for item in features] + ) + batch_prompt_text_lens = torch.stack( + [item["prompt_text_len"] for item in features] + ) + batch_prompt_speech_token_lens = torch.stack( + [item["prompt_speech_token_len"] for item in features] + ) + + batch = { + "text": batch_text_tokens, + "text_len": batch_text_token_lens, + "speech_token": batch_speech_tokens, + "speech_token_len": batch_speech_token_lens, + "prompt_speech_token": batch_prompt_speech_tokens, + "prompt_speech_token_len": batch_prompt_speech_token_lens, + "prompt_text": batch_prompt_text, + "prompt_text_len": batch_prompt_text_lens, + "hidden_states": None, + "target_hiddens": None, + } + + # Check if both hidden_states and target_hiddens exist in all features + if all( + "hidden_states" in item and "target_hiddens" in item for item in features + ): + batch["hidden_states"] = torch.cat( + [paddingtensor(item["hidden_states"], max_length) for item in features] + ) + batch["target_hiddens"] = torch.cat( + [paddingtensor(item["target_hiddens"], max_length) for item in features] + ) + return batch diff --git a/angelslim/compressor/speculative/train/data/dataset.py b/angelslim/compressor/speculative/train/data/dataset.py index cd775fd6..12eeabac 100644 --- a/angelslim/compressor/speculative/train/data/dataset.py +++ b/angelslim/compressor/speculative/train/data/dataset.py @@ -84,6 +84,8 @@ def __init__( shuffle_seed=data_args.shuffle_seed, chat_template_type=chat_template_type, display=display, + target_model_name_or_path=data_args.target_model_name_or_path, + output_dir=data_args.output_dir, ) if data_args.training_mode == "offline": self.offline_dataset_builder = DatasetBuilderFactory.create( diff --git a/angelslim/compressor/speculative/train/data/dataset_builder/__init__.py b/angelslim/compressor/speculative/train/data/dataset_builder/__init__.py index 8b0501aa..3073d9ae 100644 --- a/angelslim/compressor/speculative/train/data/dataset_builder/__init__.py +++ b/angelslim/compressor/speculative/train/data/dataset_builder/__init__.py @@ -21,6 +21,7 @@ from .online_dataset_builder import ( OnlineAudioDatasetBuilder, OnlineLLMDatasetBuilder, + OnlineTTSDatasetBuilder, OnlineVLMDatasetBuilder, OnlineVLMHunyuanVLDatasetBuilder, ) @@ -28,6 +29,7 @@ __all__ = [ "OnlineLLMDatasetBuilder", "OnlineVLMDatasetBuilder", + "OnlineTTSDatasetBuilder", "OnlineVLMHunyuanVLDatasetBuilder", "OfflineLLMDatasetBuilder", "OfflineVLMDatasetBuilder", diff --git a/angelslim/compressor/speculative/train/data/dataset_builder/online_dataset_builder.py b/angelslim/compressor/speculative/train/data/dataset_builder/online_dataset_builder.py index 3a160161..3ac1a77c 100644 --- a/angelslim/compressor/speculative/train/data/dataset_builder/online_dataset_builder.py +++ b/angelslim/compressor/speculative/train/data/dataset_builder/online_dataset_builder.py @@ -12,21 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json +import os +from functools import partial +from pathlib import Path from typing import Any, Dict, List, Optional, Union +import numpy as np import requests import torch from datasets import Features, Value, load_dataset +from huggingface_hub import snapshot_download from PIL import Image from torch.utils.data import Dataset +from tqdm import tqdm from transformers import AutoProcessor, AutoTokenizer from transformers.pipelines.audio_utils import ffmpeg_read from angelslim.utils import rank0_print +from ......utils.lazy_imports import onnxruntime, torchaudio, whisper +from ......utils.utils import decide_device_for_distributed +from ....inference.models.eagle3.target.modeling_cosyvoice3_kv import mel_spectrogram from ..chat_templates import ChatTemplateType from ..data_utils import ( AudioDataCollatorWithPadding, + CosyVoice3DataCollatorWithPadding, DataCollatorWithPadding, VLMDataCollatorWithPadding, VLMHunyuanDataCollatorWithPadding, @@ -810,3 +821,373 @@ def _process_single_conversation( except Exception as e: rank0_print(f"Error processing conversation: {e}") return None + + +@DatasetBuilderFactory.register("online", "TTS") +class OnlineTTSDatasetBuilder(OnlineDatasetBuilder): + def __init__( + self, + tokenizer: Union[AutoTokenizer, AutoProcessor], + max_length: int = 2048, + shuffle_seed: int = 42, + chat_template_type: ChatTemplateType = ChatTemplateType.QWEN3, + display: bool = False, + **kwargs: Any, + ): + super().__init__( + tokenizer, + max_length, + shuffle_seed, + chat_template_type, + display, + ) + self.world_size = int(os.getenv("WORLD_SIZE", 1)) + self.global_rank = int(os.getenv("RANK", -1)) + self.output_dir = kwargs["output_dir"] + self.device = decide_device_for_distributed() + + self.model_path = kwargs["target_model_name_or_path"] + if not os.path.exists(self.model_path): + self.model_path = snapshot_download(self.model_path) + + if os.path.exists(os.path.join(self.model_path, "cosyvoice3.yaml")): + self.model_name = "cosyvoice3" + onnx_path = os.path.join(self.model_path, "speech_tokenizer_v3.onnx") + self._init_audio_tokenizer_cosyvoice3(onnx_path) + self.feat_extractor = partial( + mel_spectrogram, + n_fft=1920, + num_mels=80, + sampling_rate=24000, + hop_size=480, + win_size=1920, + fmin=0, + fmax=None, + center=False, + ) + + def _init_audio_tokenizer_cosyvoice3(self, onnx_path) -> None: + option = onnxruntime.SessionOptions() + option.graph_optimization_level = ( + onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL + ) + option.intra_op_num_threads = 1 + providers = ["CUDAExecutionProvider"] + self.speech_tokenizer_session = onnxruntime.InferenceSession( + onnx_path, sess_options=option, providers=providers + ) + + def get_data_collator(self) -> Any: + if self.model_name == "cosyvoice3": + return CosyVoice3DataCollatorWithPadding() + + def read_jsonl_file(self, file_path: str) -> List[Dict[str, Any]]: + data = [] + try: + for file in file_path: + with open(file, "r", encoding="utf-8") as f: + for line in tqdm( + f, + desc=f"read data file {os.path.basename(file)}", + disable=self.global_rank > 0, + ): + try: + item = json.loads(line.strip()) + if isinstance(item, dict): + data.append(item) + except json.JSONDecodeError as e: + rank0_print( + f"JSON extract error: {e}, line: {line[:100]}..." + ) + continue + except Exception as e: + rank0_print(f"read data file {file_path} failed: {e}") + return data + + def build_dataset( + self, + datapath: str, + num_proc: int = 8, + shuffle: bool = True, + sample_num: Optional[int] = None, + ) -> Dataset: + try: + if not isinstance(datapath, list): + datapath = [datapath] + data_name = "_" + for path in datapath: + data_name += os.path.basename(path)[:-6] + os.makedirs(self.output_dir, exist_ok=True) + cache_path = os.path.join( + self.output_dir, f"processed{data_name}_merged_cache.jsonl" + ) + + if not os.path.exists(cache_path): + raw_data = self.read_jsonl_file(datapath) + chunk_size = len(raw_data) // self.world_size + start_idx = self.global_rank * chunk_size + end_idx = ( + start_idx + chunk_size + if self.global_rank < self.world_size - 1 + else len(raw_data) + ) + rank_data = raw_data[start_idx:end_idx] + processed_data = [] + count = 0 + for item in tqdm( + rank_data, + desc=f"Rank {self.global_rank} process data", + disable=self.global_rank > 0, + ): + if ( + sample_num is not None + and count == sample_num // self.world_size + ): + break + text = item.get("text", "") + audio_tokens = item.get("audio_tokens", None) + audio_path = item.get("audio_path", "") + instruct = item.get("instruct", "") + instruct_audio_path = item.get("instruct_audio_path", "") + + if self.model_name == "cosyvoice3": + processed = self._process_single_item_cosyvoice3( + text, + audio_tokens, + audio_path, + instruct, + instruct_audio_path, + ) + else: + raise NotImplementedError("This model is not implemented") + + processed_data.append(processed) + count += 1 + + # save for each rank + rank_file = os.path.join( + self.output_dir, + f"processed{data_name}_rank_{self.global_rank}.jsonl", + ) + with open(rank_file, "w", encoding="utf-8") as f: + for item in processed_data: + f.write(json.dumps(item, ensure_ascii=True) + "\n") + done_file = os.path.join( + self.output_dir, + f"processed{data_name}_rank_{self.global_rank}.done", + ) + Path(done_file).touch() + self._wait_for_all_ranks_done( + self.output_dir, data_name, self.world_size + ) + + # merge processed data on rank 0 + merge_done_file = os.path.join( + self.output_dir, f"processed{data_name}_merged_cache.done" + ) + if self.global_rank == 0: + all_processed_data = [] + for rank in range(self.world_size): + rank_data = [] + rank_tmp_file = os.path.join( + self.output_dir, f"processed{data_name}_rank_{rank}.jsonl" + ) + with open(rank_tmp_file, "r", encoding="utf-8") as f: + for line in f: + if line.strip(): + rank_data.append(json.loads(line.strip())) + all_processed_data.extend(rank_data) + + with open(cache_path, "w", encoding="utf-8") as f: + for item in all_processed_data: + f.write(json.dumps(item, ensure_ascii=True) + "\n") + + for rank in range(self.world_size): + rank_tmp_file = os.path.join( + self.output_dir, f"processed{data_name}_rank_{rank}.jsonl" + ) + rank_done_file = os.path.join( + self.output_dir, f"processed{data_name}_rank_{rank}.done" + ) + if os.path.exists(rank_tmp_file): + os.remove(rank_tmp_file) + if os.path.exists(rank_done_file): + os.remove(rank_done_file) + + with open(merge_done_file, "w") as f: + f.write("Merged done") + rank0_print("Rank 0: Created merge completion marker") + else: + merge_done = False + while not merge_done: + if os.path.exists(merge_done_file): + merge_done = True + break + + # Load dataset + processed_ds = load_dataset("json", data_files=cache_path) + + # Conditionally shuffle dataset + if shuffle: + processed_ds = processed_ds["train"].shuffle(seed=self.shuffle_seed) + else: + processed_ds = processed_ds["train"] + + # Filter out None results with multiprocessing support + processed_ds = processed_ds.filter( + lambda batch: [ids is not None for ids in batch["speech_token"]], + batched=True, + num_proc=num_proc, + desc="Filtering empty speech_token", + ) + + processed_ds.set_format(type="torch") + return processed_ds + + else: + # Load dataset + rank0_print(f"Loading cache data from {cache_path}") + ds = load_dataset("json", data_files=cache_path) + + # Conditionally shuffle dataset + if shuffle: + ds = ds["train"].shuffle(seed=self.shuffle_seed) + else: + ds = ds["train"] + + # Filter out None results with multiprocessing support + ds = ds.filter( + lambda batch: [ids is not None for ids in batch["speech_token"]], + batched=True, + num_proc=num_proc, + desc="Filtering empty speech_token", + ) + + ds.set_format(type="torch") + return ds + + except Exception as e: + raise RuntimeError(f"Dataset building failed for {datapath}") from e + + def _wait_for_all_ranks_done(self, output_dir, data_name, world_size): + all_done = False + while not all_done: + done_count = 0 + for rank in range(world_size): + done_file = os.path.join( + output_dir, f"processed{data_name}_rank_{rank}.done" + ) + if os.path.exists(done_file): + done_count += 1 + + if done_count == world_size: + all_done = True + break + + def _process_single_item_cosyvoice3( + self, + text: str, + audio_tokens: Optional[list], + audio_path: str, + instruct: Dict[str, Any], + instruct_audio_path: str, + ) -> Optional[Dict[str, Any]]: + text_token = self.tokenizer.encode(text) + instruct_token = self.tokenizer.encode(instruct) + prompt_speech_feat, prompt_speech_feat_len = self._extract_speech_feat( + instruct_audio_path + ) + prompt_speech_token, prompt_speech_token_len = self._extract_speech_token( + instruct_audio_path + ) + + resample_rate = 24000 + if resample_rate == 24000: + token_len = min( + int(prompt_speech_feat.shape[1] / 2), prompt_speech_token.shape[1] + ) + prompt_speech_feat, prompt_speech_feat_len[:] = ( + prompt_speech_feat[:, : 2 * token_len], + 2 * token_len, + ) + prompt_speech_token, prompt_speech_token_len[:] = ( + prompt_speech_token[:, :token_len], + token_len, + ) + + if audio_tokens is not None: + return { + "text": text_token, + "text_len": len(text_token), + "speech_token": audio_tokens, + "speech_token_len": len(audio_tokens), + "prompt_speech_token": prompt_speech_token.squeeze(0).tolist(), + "prompt_speech_token_len": prompt_speech_token_len.item(), + "prompt_text": instruct_token, + "prompt_text_len": len(instruct_token), + } + + speech_token, speech_token_len = self._extract_speech_token(audio_path) + return { + "text": text_token, + "text_len": len(text_token), + "speech_token": speech_token.squeeze(0).tolist(), + "speech_token_len": speech_token_len.item(), + "prompt_speech_token": prompt_speech_token.squeeze(0).tolist(), + "prompt_speech_token_len": prompt_speech_token_len.item(), + "prompt_text": instruct_token, + "prompt_text_len": len(instruct_token), + } + + def _extract_speech_token(self, wav): + speech = self.load_wav(wav, 16000) + assert ( + speech.shape[1] / 16000 <= 30 + ), "do not support extract speech token for audio longer than 30s" + feat = whisper.log_mel_spectrogram(speech, n_mels=128) + speech_token = ( + self.speech_tokenizer_session.run( + None, + { + self.speech_tokenizer_session.get_inputs()[0] + .name: feat.detach() + .cpu() + .numpy(), + self.speech_tokenizer_session.get_inputs()[1].name: np.array( + [feat.shape[2]], dtype=np.int32 + ), + }, + )[0] + .flatten() + .tolist() + ) + speech_token = torch.tensor([speech_token], dtype=torch.int32).to(self.device) + speech_token_len = torch.tensor([speech_token.shape[1]], dtype=torch.int32).to( + self.device + ) + return speech_token, speech_token_len + + def _extract_speech_feat(self, wav): + speech = self.load_wav(wav, 24000) + speech_feat = ( + self.feat_extractor(speech).squeeze(dim=0).transpose(0, 1).to(self.device) + ) + speech_feat = speech_feat.unsqueeze(dim=0) + speech_feat_len = torch.tensor([speech_feat.shape[1]], dtype=torch.int32).to( + self.device + ) + return speech_feat, speech_feat_len + + def load_wav(self, wav, target_sr, min_sr=16000): + speech, sample_rate = torchaudio.load(wav, backend="soundfile") + speech = speech.mean(dim=0, keepdim=True) + if sample_rate != target_sr: + assert ( + sample_rate >= min_sr + ), "wav sample rate {} must be greater than {}".format( + sample_rate, target_sr + ) + speech = torchaudio.transforms.Resample( + orig_freq=sample_rate, new_freq=target_sr + )(speech) + return speech diff --git a/angelslim/compressor/speculative/train/models/draft/__init__.py b/angelslim/compressor/speculative/train/models/draft/__init__.py index d69e8261..1b1eb4b9 100644 --- a/angelslim/compressor/speculative/train/models/draft/__init__.py +++ b/angelslim/compressor/speculative/train/models/draft/__init__.py @@ -13,6 +13,11 @@ # limitations under the License. from .draft_model_factory import DraftModelConfig, create_draft_model -from .llama_eagle3 import Eagle3LlamaForCausalLM +from .llama_eagle3 import CosyVoice3Eagle3LlamaForCausalLM, Eagle3LlamaForCausalLM -__all__ = ["create_draft_model", "DraftModelConfig", "Eagle3LlamaForCausalLM"] +__all__ = [ + "create_draft_model", + "DraftModelConfig", + "Eagle3LlamaForCausalLM", + "CosyVoice3Eagle3LlamaForCausalLM", +] diff --git a/angelslim/compressor/speculative/train/models/draft/llama_eagle3.py b/angelslim/compressor/speculative/train/models/draft/llama_eagle3.py index 971d8db4..03afb6d5 100644 --- a/angelslim/compressor/speculative/train/models/draft/llama_eagle3.py +++ b/angelslim/compressor/speculative/train/models/draft/llama_eagle3.py @@ -13,16 +13,20 @@ # limitations under the License. import math +import os +from collections import Counter from typing import List, Optional, Tuple import torch import torch.nn.functional as F import torch.utils.checkpoint +from huggingface_hub import snapshot_download from torch import nn from transformers import LlamaConfig from transformers.activations import ACT2FN from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS +from ...data.data_utils import process_token_dict_to_mappings from ..model_utils import apply_rotary_pos_emb, apply_rotary_pos_emb_mrope, repeat_kv from .base_model import Eagle3BaseDraftModel from .draft_model_factory import DraftModelFactory @@ -698,3 +702,76 @@ def custom_forward(*inputs): logits = self.lm_head(hidden_states_out) logits = logits.float() return hidden_states, logits + + +@DraftModelFactory.register +class CosyVoice3Eagle3LlamaForCausalLM(Eagle3LlamaForCausalLM): + + def load_embed_weights(self, target_model_name_or_path, embed_weight_key): + """ + Load embedding weights from pretrained model. + + Args: + target_model_name_or_path: Local path or + HuggingFace model identifier (e.g., 'Qwen/Qwen2-7B') + embed_weight_key: Key for the embedding weights in the model file + """ + # Handle HuggingFace model identifier + if not os.path.exists(target_model_name_or_path): + target_model_name_or_path = snapshot_download( + repo_id=target_model_name_or_path + ) + + # Try loading embedding weights + tensor = torch.load("{}/llm.pt".format(target_model_name_or_path)) + speech_embedding_weight = tensor["speech_embedding.weight"] + + with torch.no_grad(): + self.embed_tokens.weight.copy_(speech_embedding_weight) + + def build_vocab_mapping(self, dataset, cache_path): + """ + Build vocab mapping from full vocabulary to draft vocabulary + based on token frequency. + + Args: + dataset: Preprocessed dataset containing 'input_ids' field + cache_path: Path to save/load the token mapping cache + num_processes: Number of processes for parallel processing + """ + if not os.path.exists(cache_path): + # we first count the frequency of effective tokens in the dataset + token_dict = Counter() + print(f"vocab len(dataset)={len(dataset)} type(dataset)={type(dataset)}") + # for item in tqdm(dataset, desc=f"Counting tokens for vocab mapping"): + + for _, item in enumerate(dataset): + input_ids = item["speech_token"] + unique_ids, counts = input_ids.unique(return_counts=True) + batch_token_dict = dict(zip(unique_ids.tolist(), counts.tolist())) + token_dict.update(batch_token_dict) + + # generate the d2t and t2d mapping + d2t, t2d = process_token_dict_to_mappings( + token_dict, + self.draft_vocab_size, + self.vocab_size, + ) + + vocab_mapping = { + "d2t": d2t, + "t2d": t2d, + } + + cache_parent_dir = os.path.dirname(cache_path) + os.makedirs(cache_parent_dir, exist_ok=True) + torch.save(vocab_mapping, cache_path) + print(f"Saved vocab mapping to: {cache_path}") + else: + # Load from cache + cache = torch.load(cache_path) + d2t = cache["d2t"] + t2d = cache["t2d"] + + self.t2d.copy_(t2d) + self.d2t.copy_(d2t) diff --git a/angelslim/compressor/speculative/train/models/target/cosyvoice3_llm.py b/angelslim/compressor/speculative/train/models/target/cosyvoice3_llm.py new file mode 100644 index 00000000..ab7ad341 --- /dev/null +++ b/angelslim/compressor/speculative/train/models/target/cosyvoice3_llm.py @@ -0,0 +1,283 @@ +# Copyright (c) 2024 Alibaba Inc (authors: Xiang Lyu, Zhihao Du) +# 2025 Alibaba Inc (authors: Xiang Lyu, Yabin Li, Qihua, Shengqiang Li) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from https://github.com/FunAudioLLM/CosyVoice for AngelSlim project + +import os +from typing import Optional + +import torch +from torch import nn +from torch.nn.utils.rnn import pad_sequence, unpad_sequence +from transformers import AutoTokenizer + +from ....inference.models.eagle3.target.modeling_qwen2_kv import Qwen2ForCausalLM + +IGNORE_ID = -1 + + +def make_pad_mask(lengths: torch.Tensor, max_len: int = 0) -> torch.Tensor: + """Make mask tensor containing indices of padded part. + + See description of make_non_pad_mask. + + Args: + lengths (torch.Tensor): Batch of lengths (B,). + Returns: + torch.Tensor: Mask tensor containing indices of padded part. + + Examples: + >>> lengths = [5, 3, 2] + >>> make_pad_mask(lengths) + masks = [[0, 0, 0, 0 ,0], + [0, 0, 0, 1, 1], + [0, 0, 1, 1, 1]] + """ + batch_size = lengths.size(0) + max_len = max_len if max_len > 0 else lengths.max().item() + seq_range = torch.arange(0, max_len, dtype=torch.int64, device=lengths.device) + seq_range_expand = seq_range.unsqueeze(0).expand(batch_size, max_len) + seq_length_expand = lengths.unsqueeze(-1) + mask = seq_range_expand >= seq_length_expand + return mask + + +def get_qwen_tokenizer( + token_path: str, skip_special_tokens: bool, version: str = "cosyvoice3" +): + if version == "cosyvoice3": + return CosyVoice3Tokenizer( + token_path=token_path, skip_special_tokens=skip_special_tokens + ) + else: + raise ValueError + + +class Qwen2Encoder(torch.nn.Module): + def __init__(self, pretrain_path): + super().__init__() + self.model = Qwen2ForCausalLM.from_pretrained(pretrain_path) + + def forward( + self, xs: torch.Tensor, xs_lens: torch.Tensor, output_hidden_states: bool + ): + T = xs.size(1) + masks = ~make_pad_mask(xs_lens, T) + outs = self.model( + inputs_embeds=xs, + attention_mask=masks, + output_hidden_states=output_hidden_states, + ) + + return outs, masks.unsqueeze(1) + + +class CosyVoice3Tokenizer: + def __init__(self, token_path, skip_special_tokens=True): + # NOTE: non-chat model, all these special tokens keep randomly initialized. + # fmt: off + # flake8: noqa + special_tokens = { + 'eos_token': '<|endoftext|>', + 'pad_token': '<|endoftext|>', + 'additional_special_tokens': [ + '<|im_start|>', '<|im_end|>', '<|endofprompt|>', + '[breath]', '', '', '[noise]', + '[laughter]', '[cough]', '[clucking]', '[accent]', + '[quick_breath]', + "", "", + "[hissing]", "[sigh]", "[vocalized-noise]", + "[lipsmack]", "[mn]", "<|endofsystem|>", + "[AA]", "[AA0]", "[AA1]", "[AA2]", "[AE]", "[AE0]", "[AE1]", "[AE2]", "[AH]", "[AH0]", "[AH1]", "[AH2]", + "[AO]", "[AO0]", "[AO1]", "[AO2]", "[AW]", "[AW0]", "[AW1]", "[AW2]", "[AY]", "[AY0]", "[AY1]", "[AY2]", + "[B]", "[CH]", "[D]", "[DH]", "[EH]", "[EH0]", "[EH1]", "[EH2]", "[ER]", "[ER0]", "[ER1]", "[ER2]", "[EY]", + "[EY0]", "[EY1]", "[EY2]", "[F]", "[G]", "[HH]", "[IH]", "[IH0]", "[IH1]", "[IH2]", "[IY]", "[IY0]", "[IY1]", + "[IY2]", "[JH]", "[K]", "[L]", "[M]", "[N]", "[NG]", "[OW]", "[OW0]", "[OW1]", "[OW2]", "[OY]", "[OY0]", + "[OY1]", "[OY2]", "[P]", "[R]", "[S]", "[SH]", "[T]", "[TH]", "[UH]", "[UH0]", "[UH1]", "[UH2]", "[UW]", + "[UW0]", "[UW1]", "[UW2]", "[V]", "[W]", "[Y]", "[Z]", "[ZH]", + "[a]", "[ai]", "[an]", "[ang]", "[ao]", "[b]", "[c]", "[ch]", "[d]", "[e]", "[ei]", "[en]", "[eng]", "[f]", + "[g]", "[h]", "[i]", "[ian]", "[in]", "[ing]", "[iu]", "[ià]", "[iàn]", "[iàng]", "[iào]", "[iá]", "[ián]", + "[iáng]", "[iáo]", "[iè]", "[ié]", "[iòng]", "[ióng]", "[iù]", "[iú]", "[iā]", "[iān]", "[iāng]", "[iāo]", + "[iē]", "[iě]", "[iōng]", "[iū]", "[iǎ]", "[iǎn]", "[iǎng]", "[iǎo]", "[iǒng]", "[iǔ]", "[j]", "[k]", "[l]", + "[m]", "[n]", "[o]", "[ong]", "[ou]", "[p]", "[q]", "[r]", "[s]", "[sh]", "[t]", "[u]", "[uang]", "[ue]", + "[un]", "[uo]", "[uà]", "[uài]", "[uàn]", "[uàng]", "[uá]", "[uái]", "[uán]", "[uáng]", "[uè]", "[ué]", "[uì]", + "[uí]", "[uò]", "[uó]", "[uā]", "[uāi]", "[uān]", "[uāng]", "[uē]", "[uě]", "[uī]", "[uō]", "[uǎ]", "[uǎi]", + "[uǎn]", "[uǎng]", "[uǐ]", "[uǒ]", "[vè]", "[w]", "[x]", "[y]", "[z]", "[zh]", "[à]", "[ài]", "[àn]", "[àng]", + "[ào]", "[á]", "[ái]", "[án]", "[áng]", "[áo]", "[è]", "[èi]", "[èn]", "[èng]", "[èr]", "[é]", "[éi]", "[én]", + "[éng]", "[ér]", "[ì]", "[ìn]", "[ìng]", "[í]", "[ín]", "[íng]", "[ò]", "[òng]", "[òu]", "[ó]", "[óng]", "[óu]", + "[ù]", "[ùn]", "[ú]", "[ún]", "[ā]", "[āi]", "[ān]", "[āng]", "[āo]", "[ē]", "[ēi]", "[ēn]", "[ēng]", "[ě]", + "[ěi]", "[ěn]", "[ěng]", "[ěr]", "[ī]", "[īn]", "[īng]", "[ō]", "[ōng]", "[ōu]", "[ū]", "[ūn]", "[ǎ]", "[ǎi]", + "[ǎn]", "[ǎng]", "[ǎo]", "[ǐ]", "[ǐn]", "[ǐng]", "[ǒ]", "[ǒng]", "[ǒu]", "[ǔ]", "[ǔn]", "[ǘ]", "[ǚ]", "[ǜ]" + ] + } + # fmt: on + self.special_tokens = special_tokens + self.tokenizer = AutoTokenizer.from_pretrained(token_path) + self.tokenizer.add_special_tokens(special_tokens) + self.skip_special_tokens = skip_special_tokens + + def encode(self, text, **kwargs): + tokens = self.tokenizer([text], return_tensors="pt") + tokens = tokens["input_ids"][0].cpu().tolist() + return tokens + + def decode(self, tokens): + tokens = torch.tensor(tokens, dtype=torch.int64) + text = self.tokenizer.batch_decode( + [tokens], skip_special_tokens=self.skip_special_tokens + )[0] + return text + + +class CosyVoice3LM(torch.nn.Module): + def __init__( + self, + model_path, + llm_input_size: int, + llm_output_size: int, + speech_token_size: int, + ): + super().__init__() + self.llm_input_size = llm_input_size + self.llm_output_size = llm_output_size + self.speech_token_size = speech_token_size + # build speech token language model related modules + self.sos = speech_token_size + 0 + self.eos_token = speech_token_size + 1 + self.task_id = speech_token_size + 2 + self.fill_token = speech_token_size + 3 + + self.llm = Qwen2Encoder(os.path.join(model_path, "CosyVoice-BlankEN")) + self.llm_decoder = nn.Linear( + llm_output_size, speech_token_size + 200, bias=False + ) + + # [Optional] build speech token related modules + self.speech_embedding = torch.nn.Embedding( + speech_token_size + 200, llm_input_size + ) + self.stop_token_ids = [speech_token_size + i for i in range(200)] + + # tokenizer + self.tokenizer = get_qwen_tokenizer( + os.path.join(model_path, "CosyVoice-BlankEN"), skip_special_tokens=True + ) + + def forward( + self, + text: torch.Tensor, + text_len: torch.Tensor, + speech_token: torch.Tensor, + speech_token_len: torch.Tensor, + prompt_text: torch.Tensor, + prompt_text_len: torch.Tensor, + prompt_speech_token: torch.Tensor, + prompt_speech_token_len: torch.Tensor, + hidden_states: Optional[torch.Tensor], + output_hidden_states: bool = False, + **kwargs, + ): + device = text.device + text_token = torch.concat([prompt_text, text], dim=1) + text_len += prompt_text_len + text_emb = self.llm.model.model.embed_tokens(text_token) + + # concat llm_input + sos_emb = self.speech_embedding.weight[self.sos].reshape(1, 1, -1) + task_id_emb = self.speech_embedding.weight[self.task_id].reshape(1, 1, -1) + if prompt_speech_token_len != 0: + prompt_speech_token_emb = self.speech_embedding(prompt_speech_token) + else: + prompt_speech_token_emb = torch.zeros( + 1, 0, self.llm_input_size, dtype=text_emb.dtype + ).to(device) + speech_token_emb = self.speech_embedding(speech_token) + + # prepare llm_input/target + lm_input, lm_input_len, loss_mask = self.prepare_lm_input_target( + sos_emb, + text_token, + text_emb, + text_len, + task_id_emb, + prompt_speech_token, + prompt_speech_token_emb, + prompt_speech_token_len, + speech_token, + speech_token_emb, + speech_token_len, + ) + + # run lm forward + outputs, lm_output_mask = self.llm( + lm_input, lm_input_len.to(device), output_hidden_states + ) + lm_output = outputs.hidden_states[-1] + logits = self.llm_decoder(lm_output) + hidden_states = torch.cat(outputs.hidden_states[:-1], dim=-1) + return hidden_states, logits, lm_input, loss_mask, lm_output_mask + + def prepare_lm_input_target( + self, + sos_emb, + text_token, + text_emb, + text_len, + task_id_emb, + prompt_speech_token, + prompt_speech_token_emb, + prompt_speech_token_len, + speech_token, + speech_token_emb, + speech_token_len, + ): + lm_target, lm_input = [], [] + text_token = unpad_sequence(text_token, text_len.cpu(), batch_first=True) + text_emb = unpad_sequence(text_emb, text_len.cpu(), batch_first=True) + prompt_speech_token = unpad_sequence( + prompt_speech_token, prompt_speech_token_len.cpu(), batch_first=True + ) + prompt_speech_token_emb = unpad_sequence( + prompt_speech_token_emb, prompt_speech_token_len.cpu(), batch_first=True + ) + speech_token = unpad_sequence( + speech_token, speech_token_len.cpu(), batch_first=True + ) + speech_token_emb = unpad_sequence( + speech_token_emb, speech_token_len.cpu(), batch_first=True + ) + for i in range(len(text_token)): + this_lm_target = torch.tensor( + [IGNORE_ID] * (1 + text_len[i] + prompt_speech_token_len[i]) + + speech_token[i].tolist() + + [self.eos_token] + ) + this_lm_input = torch.concat( + [ + sos_emb.squeeze(dim=0), + text_emb[i], + task_id_emb.squeeze(dim=0), + prompt_speech_token_emb[i], + speech_token_emb[i], + ], + dim=0, + ) + lm_input.append(this_lm_input) + lm_target.append(this_lm_target) + lm_input_len = torch.tensor([i.size(0) for i in lm_input], dtype=torch.int32) + lm_input = pad_sequence(lm_input, batch_first=True, padding_value=IGNORE_ID) + lm_target = pad_sequence(lm_target, batch_first=True, padding_value=IGNORE_ID) + loss_mask = torch.ones_like(lm_target, device=lm_target.device) + loss_mask = loss_mask.masked_fill(lm_target == IGNORE_ID, 0) + return lm_input, lm_input_len, loss_mask diff --git a/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py b/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py index c3087186..23b188ae 100644 --- a/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py +++ b/angelslim/compressor/speculative/train/models/target/target_model_wrapper.py @@ -12,13 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from abc import ABC, abstractmethod from typing import List, Optional, Tuple import torch +from huggingface_hub import snapshot_download from angelslim.utils import decide_device_for_distributed, print_with_rank +from .cosyvoice3_llm import CosyVoice3LM + class BaseBackend(ABC): """ @@ -722,6 +726,76 @@ def hook(module, args, kwargs): } +class TTSTransformersBackend(TransformersBackend): + """ + HuggingFace Transformers backend implementation. + + """ + + def load_model(self) -> None: + # Load and configure model + if not os.path.exists(self.model_path): + self.model_path = snapshot_download(self.model_path) + + # Determine device based on distributed environment + self.device = decide_device_for_distributed() + print_with_rank(f"Loading model to device: {self.device}") + + # Load model + if os.path.exists(os.path.join(self.model_path, "cosyvoice3.yaml")): + self.model_name = "cosyvoice3" + self._load_cosyvoice3() + else: + raise NotImplementedError("This model is not implemented") + + self._freeze_model_parameters() + self.model.eval() + + def _load_cosyvoice3(self) -> None: + """Load text tokenizer using HuggingFace Transformers.""" + + self.model = CosyVoice3LM( + self.model_path, + llm_input_size=896, + llm_output_size=896, + speech_token_size=6561, + ).to(self.device) + self.model.load_state_dict( + torch.load( + os.path.join(self.model_path, "llm.pt"), map_location=self.device + ), + strict=True, + ) + + # Load tokenizer + self.tokenizer = self.model.tokenizer + + def get_hidden_states_and_logits( + self, + input_ids: torch.Tensor, + attention_mask: Optional[torch.Tensor] = None, + **kwargs, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Extract hidden states and logits using Transformers backend. + + Args: + input_ids: Input token IDs + attention_mask: Attention mask + **kwargs: May contain 'aux_hidden_states_layer_ids' to specify custom layers + + Returns: + Tuple of (concatenated_hidden_states, logits) + """ + if self.model_name == "cosyvoice3": + with torch.no_grad(): + outputs = self.model( + **input_ids, + output_hidden_states=True, + ) + return outputs + + class TargetModelWrapper: """ Unified wrapper for target models in Eagle3 training. @@ -749,6 +823,7 @@ class TargetModelWrapper: BACKENDS = { ("hf", "LLM"): TransformersBackend, ("hf", "VLM"): VLMTransformersBackend, + ("hf", "TTS"): TTSTransformersBackend, ("hf", "Audio"): AudioTransformersBackend, } diff --git a/angelslim/compressor/speculative/train/trainer/__init__.py b/angelslim/compressor/speculative/train/trainer/__init__.py index b7012159..ad1c6729 100644 --- a/angelslim/compressor/speculative/train/trainer/__init__.py +++ b/angelslim/compressor/speculative/train/trainer/__init__.py @@ -13,13 +13,18 @@ # limitations under the License. from .offline_eagle3_trainer import OfflineEagle3Trainer, OfflineVLMEagle3Trainer -from .online_eagle3_trainer import OnlineEagle3Trainer, OnlineVLMEagle3Trainer +from .online_eagle3_trainer import ( + OnlineEagle3Trainer, + OnlineTTSEagle3Trainer, + OnlineVLMEagle3Trainer, +) from .trainer_factory import Eagle3TrainerFactory __all__ = [ "Eagle3TrainerFactory", "OnlineEagle3Trainer", "OnlineVLMEagle3Trainer", + "OnlineTTSEagle3Trainer", "OfflineEagle3Trainer", "OfflineVLMEagle3Trainer", ] diff --git a/angelslim/compressor/speculative/train/trainer/online_eagle3_trainer.py b/angelslim/compressor/speculative/train/trainer/online_eagle3_trainer.py index e0a91d6d..11f314fe 100644 --- a/angelslim/compressor/speculative/train/trainer/online_eagle3_trainer.py +++ b/angelslim/compressor/speculative/train/trainer/online_eagle3_trainer.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Dict +from typing import Any, Dict, List, Optional, Tuple +import torch from torch import nn from ...utils import padding @@ -218,3 +219,227 @@ def prepare_data_for_draft_model(self, inputs): } ) return result_dict + + +@Eagle3TrainerFactory.register("online", "TTS") +class OnlineTTSEagle3Trainer(Eagle3Trainer): + """ + Online EAGLE3 Trainer for speculative decoding training. + + Implements training logic for EAGLE3 model using a draft model to predict + tokens based on hidden states from a target model. + """ + + def __init__( + self, + draft_model: nn.Module, + target_model: nn.Module, + length: int, + draft_model_config: Dict[str, Any], + **kwargs, + ): + """ + Initialize the OnlineEagle3Trainer. + Args: + draft_model: Draft model for token prediction + target_model: Target model for generating hidden states + length: Number of speculative decoding steps + draft_model_config: Configuration dictionary for draft model + **kwargs: Additional arguments passed to parent Trainer + """ + super().__init__(draft_model=draft_model, length=length, **kwargs) + self.target_model = target_model + + def prepare_data_for_draft_model(self, inputs): + if self.target_model.backend.model_name == "cosyvoice3": + data_for_draft_model = self._prepare_data_for_draft_model_cosyvoice3(inputs) + else: + raise NotImplementedError("This model is not implemented") + return data_for_draft_model + + def _prepare_data_for_draft_model_cosyvoice3(self, inputs): + ( + hidden_states, + target_logits, + inputs_embeds, + loss_mask, + attention_mask, + ) = self.target_model.get_hidden_states_and_logits(input_ids=inputs) + + device = inputs_embeds.device + dtype = self.draft_model.fc.weight.dtype + + target_logits = padding(target_logits, left=False).to(device) + inputs_embeds = padding(inputs_embeds, left=False) + loss_mask = loss_mask[..., None].to(device) + + return { + "hidden_states": hidden_states.to(dtype), + "target_logits": target_logits, + "inputs_embeds": inputs_embeds.to(dtype), + "loss_mask": loss_mask, + "position_ids": None, + "attention_mask": attention_mask.squeeze(0), + } + + def compute_loss( + self, + model: nn.Module, + inputs: Dict[str, torch.Tensor], + num_items_in_batch: Optional[int] = None, + return_outputs: bool = False, + ) -> Tuple[List[torch.Tensor], List, List[float]]: + """ + Compute the training loss for the model. + + Args: + model: The model for which to compute the loss + inputs: Input data dictionary with input_ids, attention_mask, + loss_mask, position_ids + num_items_in_batch: Number of items in batch (unused) + return_outputs: Whether to return model outputs (unused) + + Returns: + Tuple of (prediction_losses, value_losses, accuracies) for each step + """ + data_for_draft_model = self.prepare_data_for_draft_model(inputs) + + attention_mask = data_for_draft_model["attention_mask"] # Batch x Seq + position_ids = data_for_draft_model["position_ids"] # Batch x Seq + target_logits = data_for_draft_model["target_logits"] # Batch x Seq x Vocab + loss_mask = data_for_draft_model["loss_mask"] # Batch x Seq x 1 + hidden_states = data_for_draft_model["hidden_states"] # Batch x Seq x Hidden + inputs_embeds = data_for_draft_model["inputs_embeds"] + + hidden_states = self.down_project_hidden_states(hidden_states) + attention_mask, position_ids = self.prepare_attention_mask_and_position_ids( + hidden_states, attention_mask, position_ids + ) + loss = self.draft_model_training_time_test( + hidden_states, + attention_mask, + position_ids, + target_logits, + loss_mask, + inputs_embeds, + log_prefix="train", + ) + + return loss + + def prediction_step( + self, + model: nn.Module, + inputs: Dict[str, torch.Tensor], + prediction_loss_only: bool, + ignore_keys: Optional[List[str]] = None, + ) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor], Optional[torch.Tensor]]: + """ + Perform an evaluation step on `model` using `inputs`. + """ + data_for_draft_model = self.prepare_data_for_draft_model(**inputs) + + attention_mask = data_for_draft_model["attention_mask"] # Batch x Seq + position_ids = data_for_draft_model["position_ids"] # Batch x Seq + target_logits = data_for_draft_model["target_logits"] # Batch x Seq x Vocab + loss_mask = data_for_draft_model["loss_mask"] # Batch x Seq x 1 + hidden_states = data_for_draft_model["hidden_states"] # Batch x Seq x Hidden + inputs_embeds = data_for_draft_model["inputs_embeds"] + + with torch.no_grad(): + hidden_states = self.down_project_hidden_states(hidden_states) + attention_mask, position_ids = self.prepare_attention_mask_and_position_ids( + hidden_states, attention_mask, position_ids + ) + loss = self.draft_model_training_time_test( + hidden_states, + attention_mask, + position_ids, + target_logits, + loss_mask, + inputs_embeds, + log_prefix="eval", + ) + return (loss, None, None) + + def draft_model_training_time_test( + self, + hidden_states, + attention_mask, + position_ids, + target_logits, + loss_mask, + inputs_embeds, + log_prefix="", + ): + # Step 6: Initialize containers for losses, accuracies and cache + plosses, acces = [], [] + cache_hidden = [[], []] + + # Step 7: Iterative speculative decoding training loop + for idx in range(self.length): + # Step 7.1: Get input embeddings with gradient tracking + if not inputs_embeds.requires_grad: + inputs_embeds.requires_grad = True + + # Step 7.2: Encode through draft model layers + hidden_states, cache_hidden = self.draft_model.encode_layers( + inputs_embeds=inputs_embeds, + hidden_states=hidden_states, + cache_hidden=cache_hidden, + attention_mask=attention_mask, + position_ids=position_ids, + use_cache=True, + ) + + # Step 7.3: Compute logits from hidden states + logits = self.draft_model.compute_logits(hidden_states) + + # Step 7.4: Compute target distribution and position mask + with torch.no_grad(): + target_max_token = target_logits.argmax(-1) + target_mask = self.draft_model.t2d[target_max_token][..., None].int() + position_mask = target_mask * loss_mask + + target_head = target_logits[..., self.draft_model.t2d].float() + target_p = nn.Softmax(dim=2)(target_head).detach() + + # Step 7.5: Compute loss + out_logp = nn.LogSoftmax(dim=2)(logits) + loss = -torch.sum(position_mask * target_p * out_logp, dim=2).mean() + + # Step 7.6: Compute accuracy + with torch.no_grad(): + correct = ( + logits.argmax(-1) == target_p.argmax(-1) + ) * position_mask.squeeze(-1) + accuracy = correct.sum().item() / (loss_mask.sum().item() + 1e-6) + + # Step 7.7: Store loss and accuracy + plosses.append(loss) + acces.append(accuracy) + + # Step 7.8: Update inputs for next iteration (skip on last step) + if idx < self.length - 1: + inputs_embeds = padding(inputs_embeds, left=False) + target_logits = padding(target_logits, left=False) + loss_mask = padding(loss_mask, left=False) + + # Step 8: Compute weighted loss + ploss_weight = [0.8**i for i in range(len(plosses))] + ploss = sum([ploss_weight[i] * plosses[i] for i in range(len(plosses))]) + + log = { + f"{log_prefix}/acc_{i}": round(float(acces[i]), 3) + for i in range(len(acces)) + } + log.update( + { + f"{log_prefix}/ploss_{i}": round(float(plosses[i].item()), 3) + for i in range(len(plosses)) + } + ) + self.log(log) + + # Step 9: Return loss + return ploss diff --git a/angelslim/compressor/speculative/utils/__init__.py b/angelslim/compressor/speculative/utils/__init__.py index 57952ee6..48bb02cd 100644 --- a/angelslim/compressor/speculative/utils/__init__.py +++ b/angelslim/compressor/speculative/utils/__init__.py @@ -5,20 +5,26 @@ MomentumScorePredictor, evaluate_posterior, initialize_tree, + initialize_tree_cosyvoice3, padding, prepare_logits_processor, reset_tree_mode, tree_decoding, + tree_decoding_cosyvoice3, update_inference_inputs, + update_inference_inputs_cosyvoice3, ) __all__ = [ "prepare_logits_processor", "reset_tree_mode", "initialize_tree", + "initialize_tree_cosyvoice3", "tree_decoding", + "tree_decoding_cosyvoice3", "evaluate_posterior", "update_inference_inputs", + "update_inference_inputs_cosyvoice3", "initialize_past_key_values", "MomentumScorePredictor", "EWMAScorePredictor", diff --git a/angelslim/compressor/speculative/utils/util.py b/angelslim/compressor/speculative/utils/util.py index db13187f..406b8a88 100644 --- a/angelslim/compressor/speculative/utils/util.py +++ b/angelslim/compressor/speculative/utils/util.py @@ -126,6 +126,47 @@ def initialize_tree(input_ids, model, past_key_values, logits_processor): ) +def initialize_tree_cosyvoice3( + text, + prompt_text, + llm_prompt_speech_token, + input_ids, + model, + past_key_values, + logits_processor, +): + first_token, hidden_states, inputs_embeddings = model( + text, prompt_text, llm_prompt_speech_token, past_key_values + ) + + input_ids = torch.cat((input_ids, first_token.to(input_ids.device)), dim=1) + # add embedding + add_inputs_embeds = model.eagle_layer.embed_tokens.weight[ + first_token.squeeze(0).tolist() + ].unsqueeze(0) + new_inputs_embeddings = torch.cat([inputs_embeddings, add_inputs_embeds], dim=1) + + # Clone the output hidden states + eagle_device = next(model.eagle_layer.parameters()).device + if hidden_states[0].device != eagle_device: + hidden_states = [x.to(eagle_device) for x in hidden_states] + hidden_states = torch.cat(hidden_states, dim=-1) + draft_tokens, retrieve_indices, tree_mask, tree_position_ids = ( + model.eagle_layer.topK_genrate( + hidden_states, input_ids, new_inputs_embeddings, logits_processor + ) + ) + return ( + draft_tokens, + retrieve_indices, + tree_mask, + tree_position_ids, + hidden_states, + inputs_embeddings, + first_token, + ) + + def reset_tree_mode( model, ): @@ -163,6 +204,29 @@ def tree_decoding( return logits, hidden_state, outputs +def tree_decoding_cosyvoice3( + model, + tree_candidates, + past_key_values, + tree_position_ids, + input_ids, + retrieve_indices, +): + position_ids = tree_position_ids + input_ids.shape[1] + if position_ids is not None and position_ids.dim() == 1: + position_ids = position_ids.unsqueeze(0) + tree_logits, hidden_state = model.tree_decoding_forward( + input_ids=tree_candidates, + past_key_values=past_key_values, + position_ids=position_ids, + ) + + hidden_state = torch.cat(hidden_state, dim=-1) + + logits = tree_logits[0, retrieve_indices] + return logits, hidden_state + + def evaluate_posterior( logits: torch.Tensor, candidates: torch.Tensor, @@ -326,6 +390,92 @@ def update_inference_inputs( ) +@torch.no_grad() +def update_inference_inputs_cosyvoice3( + input_ids, + inputs_embeddings, + candidates, + best_candidate, + accept_length, + retrieve_indices, + logits_processor, + new_token, + past_key_values_data_list, + current_length_data, + model, + hidden_state_new, + sample_token, +): + assert input_ids.shape[1] == inputs_embeddings.shape[1] + prev_input_len = input_ids.shape[1] + # Map the best candidate indices to the original indices in the sequence + select_indices = ( + retrieve_indices[best_candidate, : accept_length + 1] + prev_input_len + ) + # Append the tokens from the best candidate to the input sequence + input_ids = torch.cat( + [ + input_ids, + candidates[None, best_candidate, : accept_length + 1].to(input_ids.device), + ], + dim=-1, + ) + + # add embedding + add_inputs_embeds = model.eagle_layer.embed_tokens.weight[ + candidates[None, best_candidate, : accept_length + 1].squeeze(0).tolist() + ].unsqueeze(0) + inputs_embeddings = torch.cat([inputs_embeddings, add_inputs_embeds], dim=1) + + # Update the past key values based on the selected tokens + # Source tensor that contains relevant past information based + # on the selected candidate + for past_key_values_data in past_key_values_data_list: + tgt = past_key_values_data[ + ..., select_indices.to(past_key_values_data.device), : + ] + # Destination tensor where the relevant past information will be stored + dst = past_key_values_data[ + ..., prev_input_len : prev_input_len + tgt.shape[-2], : + ] + # Copy relevant past information from the source to the destination + dst.copy_(tgt, non_blocking=True) + + # Update the current length tensor (currently only support batch size is 1) + current_length_data.fill_(prev_input_len + tgt.shape[-2]) + + retrieve_hidden_state_new = hidden_state_new[:, retrieve_indices] + accept_hidden_state_new = retrieve_hidden_state_new[ + :, best_candidate, : accept_length + 1 + ] + + # add embedding + add_inputs_embeds = model.eagle_layer.embed_tokens.weight[ + sample_token.squeeze(0).tolist() + ].unsqueeze(0) + + draft_tokens, retrieve_indices, tree_mask, tree_position_ids = ( + model.eagle_layer.topK_genrate( + accept_hidden_state_new, + input_ids=torch.cat((input_ids, sample_token.to(input_ids.device)), dim=1), + inputs_embeddings=torch.cat([inputs_embeddings, add_inputs_embeds], dim=1), + logits_processor=logits_processor, + ) + ) + + new_token += accept_length + 1 + + return ( + input_ids, + inputs_embeddings, + draft_tokens, + retrieve_indices, + tree_mask, + tree_position_ids, + new_token, + ) + + @torch.no_grad() def padding(tensor, left=True): zeropadding = torch.zeros_like(tensor[:, -1:, ...]) diff --git a/angelslim/engine.py b/angelslim/engine.py index 3ec3e3af..d0582037 100644 --- a/angelslim/engine.py +++ b/angelslim/engine.py @@ -409,6 +409,10 @@ def __init__(self, config=None, deploy_backend: str = "pytorch"): self.BenchmarkConfig = pytorch_benchmark.BenchmarkConfig self.BenchmarkEngine = pytorch_benchmark.BenchmarkEngine self.BenchmarkMode = pytorch_benchmark.BenchmarkMode + elif self.deploy_backend == "pytorch_tts": + self.BenchmarkConfig = pytorch_benchmark.BenchmarkConfig + self.BenchmarkEngine = pytorch_benchmark.TTSBenchmarkEngine + self.BenchmarkMode = pytorch_benchmark.BenchmarkMode elif self.deploy_backend == "vllm": self.BenchmarkConfig = vllm_benchmark.BenchmarkConfig self.BenchmarkEngine = vllm_benchmark.BenchmarkEngine diff --git a/angelslim/utils/lazy_imports.py b/angelslim/utils/lazy_imports.py index 246050e8..f544ca86 100644 --- a/angelslim/utils/lazy_imports.py +++ b/angelslim/utils/lazy_imports.py @@ -208,6 +208,12 @@ def __getattr__(self, name: str) -> Any: # --- multimodal related lazy imports --- qwen_vl_utils = LazyModule("qwen_vl_utils", "multimodal") qwen_omni_utils = LazyModule("qwen_omni_utils", "multimodal") +torchaudio = LazyModule("torchaudio", "multimodal") +whisper = LazyModule("whisper", "multimodal") +onnxruntime = LazyModule("onnxruntime", "multimodal") +inflect = LazyModule("inflect", "multimodal") +librosa = LazyModule("librosa", "multimodal") +wetext = LazyModule("wetext", "multimodal") # --- HunyuanVL related lazy imports --- HunYuanVLForConditionalGeneration = LazyAttribute( diff --git a/dataset/tts_fake_data/question.jsonl b/dataset/tts_fake_data/question.jsonl new file mode 100644 index 00000000..db76edaa --- /dev/null +++ b/dataset/tts_fake_data/question.jsonl @@ -0,0 +1,100 @@ +{"tts_text": "\"He says we're to start to morrow at daybreak.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"And all seems favourable for our attempt to morrow?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"I wish we could charge them boldly, and send them flying over the plains.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"I shouldn't be a bit surprised if we saw them over the way there-just one or two, scouting; and if we do I should be for a stand at arms all night, for it might mean an attack after dark.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Nothing, sir, but wait.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Exactly,\" replied the doctor.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Hurrah!\" cried Chris.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Yes; of course,\" said Chris, with a dubious look all the same.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Finally, just at dusk the animals can be driven in for food and water, and-\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Serve 'em right if they did, sir.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Look here, lads.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Never to come back again,\" said Ned sharply.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "The Indians can wait; we cannot, and they seem to know it.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"They seem to me to be hatching up some dodge or another,\" replied Griggs.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "GRIGGS IS STUBBORN.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"But we shan't, my lad.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Look here; if you say that again we shall quarrel.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Why not?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "CHAPTER FORTY NINE.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "It takes a deal to starve a redskin.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "We must get away from here to some good hunting ground.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Yes, yes, but you know what I mean.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"No, sir.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Not a bit of it, sir.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Oh, I'm only a little stiff still.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Well, I do.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "They'd have weeks of work before they could get their horses out but without horses they'd be out in a week.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Never been away at all, I believe.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"But the enemy won't be standing still,\" continued Griggs.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"But all the same we can be making our preparations.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "We're not waiting for you now.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "I've just been trying that place again.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"I'm not afraid of them hitting me, my lad,\" said Griggs confidently. \"Being shot at by fellows with bows and arrows sounds bad enough, but there's not much risk here.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"You mean the shutting up the enemy here to starve?\" said Bourne.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Canter?", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "How did your pony go this morning?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "It's all settled, gentlemen.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Oh yes, I hear.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Oh, here you are, Griggs,\" cried the doctor.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Would be if we let them get the better of us, sir.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Oh!", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Why?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "I shall be all right.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "We shall get strong more quickly journeying over the plains or climbing in and out among the mountains.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "But there, I don't want to make speeches.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Very well; I can do that,\" said Ned haughtily.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "I don't like to bother my father any more, but what does he say?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Well, they showed themselves to me; I didn't want them,\" said Griggs dryly.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Right; I do, neighbour, and it's very handsome of you to offer me the chance to back out.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Had a good turn at scouting?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Yes, sir.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"It's all nonsense, Ned,\" cried Chris, \"for them to think they are staying on account of us.--Hullo, Griggs!", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "We went at a good swinging gallop.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Going to give up young Chris's plan?\" said Griggs slowly.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "The plan is to get out of this valley ourselves, where we are regularly locked in, and to put the redskins in our place, locking them in.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Don't you?", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Provisions can be packed in our wallets; in fact, everything held ready for a start.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"They're an artful lot.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Did you canter this morning?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "You propose offering yourself for a mark to the Indians' arrows, and-\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Splendid.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Serve you right.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Well, you must talk it over with father,\" said Chris.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Don't you see that we're playing a very ticklish game?", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "I've no doubt about it now.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Griggs nodded his head.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "A thrill of excitement ran through Chris, and his heart began to beat. Then he was listening, so to speak, with all his might.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"That's right,\" cried Griggs.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "The Indians have shifted their quarters, and they're in about as awkward a position as they could contrive for our purpose.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Yes, my lad; but I want them to be planted farther back still.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Because I've seen Indians again.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Nonsense!", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Then what do you propose?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"We've been patient enough.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Don't be so petty, Ned.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Here, I want for us to be off.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "We start to night.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "I'm going to take care they don't hit me.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Were you listening?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"There,\" he said, \"I've made up my mind.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Not allowed to go off again?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "I have felt something of the kind, but I am convinced now that it will not, and that we must chance something and make it.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Too many redskins about, as I told you.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Hear that, Griggs?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "There's a bit I've been looking out quite a quarter of a mile farther off, and I'm going to propose it to the doctor as being safest.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Any time the doctor likes.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Not quite, my lads.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"I don't know about that,\" said Chris anxiously.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Let's see; we're going to have another look at the place this afternoon, aren't we?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"Because it is a very risky thing to do.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"What for?\" said Griggs sharply.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "We've got something else to think about besides teasing and bantering.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "I should be running fast and dodging in and out among the rocks and trees.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Just halted a little on the bad leg; but it's better than it was yesterday.\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"And what about you?\"", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"But what about the arrows?\" said Ned.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "Every one was busy, for the keeping watch regularly took up a good deal of time.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "You're always seeing Indians again.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "The days glided by, with the stiffness in Chris Lee's limbs growing less painful, and the pony recovering fast, for the clear mountain air seemed to act like a cure for wounds.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} +{"tts_text": "\"There, that's enough,\" cried Chris.", "prompt_wav": "./zero_shot_prompt.wav", "prompt_text": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"} \ No newline at end of file diff --git a/dataset/tts_fake_data/train.jsonl b/dataset/tts_fake_data/train.jsonl new file mode 100644 index 00000000..c3ba25a1 --- /dev/null +++ b/dataset/tts_fake_data/train.jsonl @@ -0,0 +1,2 @@ +{"text": "\"But you will get no more money out of me, I promise you.\"", "audio_path": "./asset/0.wav", "instruct": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。", "instruct_audio_path": "./asset/zero_shot_prompt.wav"} +{"text": "His ways might be affected and effeminate and his conversational powers indifferent; but his bandaged wrist was a constant reminder to all the nieces that he possessed courage and ready wit, and it was but natural that he became more interesting to them because just now he was to an extent helpless, and his crippled hand had been acquired in their service.", "audio_path": "./asset/1.wav", "instruct": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。", "instruct_audio_path": "./asset/zero_shot_prompt.wav"} \ No newline at end of file diff --git a/dataset/tts_fake_data/train_regenerate.jsonl b/dataset/tts_fake_data/train_regenerate.jsonl new file mode 100644 index 00000000..26ed1b7b --- /dev/null +++ b/dataset/tts_fake_data/train_regenerate.jsonl @@ -0,0 +1,2 @@ +{"text": "\"But you will get no more money out of me, I promise you.\"", "audio_tokens": [29, 29, 248, 503, 6361, 5100, 1869, 600, 4592, 1424, 1901, 1921, 3943, 1514, 1712, 3946, 6535, 5817, 5646, 3456, 367, 170, 2138, 6554, 5526, 1140, 708, 4809, 348, 159, 1212, 5343, 5663, 1778, 323, 3719, 5180, 6065, 1052, 323, 1295, 4697, 2834, 5100, 321, 3872, 4115, 4537, 4455, 4547, 2642, 2912, 4930, 5656, 1329, 597, 4839, 5505, 1946, 1217, 1052, 80, 3875, 3872, 1685, 929, 140, 4591, 4512, 4509, 4456, 4559, 4586, 6283, 1879, 494, 521, 4433, 5165, 6038, 2591, 2825, 2740, 3471, 566, 6546, 6300, 3861, 4887, 4968, 3565, 1901, 3863, 6051, 6126, 5155, 5153, 5114, 2216, 2, 29], "instruct": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。", "instruct_audio_path": "./dataset/tts_fake_data/zero_shot_prompt.wav"} +{"text": "His ways might be affected and effeminate and his conversational powers indifferent; but his bandaged wrist was a constant reminder to all the nieces that he possessed courage and ready wit, and it was but natural that he became more interesting to them because just now he was to an extent helpless, and his crippled hand had been acquired in their service.", "audio_tokens": [29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 28, 136, 3133, 6050, 6544, 6546, 4860, 4164, 1739, 6373, 5823, 5815, 6301, 3809, 1631, 1631, 5589, 3402, 80, 1781, 2348, 6310, 1147, 494, 1223, 1928, 1901, 5553, 5172, 4916, 2675, 5094, 5086, 197, 2411, 411, 2865, 6021, 6012, 6033, 1383, 5274, 4536, 4826, 966, 663, 6312, 3190, 2729, 2702, 5796, 5086, 1285, 2024, 3855, 240, 6039, 6013, 2352, 654, 2652, 109, 4510, 4509, 4510, 4591, 2269, 4798, 3150, 726, 3352, 6059, 5616, 2275, 4598, 4440, 2507, 3561, 645, 2702, 4283, 5566, 4968, 6516, 6554, 1919, 5057, 5756, 483, 5425, 6140, 503, 4431, 4601, 2669, 2906, 6139, 3677, 5966, 3878, 4051, 3402, 4537, 6275, 483, 375, 1383, 5815, 3544, 4916, 5594, 5984, 3126, 3096, 105, 654, 2895, 111, 2323, 2322, 2322, 4510, 4590, 4509, 2244, 494, 503, 4669, 2121, 6268, 3861, 4131, 1318, 512, 4219, 6041, 5824, 5085, 645, 1392, 6058, 1158, 662, 5759, 664, 3491, 1475, 3950, 6550, 6040, 5274, 4725, 2673, 654, 2653, 254, 3917, 6234, 3402, 5721, 843, 4544, 4435, 4429, 4685, 2747, 2825, 645, 513, 4887, 2835, 4297, 5310, 102, 2918, 5984, 3724, 323, 4694, 2510, 5097, 6545, 240, 1384, 6019, 5921, 664, 2650, 1843, 1757, 4375, 2188, 4384, 4907, 4934, 4934, 5663, 6382, 6373, 1957, 1959, 6312, 159, 1455, 4358, 1442, 2162, 4914, 2727, 5997, 6003, 3810, 1542, 5589, 2457, 501, 4227, 5313, 1411, 2405, 2153, 2171, 494, 2492, 3876, 4131, 4131, 4851, 4825, 5313, 4914, 4860, 678, 2625, 2357, 4597, 4509, 5579, 5821, 6461, 5948, 6015, 6004, 6003, 663, 5024, 5513, 4536, 4556, 4609, 240, 1385, 1715, 1466, 5587, 5580, 1384, 1658, 1901, 1469, 1712, 6375, 6553, 5311, 4536, 411, 675, 4836, 139, 2322, 4509, 4510, 2323, 2322, 4591, 109, 4455, 4826, 5337, 1428, 6553, 2188, 1721, 6394, 2025, 2676, 503, 5639, 5073, 165, 402, 483, 4851, 2909, 2900, 2904, 655, 5054, 4976, 3896, 5948, 5125, 6094, 984, 3417, 3129, 681, 2405, 1424, 1903, 1232, 1919, 170, 2405, 5329, 6302, 3871, 323, 1052, 6149, 5663, 5660, 5273, 5266, 6544, 726, 5057, 5705, 6064, 5049, 2673, 654, 6274, 209, 156, 705, 5538, 1554, 1230, 5310, 4583, 2968, 80, 809, 56, 4428, 2322, 2322, 4509, 4590, 35, 494, 4327, 1685, 2384, 5322, 6315, 1944, 5589, 664, 2837, 6277, 5089, 4998, 4860, 567, 483, 2184, 5095, 2911, 2668, 2401, 4426, 5870, 3701, 31, 4509, 4591, 2404, 109, 2405, 2171, 1841, 1721, 6144, 6186, 4860, 654, 5077, 1843, 1514, 5748, 483, 6556, 170, 4489, 4887, 4860, 654, 6519, 2899, 2668, 915, 102, 663, 4836, 4590, 4591, 4831, 5634, 4159, 494, 601, 1719, 3906, 4560, 6003, 4752, 4887, 4887, 4887, 31, 4509, 2322, 2323, 2404, 2296, 4536, 4826, 3153, 699, 4078, 6048, 5589, 2194, 2357, 4433, 3194, 6550, 1232, 2681, 6382, 4185, 1245, 4518, 4591, 5824, 6554, 6068, 5310, 2371, 3021, 483, 2865, 4509, 4843, 4062, 503, 3410, 6309, 1456, 5340, 89, 4409, 2243, 1001, 6383, 2822, 2903, 5581, 6023, 6007, 5939, 6029, 664, 3081, 2351, 6310, 159, 78, 5553, 6074, 4085, 5022, 4941, 6522, 6559, 6551, 3770, 3404, 5310, 5283, 5295, 4887, 4887, 4887, 4887, 2406, 110, 29, 29, 2], "instruct": "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。", "instruct_audio_path": "./dataset/tts_fake_data/zero_shot_prompt.wav"} \ No newline at end of file diff --git a/dataset/tts_fake_data/zero_shot_prompt.wav b/dataset/tts_fake_data/zero_shot_prompt.wav new file mode 100644 index 0000000000000000000000000000000000000000..a7b9d954289ddf5c90a4dc4f0a912edaea10945a GIT binary patch literal 334138 zcmX_}37pO4_xR7)cVi!0CQF3u=H9uVD<8XrCS)mUVvsPLKqblxfmP50fYv}ypd`>7_$Oba7`PPB4Y&>P0e1q8fUW>GtUMFlypAGil+Qsw z4t95mSr5Jycnp{Ve;K$scr^GGU?Z>q_$NiADmWKN0HQz|dO7fGcddr0Ccgi7_H-CdmTQmg)WHm zY;NG`UbF*a+XhI9*_+T26C`JoW5m3S3o{LgneiM4$;1$t!#9v!!LwpEWDD8 zjlhc_cflFpB=kE1-O#&9c@1=PK*!w&UW*?;AY+`(9Li_W=>#4QuZTHOJHJvMhh8P@ zGjDPh-QDn3L3anXQKv5DrQi>N_RvN6ZsWM4m`#)?z)wW?BK8(QYs}ur7kL!fRmvTp zzk@fMvieXSx-ob(<;}=+9#%p>2I%we0&;`eFrY3xh`sB7b|O^ABfuMF1lYy%8Mu5qgCEEzf* zdJM22-^P?z3|ssDmxwREPM0IwPdS}-XHoY! ze6^?XI0xPf;4*;PDgrc?Zi&+-1ok@EHtgEoMt!xH4tUg^9uk=ZT?wpXtNF64^6-!N za!WH#+p(cz(GCbxrv+H&{dOP)s0NS&)>3tmRwL4Wko=_Xsc)A9f*I3ax9!_KEtsHadXE05hP|@l*5Kujqa6BX7bd zcN>lA-+(mA`|*1d^0Jh5d^>_`(YIpYa?~vYtb+gl<6iKk8ReSH-96~_L$5Tve)zKu zexkOE-Fo1`$o48mcGV{)hw^@KAz0hjwmt#&p;H|GSL!b|M+5ue?N1@6fVF=OfrOAq z3H%r2*~m@-3y|lKS6It3fcR|(o(Hcy_OuNL(6z)|nS;Q5$~x8>H{GzO>-Y*h4?G8c z1AMrXwOWGy{QzI*Q{((y@OQukpcArxu@wms-#`c85_6J?O#3?Xzjidfj$#jg&0%Ef zude4;fQ__wKlpcGFOWg~_ra=9`#QeT9h$aeBK+?3`!8@JuolQ;-gc3%`XS#)`6Jp~ z3YZXUi+Yu4^K1Az9*4jW(x;)&3Fu^iH6Nm7X0po)VLaw}u$Z6NknXZ{B^o8*-!>L1D2;(Jt z0Af=vBg;a@TGl*cv~M?3#MQi{V;BWa!HKb%J*)O%rQeP*avtD`lQybidc&-3lFT|b*oQKx(#T3pu$mx@efUoP9vC-H!8vz}c zPbsU7B4R3r&b!cpzWSnLr}3|63=iE*fF2BQ7Pu+8*U{+;?gAWw*ALlsFh01|;f(;>|8}wo;v3k+u_uE2KoT1G`@&p_>>MuMur5fwuzrc<_K;|0MFq z@$p+g^OKIR*3Uv#MEmK)<7{{_%9_*Fw+Qr0VC`ckUwnT#t@^wP&__Lk>iyH+vzS-; z8gPL6$TU8?0;_=@etQ-+H;^3yrUTchHx9iP_)LznXBC$ih{Dr4I$s)_?R@wQZiP;9 zWXx~+JnAflejDCM`bSQXrN9Psk=t{f#`IgzI%b;ViC?!hy4~O{r+kC9!r(EK^{m?y zOuX4W!{_6S4V+A|?;D0Yo~!G)5WU6l)ux_p%TR~)Xzy2yq0MJ^Qho|P{m{MH?#1UB z+fONLypMpVIc`0mv8cAnLBHXPzp<184+mD$wys4zC(j7Q_q7-4^K#${{Gsr5-91fC zs_y5^37tkenc!Jq?v!p@e9pwqIqIAQe)fsc{b2#T2hk&Lyf)~SLvK0dO~5r|uZGCk z*wp*2o^`%~k3Z&NXpN0ylsBR`3cLqA6#WEjI{t*$#xztIinY z!#@ZN0@RPkf!^q~^2PhC`NzO30%`#3km-JL9J-2+Tt&G!?Qfwx4tq=C^@lzPKZK1> zD9;00!rKL0qc7wd8=oFJhB(^`U-OHhta}+_r~8WL4ttk**~j>@XT1q;4*7T+n0XO$ ztIdh;V>2CEa|?a4_ka$p4|1wKe-R&ghcWHZNuu5g;)H$MzGZFpk*=Um63eU6`4}56 z`j^0M&}|D;hu#aW0I-Ja`M5rHR&y@aJ!K7i`l{y%&w(%bc-al`pUvgeTClTIDfX0}f|26*|$&csgo$z!$vd+Dp z!0mwA4Ki0L*#3%kHTNB%z7ygN17HqRPkLiR$0r{fTcOt?I}2^7Qv>-f=rPdmqmzjI zBse!ee(t=;noB{S_>{)*p9TJh-I8IuX3QzdcLO6r3@0}BL+dzw6pHVY3!pXrYZA+P zH`RF3^rU7DGP_)B#%49=TJtQ>Qz6QQE3N`^~^_qq`EF z!y)!n@CC~2!51mBmhAqcu{4OfS|x+Gb;t?1l-Vmi_4!qJ8V8qXU-MvjWQPG=hmK;}$idc|&?7_fJbMz}KIl2< z)CVu3{1kHDQuNMZbYE2e<^xmU{{rtJctZfyVGaD>eUzMI`&|s)G<d1pO_a|i)H+Ff3$bv zfzVpMfz2zlSB83Kmw2_pk~|+DhEraG-9qYXY@LTzTiXB2*uIF(eLmg?z`7o>WwsMj zx)uxIw?y{}PzGRMac{?F7w7~ZXJzW&Pg(DC^vNv226JcX`t2U=-VtVPBddYFo_WYW z`o3i6XasycTh~JWHEbQBenCjgKj=;ehzEW5GGXXejGOKyX{^1f;HS~ky+r3v$B^}5 z^E-0;eI|`IbU*qKySla$8Z zW8eWk^DacGS<{MYvfSKoMbt-eh=dM@Y;=y(@WUJYFjSq9}TKAR78-t;}f zzRzq!J{G;(@DqC)ul5}NCRqO~=d){4s)7&Vi}rUSa0#2)@CF05DI5B|4|x`NGi{xL zS4babp?5xCOc8CKMOS0+8QLReZBCtn9(FZX=>F*X7`JKI`JQr+emz2+V!*TLZ38z3 zdeLvr!15>ccA)nvJRP5gV6`_9{2YAxXf`v(T5lYF==|%t3_=&tMp^iGz+>LcJLsOE z>;nCOPVieYfB*m8Yyy2?OmshY*_-U$O>>UHR(eNq{j@U}eg*8lj=gWGKN&l}LQ9w! z0yfdcad>(L8x5`pFh(}^nnAAy_9J_WI&IP4kL+uB?Wp$`cpq)$5VyJ>HI5RPv(NEK z{_+T%J-&vvc?8Dl!F|z}G%>(K? z=Qul;bs6`^pjj`Pa}9aJ?E`%SyUWr2251dGG5!uy^alPBllIQ`K6BdxT|c>=*q40x zLw(FU`f2bz`o`L|_vDA+y@Pxkc5g*C9N8Xdo%?L)9?0mM-U$tI`(AVndJp{~ui5vK z#^4j^G=oon?Ky!wqi0vI8+A6re+d|h-s8wMU$w%=%E+$3zm6^PljcFMv7dP19HsA{ zU!(gJI#SWQv#9eYW$NcFhIcPIkKiMFXYwNG7RV+5nxnNJ=(=;@ z&qtRy^)wzTfTv+=1~g-5m=Ap?&~eedGkFO(AD|5no;euetO=fl&X)jw*}Zx@>+ECf zm&Z;WeAjj+<9ik4%x#gDxpT+{`u_rLY($^?i*!b3Fn9>Ijse)P?*xs(7imk!Is$)< z)&Vl1-vGD6rrzImT*%|*QS9h^ddOMF_U$nRtZ~TtHzmQm&)A%#aatLg{YL%*UZkvZ zt-g`d?LFsa3g=>9Jnv;7r)}+rzBAbOzCqxB;T;DK0n@R=y0@{k%O@^tNf=jq@41#@ z@AT%f+G8$UfBY@F5@qhgl1sfL>eG&XKee{*f#zOs--q?gp?gO%xkJZX*KS3y;E%>_ z1ic)ey$gG)tM@8BPddoR>-KwJCgtI@m4;q#;;te(Gmziq!v?%L_%I0h*UVwSAAdWF z(Z*LmDS!NqlTJI@W^=}R_J8M)w%E{|O#aZ{ct|p~z60(C4l@pSP`48H4pIIB5d83? zUmaU(DJLN737!X3g?7N~Lw4=B#7lk3*_0VmeTOtnXkYW~B6!Csd-&hdAAeWb4o~wz zE$oyC#m|+x2XzC+0F!{-z#Y`lI77$ngYIoW%M`wYg439vW7z2ht$z1Lf0WPe9}=Km zNqFj$-gkCTehoYSq>#_y{Yd#ea5H!v!8)d|g7H)TGe?lmMb;1AMe1J(+3#ESeU?4K z>_k2jc?aYRke3G^1-b&d|IGl`q7Loo_co*Rq%pCCa$WfAL-uzC`d!QVSV+4$*qupP z-^2v@BzSL97T^!cQ-S%=$KY3|&2i9E0CKij1a15vkqPv z>Sa?uk2du;0fk&WMhiDUDv!K(FzYk_^>^+L}fc>wK`t;Ymg*H6qUe6)h z(IYO6z`K)rxs)@oS%os^44r#D`|9^H!<^}Rm3)lObia75i3{e-BtsVm{|cY|$etPc zqNn3ZuC%`~xf}TbY#3lC_PQfupOZuQwi(_*Xg!;q1Lxrv=OcR`Y)1?=qpps_9{keS z)%n%2$^+`b8wTDBe-?d9pxp|gxX;`b)VBRDJQ@5bP=dBju|CV<7kjJejPGrzGn_ga zQ{*ze+t}Ya%>ZwLr?IVbaGADP+xEMMo-2qad%j!^UvnFJ_I~?3eBu2k8nXMGou{er zIs-cQcLSTzer@JrCJ?dn5G+9$4=W@tgro9@O)PIRLG9KRqkxI1i(&@wpVx z+;Rr^5}G~5{x(DBYb7=Vl*yO&om1CcBK96e*M~3U4BgWZ#@AY7Xl&Z^TNnD21Yg&2 z5_NRVaBj0}={a;}LlcW`9qeP%e*b9>Psgy$ZekC9b!Z)n&%if0P?rq z!`+GWBfF!$uiI}FAEN8fPE+`-WBUeM99`_$eSkJ?p0zofc+@>5AK4P1Ep|VkjqA|8 z>C1%O@px$O!&mIKfIkE%L0_)JOGiEzTp2jexlH%Qi`3IK_9c*k9&v1bVZ4X3mM-x| zwh_FW^;;UeCPeJOn@#zC{N>JK)`PW=9&4&O^cw6CU-}lUW9~JeUPH>m&?yFfjq*5f z5czRH>o-8Q6(BFz{c}3}HO!;7M_k+e`crrrl*xadzP;)C?H`K2LE)2~qXh6NY^Wc{ zpieUfC&1Z2(-7+rCa{{dh3p+kWB8^Qlv zhufQW4#VG0c_sXn#3_BT_lQxHvuKwbY4ge; zu&!Cn5!Znc(ApL;V)JD_Wz}s8uLkfDpt&-g@p}&X3iuTD%0p|eZWM}tv+7J=G*{)L z>yf9&A=CW+3_j`H>Di<=W>40a}Rr+ecxo< z?Khtr_@MPq0_=BgBlJVi5$ya;U*Nmls6)H<4y|iqG4vnc3*g&m<1XY=p_8$x>*oug zZOG)=*RrmG zp41tJPB!zHft}sxT%wNy;T17%?^CY~Yf^1yLf52Ux~IQSJ8$`Pcz&?0A0P>}MXPyTC3EI$e#T9I21IMYOb9xflGw7aR zoR?ERMEMD_=^A79X#)MB4SS|&3a#VC7~9`OGM2Wle`2>BbXV%@S@1Y|Goj}KuR-5IeW4#3r|TFK z)}FrS+k4BK)YE-O=-&AH3>77IDiNK!0oCX{>2p zz9S^bOR+VKvi9Rsfb+4v&$-m8Mwz)ci}1}wZ$IVwKtEs>ZH}VeP1<`8`K{2uz(?1{ z4nAqW?fXD~>iQ{j7kA6x7k6+e4rsjVn3DUGGbuAZ`fg(1JnjLbqqJwJ&pp@Z{-NX0 z1pFDc_d+w*vW9pqfo5c7!>r;g9F{&c66O=3wgw zpai-VsHgKb1pSY}I<~XG8q2y~bj-h`ydJ1Wy92?gKwH{mtn6JriL&N;J@-D~W3Qr2 z|LwQTAhfUI8l6_I4k82r$7Hf2-~Y{Sq?uwpIXS;(7$2LKDS%(UG3aMQvzObq+f;aS@PS-q z???mUWxGF=fxd{|cRQ*@OQ9}HAuUc(9`}H%E!Tzk!zk|e)XG<%X@~t*&DUl85jnT zOYHk&^%QbDAlNa~IS-r$hN4q}{YdA%3-vp|`vjiGjUm6i=wqID+cjptHD)q48Z%XB zQ)3{F*lI^T@|69}4`&~Jt8?|PqGP+0{^)+goO`SiccIy* zZA@-MrgPgH7zDopSjTq{y2r8k4e&odcL;o|jcg_S`@qb#wqf6)uTriCB+_SHFH?a3 zQSXe8^*~(%pT5h>=rp6fHstRQpdSJ>-fEy<5_$~u2Kwz$_gi>(13ET0kv#y+1BL-Z zfM?^_VACDwC;!pTIBc&08iefoj5&dh#&8+z_JdxIjCyW4bdOOkijTV~Nm*lU6;K1{ zfWGS1g6<6Hc_R~GtjulbhLK-F9)mXmdv)Q}qr8v#?ElN&{+&UoO#vzI(F=DukQhKr;)e8r9$>wjyVs#nsJ#$o!3M5-sy2})9*ZHA+n16 zJ;!wF%z<78?*MoMnD*n}zG+v-xg30rJJr#)^}B}ltxbq?YsmgiM+zyo1WLe5Mt+$- z&!*fQ=nC&7JdO8>lve;cW`}_aA=WqdMfz{wRC?2%#uz@k3HUaFcAut9|Ma`L>-fbh z51+Zwz0%(Ie#0+~i>j1=04D&tCi>tT?>F{rn+C5Xa<3ym-8Z05gH?Yj^cO%A;_g;# z_odCT@TUXw;ZMV-gJA97oMAFg1-81a{&6e(EZRkfd1paYtYn@3*cFlAD|uG`})Gmz_&&4 zcxSP1)=l7NhvIMi@1g%Oal-c#d+!QkTi2tm)f?35iM%mBjHbMe`Pu;QGXCiOwhyw| z@v@#zywcbW(w55}$X;yokMgpJU0nwYuzd*|v?-dKi^Dra{RNcgAX^MwD-{2`h{^D- zQN9xhQb*^4JBocy*yLpeY&h1L@QvB)V<9-UJO|KpD zbCi=QGX{42Zv#IZivMQ&0<;Ic4V@UY=ADz!59^}ShRMt|dL?~fVy^gq5aOy{hU zg^c$EkMBX|N5+GF*Qkv&e9=5#1(=W$|E;2)@s!_!arhE`5%y`~SkHT1;JfI&hM%;rcWm<#a1L7I^>J__P>;Sp3fBC0 zgLZ3B_u_wL;wkxM%5AClIXqp*8-4M87JoHQcs2d;vvzlEs(m;2X5t6?zgd#An|K?Aek&m;vZdLZ=)uU89=U^&Gwr`YwR^v)@7@*ys&h zMRzszjv>1bNC3VAitu+Y^2*R0B^L?0O${|8FKa^uM_3D(B}d6Jo~o} z$I)f{?LBvqkFyB&bdUL&c_6>rcL2?|#nD*{G)6WYy-e_jjORSs{g84)K;v^J<;wJh z__6zk&PP*Zy}|YAgRa*k$}54Z*l^)#Uz$*_0A3q-S+rLQTJQSP!B>C^$d`s#){24a z@Q5S(E>D}T=CwwEuCqbNm?xW4uR^O|pMo`aE(VXI4UJLF%Nr;cQBDW%Lp}nmV>%Ri zKX^0coxo*4Lh-v9zUsW$ebHc}82oMM>%Bzpu2ZO|W2R@Ik;szZ{{!z2#djZ~lY%&)1o*pI!yHQv3OAx5c z_`Hf9y7t|Yd6M_xh0!O+m}=ydRQ%YAye+(A$P?hNz(3B?W($~mhRg-fm0Qu%G1|{} zd+sKl-Vs^*HqYIe@$PH=)Vq?7PjmDigh#G4UnA$0EJAdls&g+2nW2)M|&<#5J38~0zf!P9;14$7UdLoPGF!^>qYp+Txe5IVw((8R9BxNH?V-C0-TUDU zqU}@Y=zRW4f5@N0nA&q9eYfw3C-FDpv%jlvzJQjM`kOYv*xa{M?7XhP^F*wna{C|KB%; zQM99Hozv(Oi}v?`G30uV_zGAIXq-(1TKl+T`|YWU5EZb%D>S67PPDJB-l)&er=!v`(zTldg%R0J8h742XZKD zzc$ks{C7Ef+i$iSBbo5WgI@y^b2>k+V4MBcqYe8z+S?h66s?2aB>a5|to~O9Mgw}s z{{g>#gKin(Egju+lwSsl;N1!69#TAH?}P3O$Uj7u&scE>5d5+Co6phPhh7ff886{S zFZ@tn+X8PP(>P|{&0b{f;prL(0V{!B@CL!t{HYFFnuII(*ZU#C)_XTrTGdIxv6908g&I6vJoR}iXN^hWE zQHS`wcLw@;mU$L?523@@c)F)=z+cAH9fE8bGM%r!;A#0%v=H8d=)M4S07e2wfZ?I| z_l(BO;~MP#39mGOuAP%s$n~taC*PhU%+JW0($*&Av!R(EQytsHl%84iH{Uj2jR*gQ zKZ(fB!e0%Pz{U;MQyOg<%0nr)N3RR^V&K=|1&QyL;rLr)bIQE{KmA}$NKJrxM&slz z@O-)w8mEqI10@;*)#38^u0bd*dy#5Jcn!%vJ#Brx={RmPHoi#bdMsJ z*nG>pSa}TldVa!BQwI6h)On9KHllwTU6&kl7@o#n0Ny5KcAlv70(|wY6}Mk9x1t2D!+7 zLoEqU`*sM(f=9lx=jtokE@fTENz{KD`ZIJ4^nUD5hSomhP;Lp#r<_2!54b77db8uW z5c)|#_d%VzL?7QI@Uc43gmNlC?CAF*8_S#<^j=}g`Q!Coq+ADiMapBrI-lvl6zr+p z=|E#>JwJEHkE4{+0G}_;)BRe{78Mzvr?9~qkmdk!uiu`%+o0b@w>ov^fOU-dErBT= zws&7?guj}5GzKoC;{$d9J>au{d%6b7K+lFpJ->*&4MzVDK6jOOn>=_>kidvq0iVgK?< zfQc=;f2L!jb~v8zwT|A+=&SwR6*7Ad*0G!h{X!`ITf_{??P-G?X;z2gcbjXJsb}Xx z_jc8(01N^=Y&OF`)}-l3TRK-c=<8Xj4Z1q+IegbxfxSQAXH!Sl^Az?(&38h%ka9af z=Ss&SM%|Lw(m48#@~6Nc_65y-HPM>@bVtV6=~~tKw0F)L$WOpKf=t(u=06=j)qNhk z5quhb)}pp;vT2_+WAoz+$Tj}-zIqM4mFT(1*~@LdD3QWg1Fun+v37|~`@Q!Ba$R3T zsY@)_z55sFC~yfIH<4?6ZwLF(TYzoO1NIF-_q5O81NOWz8v1kKGH`{M>BD@EK<6NJ zjsxUfqp|%kzH2PAPV`Kpb70iZYk;oH40Ltv>Ap7tyb#bBEu?SdLw5hs-@KSJ@N~Q{ z;?DqdJAn65w+J5R3O$F~-;!!h*SSiduI8x~$P&WLE4s@l>sYZKG*8-?Jq+&>^VI`C zv!IUw%K_cbLf{+pBcJxQ?JRuX1kMhLL8fmk_HFzewr0XBOZgb>5HI#Ey(GHJp=*Fk zhT=K5ka8yaTi}sD?6>j8*eB-Xb!@P7HE(2v6R)26;5%)`H`*hqz6 z4SoiAHK4I_3itpXZQC>FLCRB*b-{PdpVMgL1IoImb%OpwY3v1fH&}>G#?hY3{MgVu z)dTz;P#4{@lv{^5)6%ay83%l@_y1zpxIsHP=p_Qgr5pf4z-8bz>Wx7Ec`)S*!+nyW96dnzQ}LTXT!TrxeE2t)Ngz^h)xzz4A})RF{ST6_TMo` zp?q;ScRTc7hQ1Yi8(8P}Q}jktrztQ9cpaI>lG@Pt{~7!We%u8h)8EG1b;5Yr*z5|Q zv!nfOz;j^cTmQzyD}~+z_=O+(y~)+M@Bz02XRu2j_4fcKlm27dj{Amu`}+gi-{Q2} z&1c`P^qt7QaSg$4PwMjyBQwxVrHl<5SK8Mnf%eFHQWu}}+^N6C&~s$bOO$n<=7IO) zQ)y&+-c`V50>HWysFvk_a;n}_3cj2>ZSphz1haYD9Q_{*B!{FUIQ@kqu-Iua_F73 zLEHLwEJo+!|<#_l(N$JfJt@MB6q${Y#(7$@VUx zYheyF{gC&-8_?7KYwnp!c~tz{jigZin=*5u{k3l={ixHEvCudqPuM+2bD8F<1Y|dX zT0ke{-Qj88BDU>!GqsnFEzLuH0mfKu>u)+-V#ThbO!_gLGmx%x>T7Nh9peufb7D~A zU2}l`zDHwA|JEh_E%>d`nNB;J#}38E+#@HbeNz)V#C*}q&^q6fv7vE;k7`T5N!fR? zF+O|0uy4}TwPz}gANBWKJm&1Tr0?K81q@|x$e=!JSXy91$AsMC&8N&5>)Fp`jP%}a z-@b-pBMpBuDKqAJA9Oi~daOlVm-c*_4WHa;_k(57tR-E~HkQ|5n{{l*xHDs?dkkZ5 ze-m1tI?SbLjIIFFKV8H6#;d@copN zeb&AtAZI*0?b8zEtTWMhA;#@qx(It?;fhXrGU~g$;6oj=yP!zUF~>$XG8n z{@Jr^&Sd?Yr~JeL^QLjYnApAJZS-@9t)<|h)T95JH|+11KLek~{`)@sAzy30oPpgs zw8fsU|Gu7gE4(M*Z9{huPz`7eJPVI_(C_f}Y;+krStX2yd%+fU2Ee2?el1#ZO`j~>;y6$n=pLVrak-USgu3&FL>Jly(jz+ zySJi$nQ~QNE;hag@WFoL)wXp$CWm5iXZJRH*V&EUA>Stnm0X)XSe#71auQ_u4?-FgzqO4<}vGj(|zD?QRE)=KS2e=Dg-UaLT z1-)wy25!=B4aWR+_%1fcvnGdj=K11p0FT2j&e&@ELG26Wy2z6K@qc^aI&@8VYX3U* znOn0C-b!p-0xOMQ9&PA*gJ}&O1yADbxIw?TBjI-g%G1s;;^Y>1sp#EzC zNcjiaxRdfM=%2ygfCmDJA$xa{_0Xfx-2g7m{8xsqg)Q2#-x11FX9u$LKqJ~I3Ewcj zT|)8u$P36j!yAM=3()y#79UG{KTD>pe-EPLL44UebJAkYn_#Sx6ps!I+&qD`&_TQy2 zJKcM!%-%njBC7+hH2VKwHx=JcLEj4x zef@6aGCrdH`IhorU>~px-6KFxXyVcS242@-rBIx|2HrsE4&YqyLFT>~HuMg95^$h3 z=5*dB0$qW{_{X}pe-BtHBxWFVP3rcAejE96^nV9mg;$7;zoEYdZpUZF$@8Iq23Zz* zy0&^Swwm`CNBfN)JN7QC^BINKb?1QRQ~xJ;Wq>IFx!<1K_9GvjAAcLdH~WsGdvQzX znbcwY_5NbNBUXgo3y=fsT^PB27t%F%FW?eeb-{Vmp9K!*$N#Q8llr+pdPwxVWL`#= zL0MyidyRR^7r#dr?T-IutULVP@Ft>{i>(ak=>TiO=sH*e&DyoUWBx$-A^ST(T?d}# z#+Tq}3^9hrqs=DpbX;`|R>IeHrG08l+tf9>F4j=a23T+U&BXo=@fdg*F}W4qN1^zA zP1`;UkRPnx80fapv?Heh?SENc{4AXXJp|Bq=ZhK}_|c1US7P8U+U!Ex8aFrb16{8X z@;4}R2R5(Z>se@x*F(TnU=;egUu(|PTzo2@^~v~XU*`GZ_i;Ugy@pO}%9DT!$Q-cV z0V-4f3bc-;w)-kL5oiVf5alw!ivaQHm5SRjnaH)T1wbu8`#9KV_YQrRw)u~IVD|{k z-Sz1kXFvOn`T%+w_nUyC5N(1z{L#6^f43xhjDh{V({|=g&p?MM>)Z}UPv^BaH2#`* zs6#yI+m9h<+PD})c_4hfb539!dSQdSYu6*|PtRobU1=J6z0pxyccH%>tnEGr-3O=z zXzbnwzCXm65G%U(R-x2-5b7#uj@M{@7N=3{%!$J=X(zEhVS;gpf+~GJ4hdEQ;$7FTGAKI zhkCBK$hbZNUv(B!U+DwTw*#zkqjSTaW#0t*LuXL;0bm6F>7J{3SL-r1ZYgNB-wn7A z`4;%PCUt)v0=*&>&xM7wJC*V!`b}G=A97)SRx_uX_ru7h0cTUFhaFwNPg8y#s07^0 z_-ng)&{OE^S74pvheB3Q-^TPimhQ_oFBL-;1nvpNzbWdN6KD3Wd4)DK5A;RXgVzdR zAJg|nuOT*6?+{P`Xx`B^H4+#A=-b*A@Gk1N0PhD^4#j_yNKVn;J=izIPr#ZFM(>Fa=uUA79K#E$1^FA@OE~<7Z0Uqx--g2Rwt`dhl-G2ztMV?7xlTamTRV zvkK_rN@Qcp7#)1$v9vEN&6P~Jcr zn!D;ES3k}n8-l%$kmV5j|4=Uc544S~=mx{_JFxf|IbbO%A}9G3)VV6!!Iek<{ImU1;>Oyh1bGAr3h+2{r7s1uR;=IJze%*m+O=nw)x_)$XwHF}d-UzrG=rvno9k%P z3+xOD%;j3KxQ5k#@ ztb1Kw#-3cI@06a72V-jgR_hsf%lYl{D~tu_Co>HhIjQJXaDAW)dbI2Hgzg8eF>(tw zIHRP`{I5;`*`NG-W(NEdGIE8RO@5n;ob!-Z6Ta@#>gPA$)c}36-(hB>ukDP-?}T_x zwBO|4WsDeWHwoE!^gYUYULr3Wox=yv??IWoqrcU5>63dqGFi*=*l1?=9VZEK$JVN=&ZXXrHgvlGyD`xvk#6u)KXh%N2&fgZzuU0; z^&)iZgm@E%?}xq+ivK$#yWnX)WnRp_5O4S4_?v$w_8QZ6XJi`BIzG=q>)kL9**R#n znE?MVwqk(BP77c@vc_TiMr(Laux~|a)GGn6D|3VmGXxNL>C9t&=&Hy%B5MK~t}V|+&Y8sAK{*xp27BaMJtygTRZ0QhlE*js#m%CAQ!smi_C@z@`}f4% z(b2PYZSYI*uVc3lbOw6bzUJ3ql(nt-;K~4JHm^D5Bw!5X7U-6TPJ}*<>^bmM>U9Hd z1JJSOyJGmJ`)DP~gW%6X?-aZqzzg`pzNR@#_h8M@_UxoBLKP6{*4*uU5!=yZa2h7 z4B|Wco3?Gf1MAu32DToDzKEE)wlM*5oAj#R|f|vmj}uMBY~}q2X*ut zihUb=(Z?QuZcEBL;I9T7bjY1O4fX-*|I|FL~{+$D(x&Az$?W#|QfM)0~0Jp}j=DINDhLmpt+U`&2kAbf- z!+-Yd{R8v_pb4@Msn?LUwooQ_*gm#H|8B}VDdVTUzuPy|DEjJCYhV0*n4D$bcXz@g zSLmC#RG{u7KKhdq{|>J)u6~w*HwAk|@VkR2p|5ef4SE9b9&`z0+tKX=%%^+}TnEhE z%Va{|0%Q?~MV#v!!q>BZZ+t54w{Ik7H9U=(BrW47UdOy&h4)NG3sd!Spxkg&=C4Ba7FB~{&g+ryS3%*!+uTVfX#Dh&}Y%r z{Kt9Bz7IbSk6h(y?x3$Zv%!y2?g3W&ZNRmt{~NZ*S1$R~M5(Vi?E&)9J@7P_`GLWH z{_Qj57t2E|{~9{?0IcIp^ce&`-wec`rb}=;t@}z&z?T0wz&+ zCFfaq_U^qAoejVcU_5}mCz|Kw3*(C%6Q+U!9Gd-6Wz*RlO&+@^U4{Ws(7=0JP<;2%WiZSb17 zPSFBvy^G$*$k`i`kK#)hSVY^{mWAl;q)ZpRK7bvUwhTNzG>f?pn23(wG{RVa3K|fj`Mcd(f1>*mjdPM~?;Y;Y_1>#*V zkffgrWatkCk_(+zEF%7E)VWf?`&C3TD@Wvbm5AIZ6On!8Ba&5va_NXnxmh61N<<`? za(ab`bgv$f>a`S>%_A}n-7(b7N{+DpX|E){<8KF` z1Z{6bKiC0V0oratAMmAjQbZ1wi^#N@4;j~w3gjYV>Mu^){}f0Vy0?}K3*;no zlC}ZftKf|VlDwL^m{TD8vl#o8jCmgOu!8nxqR*TbHe38*Jx}izX{E6B zC-ImQvN_Ei>$ktrO(q8>XF}__=Kye)J($?dc{eO=s-)sas{QV(fA8tlOO-JzQpF`t z6n%+(d@<9(SJ=I{myIV(%y@#n?>;VLYJh^>>u7Lbb>%Vcv_G3b25|la~t;S;(HbR z=;o8e+W6TAKi7q2LKVjUCDs6IWDfJ-UBc%c*jVb9taA8wjyh%77aKB$tWmu?6^&+W za~OBVIC(j8^3auAku}7>;~_~*q+RAU=_}@O5B6DW&F12Ru+L^N#bMkA|h$MReLlO*=oHnVd^Rk()Co5YNSR+#Wn8&{1ZxI3uj9*NA3sw}m84z)@(cbwk}5ar z1msE(-Npe)I+7~YB_M~|p??zj!c?hOG9b+!46wHZB%@M53ZD!}X1{<;Y7vlewF8nn zEFjyS4M@!mw3!31c|fM*(ieQ+ho2|=2V~ydfMn$dq;FwBMsE(t{#F6$Fg!pWre7yg zIhF=wR(3$jVPg`$q|5?-4ah&A(Wfo}NgNuGb&CUXZgoI%?!@=@0l9wx@K!*27GQsG zK(ap%$iX)Pl8oI+n*y?V9qrAht{adA&jln93do5(aD={G2*}O-0r7tjkUN*+cP2jd z4af`V&w3>wd#GFKIBk9skbK(An}Z()-5uDA1!Q6tdf3@XztRUnM+4Fh-BKS1WaaXJ z3{OUn_6F0=nq`c^69Kt)IUvtnqb>Tn=H-BNrQN>tAxyuPhXe9;c}MnP*S|K^`d#%) zeB8#^zZQ@mexzUHnWqu zkUtX~`Kh=g?yI!FJ5_SJF~`k`N6sE5ojhI(UuOrT&6t4HVVtkf$K}k`@LCZ$TqP?0 z(*?5OE8>(k(pbZV>r!Q6IY*wLf6JS)f2XiFRxglC)g$Ci*1%WT{2^5miJ{ZjtT%y} z7eFbAvFCy#a(Z{vKu8-VPA}XtDMWr{fbw>k7w!VWOf_-vw z(z#T*_%vsSO!n-H-&OCnNxOjK&Vjve|iD9rkpP9E^^{|}#BIf3n^Q3+Iw%E5yHX;s0IXNc1| zv^{~kz0un~GAd=;MdjwGh~zb3jr^1<=Qnd6=!O1!0Xf<^YQBp`W!KjcsdcX-iNON- zxoK2}PYTHFQjYX57d17GM&-kvQMsq9BQxd%WT>J2SD2gX0U7Z~RNl;r%ETW6vU*QI zo|+exGer^kyt^Z@-vXxNhNv`oDk?2mzuu$*vtwaYE_aQZzH=O@)x|M~7e`IKl~K9U zFeI3xvl}>O&Jjn3&M$CBRg00| zBC`Kk;-+M(ygf0>zgHfWDVtJd2IIZ|K&pJuH!6o(M`g~80(q}~R4ye(2doz~;`m4G}!jE#RVDjR=eZttPLsS)X# z#8?dpnB4^qvFMnwCTdFOMCGA`sN_v$Un|ERHO?=AE{>$HDv&QKMP=JA=3#u;Tw%_0 zw?t)ML&uE!#F33d$!(pYl2(uWl^KxpwDU%Qehx~Njqj5)`Vl*60ok>lb=AH=n(tyx z$SY43cVy&b?3SdTU#7~yevTY!6_EENDiuEFoI5BYdx*Eu#K&C^JKWF1vZHoX3YVvH zR%Ja`i0?#$!@G|BB94s1-llF*soReH(;yoCEnm&v?6p-U19QwpLy-`%Y zE$K)!B`VK+%UFgSDfd0OmH51^LDVdL&oQNEIr7ogs0rss<>#7?*}aVXiO$91tlv8# zQfwi)=xnNaJTGdlPshj49CQzoYL|~nB5NyY1bz;U$V$d>`zsOIv6VHI7LW&P zM&-$;9a$Y>&KS#KZ$`=I5%cGI$Gp1MkpZl~ymAHR>9-g^)@{z=sQgnHki)GUDU%+N zNgc7f*^$PXj=WwkD$8m_-TNFj31IA+h; zs7&h^HMbWzl2y$y`Han~DUM7n#k{{4k)pRbx6F^4%(+oBai1fPj&tP3L*(Y!1?J)m zM-CKcoW?|D&}2t`pr1+1UuIXw>|0NaMmZ;w13R$q9f@&Y>76Qpds$D%B65vboAhMB zRA?NOovfP`Q=*be-Epk73g0{O{Y2D!Um_@XwQyv9V@KwZd#@E>|6OuWv8WNo zrXpkPf0z5!*nl+b??{C4udp&|j?%B9b`dE_{mf&IJV_pTW-+lx-blVDD$CDu9=fx@ zjL#=*_iA(6*UD%9rNaVN8U~+uXJ~0OH$CRc`fR`*)k?I z=W@>37?8`gVsiDnfEj$#F)tJkn#;4I(&7qn&@doPXRucLa#n5|^#({#t^!_*sA;h< zDt*3+$c3tox$|b!4EP{wZcGkJvmaB8J!!bXecf2kMF*BufOnR>^FcXP|HOrZgvVO0}13}YId-k&@e^GH`Iz zyjChGG0%}R7bE7kgIIrL7`%4m zxNib7cLU=wkQnX}Fbh8*_Z$hEN{oU3j({vF=Wstvm3Kxva`O{MuEbJJsf$sQdM+w; zevA}7JS!-T$41PH4P&P2BFDU4CuqKE7;_I)jCpk@1SRd3RBuz+n2hS?n6}u~wfUSc zYOX#Ql*DHPCiqZH%1F#KsT%b15`!{egd-ic2F$VHG4tRFN7`&DFnjKfxvLX`=3XZ# zH*(kqiwE4p{ewBPX2i^=-D74)LeTy3o}iR?%#q9GVrKICsM$kI_kG@xqT-IUPmRf$ zq^NhLMNqQ;;0$#rA}6N!P!{?Eh+|68e{ns&#GNaP1vtt(j>Xmc7)9rN3KCw;=Um^-(8%nOHtQtwX3jIR;ndnR|ckwNYx1>O|eyH+VGzklqQ zgxQYUeQ?a(QY>h?o1mN?5tFB;#>{Brk}1_YDckv>r~nH#cS~ zy%UxGYl2elhU2=;VJ3_A|yLA?7~yRZQxQ z2^NjI987<2Z7gR&ZY;Uk9L9#+Go~;7>qu-;zf`Z7mpd>fM-v?Pfy|(sJri+{PKwF$ zyw=*uiQEjDbDzYd;wRDMor{CUGz^+2 z=EdC1r7_d=U&q}sJSasQ!#S}TF{xX|Nk73@SML{1esXcl2>B>$3bEQSkaJ{1%)Qzv zXu8aGyrcAEPvxNdacz9%%rv%7%&pcqX6EKPUiR{!d8%g2tz4Y3>CV`eh`OT+Vv=(@ zl0G38ls`WY6m@$wW?n(I=zt?tpNPqQnNhRyC+3Ho@Xd&*d+7O?`RUiFcm3g@Jl!uS zA2*D8EvLqKi%pe5>m%NmrDJA$rsKsn1!Y-EG-u{}F`2|#{5u#d%KafGE0;!#M$He( z+r;u6SEA^DL4bkCqWv~^5=8y_>b z<#662_td{9CLecmmq(K@cU3y3wO7kvY$C^{hI2)Sbl=eLiZe_Itj+Kk|CTYbG=Ip7WIV`~AGn zWz3*|*GY*ml#Ls~mn2JD7Qw33v$1B4xe?Df>-vjiL#q&Ov2x?@Z8q)bWyx}?W*R?M zaF*jDe6>P4UM}b6fQ8)Xv0J(yK9c5{bKLlADAz~COS8UJ`Z8B@bMGAPyDO!3C0Xjz zB-amwa$nd*X?%U)aek}`EX04*^(6w?;D2n+seQrTJOG_P7eizESZ?uF0JG_6$&TNX zEdO?HKI_7ba|fhZAYSSL$2d)XxbMHkl38Oi^;O83R+6(h4E2NKq`5OM=shZOebfzU zocY7U{_e#cKT|$HZ)pw+qI@C_HLstI-J^P$SkU9X{|9F~*JK)>-+S2mL795jl3ah+ zM4HvwN^{Xbt_`EuEU?U@FH4m)%Q~um{hgbCQjJPG@6j49qu1hW<~ho{w(~vaf5SP8 zv`3izD@it_zRj%Mi<=85hJIKj%Sw+hV`owQI&5Px(b5Q~I?;#O^ncoL)+>r|Nub9l zlI3AbE7^=e6i2hm(pol_#*KwG^HY1;_bZu3R!M1oqkF9bHa&o_fOa^8eHd?J%PLX- znok&Mg-xGYjI*ruHkR;}_!HG(yzODP3wqd&>-4>u6b~_k)qeK4=hK=Tb#n8*Ll zW=P{(B(2wD?rWaN&0lk+v8jmk#s5dKnU`z-J1JSIRvx{6DrXxmWE$@udDxHDHod*Z znTI~>KrgAU>BtS2;O3ejX$DM|Y-$6p58uw&;a{Y7YNymG+T7*WaO0`pX7&k|Eb(@P zS&jC%*A&TG9r75n?-O<+P7u>cvfmSkdr>{uPWU9}a)fa#P8v(8_Xo9%(7lZ~n__zO zRt+RGH)j}KS97EBZktiGku)-?hAyqe&D^zA*G!u}s3Y~nWf8{17dG~m>aUe>OSjJ4 z*p=X66>EA}uNV*2X&cKL=rLCWP!2jX%|6Y!v8e%P*WP*9s@5K(-6oIGMR{10P|j+; zBW^<&*hRg6-z<-@Q&C^se3Y$;^Dubh(Nk@b{WQmBJlRb+E0}V53E_;z9`>#-@!L4+ zO~IMQrA&`;SL1X2&a?qM0EBXymXX_W9$KiX?!Z>rkdutKtF#UvB`JSHa5KK{?foNJG=Q+=pc z#nWr_Ue_|hZuK(R$qydW)q%5ul%vBs6EA;Y^PNqSdj0&=2k8Dsww5gNrHAp2HvQM} zoIP1;)2bYhtY15NFMZ$kyquj`PCVj+ja8C1*0!fd-xMO*vk1vn+_JHyl#jECvv#Ox zV+UM>(em1~%CjYNrO>SAR7++JiZGsy;Vdfos4;D^$MEhUj2_Nee!9M>QnXjJrneW_ z%o$TSYfsqWH09*op`0aIq%r!5)c?~Z3us2QyKe+5`N-p2GoPD-W9Yq|JVu*8JjUof zk{zbnQZChIOq=U*+&RwmX8Sm6cF<$2rr19cW@B}pd(3J%(p=h_YHB6W4&0>IY5is& z$TTaoqI=0F&9Qc=w<##u=LI&m?jg+mlyGMz=@SK|uj6{@u158GB*pPJs)@lA)7OI| z+|A~4_UO1xUp+%Q0;xB@qP1kzjn zB7D!*aU;)N!c;RejaihZk)@^ir8vdUVw*Xw31@$du^F%KdH(OboE}w{vwzmu%pajr zFIrQQt{!2mernTSIk{0KQR-vHOJ7tfVYKBo{ZS#QXFH@;cMWGx_fxHVOxn~lTI-=6 z(^gFy1Adj}sA}A3vV^;1|ByyYisyfcSGG7reY%@X?|odl^CwHw7XZfcE!-$gG5D2g zcFZWrZusbWemQE~O7ZA<1!0nsEsbewZDbwbdPI9^6sLUumoT>bbhvST4e`Suo3VxR zE3BH$cz1*}#-F%x^0P-jK3PS|2m5ud(O}P`2>ASm0vaUru`u>fCg?b;=M^Z2C zyxQhId5tq$yv^K6y|2&4Om={JOyjgnqvQdPdA&L}s#EO$uaPv*hD-B#2X2^ixV~$E z^c6^<+Bb;1`Fr9>7d^gegfT)t+I$g|%LQiJ^fPOu9yVSYNffg)iNpPQ*JivuV>5=j zZN`Kcsa;$tjl)N5`gq{x&) zq?qmKv+4eUgvZa29#qR_h7pFl(^Bd^`fxL7G-v6TBlH#BrGA^P=Wt8Hk?%e3^jTET zleoEI5I6SiAbwBvF?dUa`^*8U4{j-aH|BHS@XZv9JtGWXS7{z8#dWZ8)@eQUMGNf# z#qvIf2BW%T)SK-wdcF0S&oZU{Z!ov!sRjCc!m=mUc+B|z z(o6?w?)qJ_cAJPJUG|uJ;y4>bH7JYr^;k#l`RbI0`k+*q6CF}4nsZ02N}afo^gdwi4)=u6!Fe5S!Gan?MGwBc;Zy|UCp zceP~Cs}ZK{%gu-M+1sf8^j%CiWf5oN+E9*Oq<*!9wBW9sxlYi0ta66$HpTL|0@4y( z5ZS!0HhsZG>ankEWI2%R&c#gM#Xo5ulOpsjw<+HCWtz8j&Q=$-nK2X-LpPI7rab2S zB2qsynQDAdZnl`iwFUo^KG$c$5jD6`yE@k+`$&gvzGPcp+Vp0`ovYsT_)e{n=EP#$ zc)8kU?ye;DEq!PWt8?=^Vesy+Jcc${nuXdBMzwGUOP228S0!6U_2Yg9_1bYZvrZ>z z7M#b;c_X;nORuH&l-dWvI5{Dl4Gtx&__C$>s6KI``dt6DywnpbNn`n38w>m+!kA6G zjeTfoJ}kq{^TbbHjg{`XTcx=*5A{^S^;U|-n(H$iM^8{q`bOG87s-ZQmd3^THnt*z zaB&HmgH4JsN>c5-aMGraWW+TJdCcrt+!(stV`_=g=tFBx+8wL@D$`6PJ!7z)o1>?3 z7T+nt*F2s2M0=_Cz0b|wVN}O+=zeE=SSa;l+HY3oMW!+0hRvKgl$)8FG#dWpal4*z z_PVA=k9HBy{8sj=}ZPGHrCT@?zLGdi?(%e6v_Ma$~mi6z{C1b zKbSb28{yR`Uvfxi9+=5a6DH_$%cFN`P57Mn_UD1rE21RZP+Yo)rgF1&U2aTXDRn+q z8ly&Y{So!%OK!%bYpob9>e1ijZ`k$U%vwEsc$-A_pmu5#4) zSxEMsdyIT>+}zhovej2SdLzh=m~g5U{S1$(J0$yhD8gvfi?hA=BlJE~sP@nDu#|~}GYP}oiu4%ig(Z`UW}6L2 zuPSO|yS`H_MI2?f3Q5+!vd7q;j~f>W&(x>AXlV7Y^d8`aN`G*clwb7co%q{7rUiQ`YBYm1^9o7Oy&&HJ9o zus3PsbBRk#=d3s3vA+k|OjUqz)H%`=2_N^aLU~t_v(foDJ4rfTJ=)htdq^*9el zc!YzSksXPf79_q&oVCeC?Rw|sd+9w|4AF3NKtfbvgjm;Zn(`UCQEiA$2_;(TUf%eka zyw<}CKP8=Ki!|<2?>R=?Bm9$x-LLD>V~b0J z&fO-$h$XG0KpAOfJ(leHLCMnhWwO_4#FJ?4Clki843TX9XTlvF>9xkxgQt1a2X zD-mpHbIum`@Hn2Wl)fE1CA&x88Bg~-^>2#J)kj(NH8!IkUGJrqr2AB*HA#&yCS0#}DzT4dW zcXQ+JZ=Ct-6Fy7gdWDso@mQN_PLu9Qhov!%;<#y3$(nDFzG17R`Rt^0l=&jv$Md5W zT?X0f=4km*4;^`1pq*Dmb7Nh!6purF(@@kav_v z>@^X_tV-PRem~brKH-io-?{r+Y0%x3z*?&|coVvSc6K1>lj6Yx3?#e{KiXNk4(B zZY?kimj_?v52Tq=&p%<1-cXL4!y9p9(HWYh{q8X$K6-SGbc~oxs;k!|t8z~2@)_aG zVyIs!i`MUr(N!f3&BBAxac466AND|J+llDhGY{RfS7O-hMQFB9#Q0;$=xnhK^^7&> zpS%nmPiLYLHVbpLd6<5BCOW=PMN3#mG*lCG)~tix)JRM}rlF&@DfOgN(kMysy;xDL zT}ShQUukA!m#jt%<-#!!>(H4S%WiY8$-wc}!}Yyqx$|uxxQ90e_dEfntO(xpPQW%V zpfzzr#*`Ic^-ch*eIaD6Uj^10>p*L^7~(pnK+wRg5Oi`Eq#f7^Z1ECs510V%=z(Af zpALx`2DncQ0BvYXaJQ-fj+La>=|#aeB8%&FC{E{3=jK0@2QZKNW_il(Gafc9hx$la z=^M0G8lD@{QmHhW`O2WX)>mmiQ4Y1L)hKQ{q0z;Tu3l|0s@`0TdpZYm;+LV`JQ*{t zuSWayKQL<0A81*-2d$qrW1?dTnsOepeoHa?z#dGjnTnQ_sc3!m6KbOxV|sIr)-gTN zGO#4AV{`O($cv6i&q#9&LiT`WIB#r{RixfxpGkG7fX7i|J9k%bbA9b_?ml*(>$L;G z`F<=|>rDsyl-^+c69??+crZS!hKyP}z!rI|;b#_T@@I&dmRRYadjZxdw1@#j>(YJpDYG=ow%QqEMdd|U& zs`Jp-Wf{6gZbp|U9o;w5G3xRz4EZ53=-X+G9drSm30E+r$99Zsy%ghmEJAm`HE6%B z(3!mkoqvx)J*E{}`nEtz*9N_G)n`p=c@{uzd5kcQ^9|FJ9zy&!O=7g?B}*a_7?>Ip>v=;y9^mEvmrg? zE@Yj44EBE>Kza`^IDb0{QHPF!bH-V46g>{%{r`ZV3rXPUXaE~934A{}fIX-O{s|1+ zLoajRK&q={dUE6WWtusYKKJ#a$Jaf9w7tvHxw8mbF4aW8Sqr`YG(!E)euVRXN6Vxo zX#cPZ9hDO>=t2rQ$|s{)%Y})n4q)Q1C(+mKCWei=hG~Cfq2A*v=1%z=4No>^Ex(QF z>yKg7lZ}`dzXQWs?Lbyy5BfgrKwr(_=#S}&_Wvkvql%%uaRt0jWs*D`>LC>PbI-z8wHLM4t`qPV$vi2^pYT1xj z<2tx(K5#dF30lXO;BN2^Cf#Hr>r{ZS&ioI;Uf+SJIak2n;wESzmmy^H3DDkcgN&+6 z!JIyk)~z$>wKn%tF0Ft z=}&-_dJSPU-b2pTFA)EViKcg$$a(%19`^|p(R~YuL)D51t!*LUbbbZvPxl}<>=J~% zx&*m15nQd)z;!PXLUtv9b=ok{2Q>%xxbNK2eF--@5+;owl4-2C;nDj=Nyp83G)KH9 ztt&04wf+fN++>WOycnb2EW@af<(O728RPb^$E=%s(REj%_s{dFUGZY>w_6z3|GMD$9d{Dx^U^71BBf!$PvqYB`E=Uyq}E&lSwz5>xiCL|2_S zv`=e%GNPJgVxI+U))Pj7%s9HcIZwMCaC*~6=-M+*13HgP6 zUm@XZnP23VeE?w=4`>s1LQe2Ti0hXGz70dbmuUlQyN2K`RT)^-JKT7&*=F<{Asxl` zOUJIg(*6Flbk!}0>04`{^N+^p_}7jhsdLajbUg z?!1HkPW#a~vkI+;bhJ7Kpb=3UjWoh)Ra!`6BXNv-E6J<2J;EI)xg|Ut5@*ZX*fo(1;pUT|0QLv~J(=$BSRB(kC+C8V$z(##@4t`rm6jY7gc zET5271H@xmv!;^*gzteLT>guY{nwvh?%V~IuPec*I0MX&%^>|@N$}mx3+`IyIIBIA zvl5>(jiH03V>4-jjt|njT^Q|H9UZQCHE8 z*YR<9KIP1PLOHk{E#v1S`>i@U2ij3PxrOE>sV&*NJRW_FoiKarOkacD+z}ZHZhZhG zj(0= zp+fDzX|(+J7~=xoWA^rA7(R6l8kKcSZ&wm^l_X6zo9pZLlm3#)nX`$pk zfDMpaLqSxmg!q1cgL}nW2!24hb?iQPPh0?F!!vjvX%R-J3c?jh>l9cneATp3fcS6S({H z9O@;+!~e`H%}H5OFY{Si9Q820LL|D=hM?=-5Hxo7M03C(bS-kAer^f6!`D$?+kxp5 zDevBF!K@aCs0JUvtl}pyEaevZR{y~0n&nl>+nOrdT1N#9sjQOjmsGop=TVNaUoegO zqFHPw#*e&!`g$*hTk|U4M??*Jqan+nar1$P?d#+*Z?5C+YsEk-v4)$~?s2om8P3jj zfOuC5I4+(5!}1W~+I)r!MS_I(BT$6>`3d4S+=SRc&miPv7Rsj_v&$$)fiUIxRztmPqbWWnK-tfHLo@3c zdTU<5kgQCMyN?*wf#Pq`Elg>Zi1zF%=uSx>-LvRXcITSMdpZwT9EHJ2YopyO0_OVs zU~WqU>t+dY*PcM)>AMi$_btS&3>L@h=EYeUHy{bkc={Q#2;)xAM#tfa zs1Kcl+VNk}K5jKSSM5Vfv+EfD+j~qd6r>WrmQqo-%BkHMq3Y$vlFGblQRxdm;n$l1 zD*e-QOevj)daXaue|j19g}dmUau&^_aj0$1kovd7T)!C2wL71={ZwIa9}r;quQQmT zI;E!Ab*46hQ{@Djex%Lr)rvC?r(u#^xcZ-W?Pkxb=m<_&ar@(RbA*2k*D<<_W zCQ8jHD%MBj6|8lDNUWC)K~Il?w{r@Z?gVf(9trMl9l$$S2W!`wpmk{u_9KlzKkzqa z4a#}U$)BY4VR_W^R7aDQL;Yeg)P|pz?8!!{J-$XgkNR_+pV1mV0j=9y=$r3G`=#}$ z6W@FJ>D;nKD`VpQ?Kx4%bOmn`#QgMU@9~1Zf zi;hn>Fl7>=K71!yjvU0e6Bm&+JdEBAzfsO@mBz9P+#I!+>qV)4?rRE~nhuT*lD>Bj|e+{2jc&C68r@&gZc64 zNAb6i*y{&m_s%a;?!SSs3Kzk3doB3-Oa<4FR=|F$2*#2MVEK)309|tI%ZhWB1k z4z!P6g;^Q1kR=R3YhEWt6>(wgqV4EcFEF}SDRq5{RrUL$sq!|huk8Ia71fG*Tmv8a zYwSbU4&v6?%P{;ShAX(D80WI}rXOW-^880xjpBNE3K6@f3R3VU1`5ug7%Sc-eVvT`ltEK%S)eFCB` z9s{jWCUM|L5VDkB@8yS-BOVAkm;~v6wgThACT=WHHsiMiHZ5QmcXlid_O!a76)OvR z67{zKCP?E-2zp!8MpiBYedGJ0PxV0kYE5)+`$L+wdPzsE6*4_fUUUtP#jxZx==BkP z{^=m*Myw~kJROas3Ftf#kGT^Q(Xm9Lwd@m&K3hU1?5nR1b>-^if34M|KAO_b1S!{u zY&3i9Ltpsss2iivg)=a*^FegITZ`JH_UPE{lUnXxu0>lQ%3KcC&-)>$&UtWL{sB=V z&VeO;513<9A#nq(QQIewb^kuF8_%F#FN=_2HAJa`C52x-gN$yOV3+H`9JT@cw-2B&3u%QEOH-U0P`#8b1oqMlV3wLfo5BP)ewl82-9iMC8z-B5P+}5f=Lw{ zaYu9+{RmS!(HEJDLG>~)-Moo$*FRCLeZ$Cx`IY(7qGDX-)Yl?4mG^ilrR~Y1%#W{8 z?|BNdR_sB?Qqry-kEPyEy)(KH8dt8;9GW~pr{0nO_6v7U`USE*3n3@-FED2206X*v z9IyWY>*=SEGm?G}dO~HOX59o8wbM4l8;<==sXHU#zU5ZNI-z%j_vnTi7 zxsv58DD{*&(*1LiG_rk?MSqm(p#kV9@j*IAL$t)X3%3|1UsVzNcUm{1-B2 z{SEe7|3c2m{NizFe!*J40cQci&WkUB<>M7FE}j9e?I?ujF(FazhS(k(!FOvZ#90zR zJ23>@?OK9M?+EU>LkK$#2EAboFxHapoOOU}u#9UfXVW}vvyEjBi7*x%B`y95&6es& zePMg419_$<4VPwaU1=nCl;+Fs(x{y#eLL4l?Z0HH4R|CiC(5HeJes&jbF?~}p;@LA zYFh`R@91Q->`BIuwI|S5@&_iy7E;ELU=?#GM5U!wQrF#0RA5Y)di}&s` z^E=h1qAAG!>`2;7d-^?7dMD&Z_P;GQ%lZxDg+H)4X*VA!QZMT zm=CkLui`WApI8iB&&q@KKtV7I__)JMv$(1Qxmlz))#&=Ew zI_(J|zec&E#;;4rtCb$XS_XPpvC8BRnjzh75~cZRgLIe3kiJ&Wq#-QmY*&-z z<T>QlV{-Rx;%$qNYWD^1 zE=aL9Zy(n;#&X@$fgAE?@{E_|tVX1bbzbe!Z`YIN=g~B`S}wH?7o<5)L(A~as6FY9 z)|q|LLKrICMO@|O3AE07gRaj(D!L}|+l1ySxn~=dy`hat&c~IpRVcT=z6w+gl=bgI zDns5x$Ms*)G4h?%55|z^^$__a^Mn8Q9>BsgAnWI|pdEe<{YK{#W=t`Wy}FL5{!cv- zV{0ISVrqyBz3U45;~FBlMG@iXNwv8`GI;0G-?p%VS?~(ii&r7fDS3PP_Tt6|;x^8j zoE@k6_l6GAOl0IUUglxTZ;+4T4SD!CdJOAan>+tj?&x%zJ9^{?cDEQf29^Wgx-f9` zs0a34Qu7^b|xT9j6zxk^9VQDsFBRUw%U6})(e(tZq6-V*~<$f}=} ztWry*xu2o+-ZV5%w3V#GDqH%p5|G%}ApLL$_-B0rYsM=`2`McSDuoK;R2{+Jv=r`( zt%Rv{64C9ti}+1kI4ai{A^F}zcE_FIYS0bXv{~GkyTj(lStGT)$ECLNIC+idNPSc} zX>{r*wMAQ{W6pl*>-<4#=Ppst{zDq&a_B5h$q2oFH?CVQ^Yn&Qz;`wV^p8%+XpjK* zIg7ycDIP2t)DyRK2Y=)KkUp#zu(*1}X^Mhr(A**a5YAd}CBGu+`ae&R4s~1lUe_jV zSD^1$PqdyKjzP_qVveRTvBL`tZCgPFxgwNfaS!EsHdJ~0Pgjoz&sJ#(i7Kk>O!fG3 zf2!rpm1CF>9e>O~R=l0ZF}gIwZ(Rg&y@OJMRbAQ!sqEO;u?c+{aaO7dOijF>`7oWvT$?6pELs_uGyTlm&hbcdzyfHS|@be z(2yOxNZRLX>DXQr-GLv;7xX*LdX|u1>5$F%*qS^^-?{%qFYv`Ig^bg?Atw$Y>)dgO zYL)@n=l6j_PlfQHMDPzL&onj%%gv&o=?7`19!RslluS0U0Qq*D(y`tvUGKt3V~eKx z{R{eEwnJxNPfWbB5Zwj#V{X^mm}qAz>U0&Au%N3l&2cLJVS+00e5q=@W`&wpe3`Oa zCn}Z+Dr0M)$_ZMGENGNRpEVWS3+_Xd{WG}xmJyK`Yl?d{Kp2%;i?1u&2zQ+*@%UVC z5m#%tsD5v-2u}J*B!|@%Im3zxZ@+_(KA-UMe1_Q@R%7g&OPG?E^7#Edj2(3g!w(+9 zoGtUvnPNq2d?9onydmAja`O4)mHPdn+^l?;Ye(yXfAc6vS-g?vh1VeM#s}y(k_jh% zhuDdqA>;*N3~>>{M{EbjuhT%+8iV7Ym+K3wk~i1s(O<5n&wDE^HwY`mRz~M#Kr`Hq zT8sl7KIJZX0s8I9FA_dkMND@t0<-&wtmS=0+SUHz0_!DGcC;6X89#~e zKRXLwtxC5Fo0@%j9(w(sYjpWC8K`W}>upFhHFs&@GmEE{GrjRsKvZRqr^OT~)(4YM~T5|`Y^3~f6aco|vZM@(E}QIqD?R;9|uD5rCZ>a%}=x_W4l($+6j z*@?4McBP@JdY(GUch-$YY)u}N?=N^%5h8BJR1sMGH|kphL}(E%B7dzV z%sEv=a$EzE{<4ZF6-}D#y&SM6{|WAM!@zj|(c`}{8{-oKm2snz(oZ%}{v-7Lg)1p` zEsx4*uosP$c~IXFOg^)xoYk)n&SDF}l7tZDdh-_I>jP6!gq!j)aTmx1TM<2oUrmJOQVTz-7e_=mYK@~q-Q!!d~6S@qw|dsF8ziC&1&GLprG>rI56Fr*0*>heL9c^!Hpk=f7hR0jx}Ru%AF8fD zsiC$OtEr-zR92G$3ajjdgXk+;3LULdxjSkQ_JVsuj|1bHm;2_XNT=;Lj4OH) z(^?c$F@tKW_@i7Uv=eId;Ht_sFM#GZZq)a!l)kK@U}+^G>h32fwYHMj-Kl{%HNK9B zxmH_5mn$LU^g!W!M>&{z0h}ezf;Vw5c{Fy=zo$a_9Sazp$d|EghD9kj+126bXnBFoTfDPrKam#p*Z_B_jSy9CBjk3P2kzRf!5r9|nqr{RP<@$zEwA)0iSHy zF;Bq0Fj@rfP7pa$mWw6DQpK6Mn?%(&Yej;8rnq;fvB>TB1zf)E;7=nS)<<&3=ta^Q z7>!xKZbJKw3=C2CF)^%w$_cEXQi7|g(PJy9n5u8r`(vIAR-1uGK z4+UWUiV?=RpD--r3ffaEsZwJ)sCz|5sNd_)QE&fPsx}N;tLD94qbdzrspgegtc;V> z)sSa>mH*GmDjwIPe=GyXg;GLx`bCs!wn#kewOWK+*d?A!+%JYzb&Jtk4Kc~6D^kk; zfbiuT!I)JK^Z{+ik2C?jRS%HY=rX$9?=kXci+Vh|h6<@(Rry<0Qn|UIN(%{88PO(c zw;xL1h#<&bb%8u8p+ffWAQE$iiqH~H;W*h}IBi_W^~I^LUIPD=8N_Lxb0Z44@u0Uf zc6+4Nkq-^=LHbITLia_E-WLOqy`7Eh`b4zG5q=EaCC!U1xUWb!cxhIZG4mGqCXkME zzn}2A4xdq@>fiHVpZC#UzBOgRhCN0szmD)m3ecOYVaso#S~wm zrrcksVh4{>iFv|RSnq<$Z>8B@V0G^QPTEM-{$j$$St4X|iU_czicZ&diK{#IiZkC< zil+blBD8W8S7GbG=+Oe$>#GsQz0oui^Wo#}!78Y95fw79IPvLn%HFh*$|%@G>1Qgc zxMn#R61V^zftfPBAn^-RLTF|=Vg0R**m|YA&}zqs^{t~tzlzPp^rgi_?&f2le;WqA zqamPe+DLV&d4$>IlyuaokN%nAXr{MAZ=edD^Z=xghP4Di};r9L~uQakh_P9kgdIi1v-n^ zSA_jLJqPpjJa8nP;_T(TOgG$?mLVL&md-^jWFyUwRuX1igqEhiqwC8U^wsZzEdHo8 zV#jd%AN?R}+J1<=`xxF^3yF~L6-4^F>LR^ah_JVM4pHA1gZcah_pz-qHCD!-dDXyk%710Hk3-^S6!g4jgu$OUwvHh{l*OmM;DXTEu z`WD}lo~11dQ`#1qZ$(y8lR`sP%I)&X+98jM-*p6?CE6g%OpkB`Mng!wn{ev4;=)~} zg}BhYy?Fevs~Fw+C-Inr2)SKKSPvWrbIu?zryk?%<8hBUpd|UUoTT^fz>NI+sn2af z>w~H2+}{l?9c}1{y(`(#&gAE{0gK%O8QBjZ^a#zr1{D(7>q3OQTvqfOoL?kPJWc(f zBd~M3GtJOmXxRV4nDmk=;eA_Wzcfr8h&NQt77Nvroy%0GFDq2^hGpvU`6QJzD?zQ_ z?NHw28p`cHgYGR|k>z|nYFu%HvurK#q-}!eley1kPA;s%Uf#znwi~^t ziX-c~I>I@x0{QDa;NG84B+aTV5?}q^Vb)r?vxW*T_1v0Wg=*kv*_$$ zVkV18mHvqw?Yq~ZUS6SAd>5ug&n7Nc54GQmB7-4PxAx-JxEOHkItH#EZ=hf2g2LT4 zNF@BPpoqF0And_+A^YkM^13tuwpO=!+i*0y9YlNo|L|+)$|`CTsMsg{2op|I7k189 z3wJG1?ubR|`R+s&yvtBA!^Wzx`MP4Am1)~-ESvL)dsPOA6NDeqS1uFn zUoR1|#6t1=jD;fn?nvQX-cWcaY$Q)oJ8s_IPiKRBlLqF&^kMl`XyJ;=+P_%IJGw?f(iacV9zB`)iOMwHho3 ztAO#_4V%{Qv$S?zjPVukqhVT9)VO*oWk(y8wLMnJrG3@Q2mRIi%du)wwf>5qbExUq zP3cihRQTXeXnpQNSCKm8t-T}Jg<#Iw8~{0|sxZ#Qis*5_2;JRH9I9^@dgoXX^b6Io zA2o!2VioA!8gSNshqMP<(0a8GT9zFqPx2FtyZRC1#$HGNtc|FZ-i+3LgzFNPqWdmK z)+UwCX57wjtU1eB^ayabJPJA8-#|wA56HRs72?cXhwS9;*&U9XbcH zku>6<#7yI#wNl3*OrKei_!FIX?lXhVg>lKSwTJ!s-KOuq&b^kt)K}L7J4PI`7J_vm z`9;c{2JNRz2)n$JJbo>~c-Dz)HqyuPbilYztI^zl4fQ>FmE~l4CF|8x>+?5K?>BN4 z+bdGBZavicDZP}XaUW%Q*iFT3ZKA@O7E`%d3az1w&~evGUiKQm_P>LEql9RRJw%Q< zNW}d`+Jk?Dh@3r4So5?KtU418dwXENQ+_TDm-++?rY~B8-n_RkyT@B}q&`Jgbq}WP zNy8L=2wnUrYORvdv9KPx^Q4ex?<1Y{tq5TwRzh~CY;X?v0=s7x64C1mir8C)#E|QG zgw_hda8o@ylLxc_!m8yflb2?cbj&V_*3@Y9-l!*OO|@(amhB5CX==>f)y?Y zA?Fi`TWx`?zth25jQYsQzagjQ8}PqA1!lQK!uxMIJJQ8w-Z!P?*B;KsUt?wNn>R zX-oSn3W$uZ5rVgi5$t26aIS7E;;5GfcKunz6mBYvVV5d^EK|%Gkf|#%JD;B>H7k* z_mdB%z-#bNdI(neH>A%!P5Hl?{Orv@??QT2LL{A$C9GA~L4Ykn&>#c$c0BgXUxGem3~)+=7rU7s2g24BlSJ;P|%*ofW78j%|;) ze(3;bo5Hx!{ENpZKa@1+B~pLTLYgb;OV&RRJ-g$U$GF&#YjghNmaPN9b)T-W;Sxwp zUj^Z7h?BiefvnDRfF5HF=3g|Uo-oj(%|0xhoHX2eBhg=KBgSt#joP<6nD&{xRDTpy zrEZs^oUEeW2h~;e-bSjV=Izwi*R56367r%oevMfbjw5S9bJXSiP`fr?nn! zdsI!ZqU3!#P*&vTttlo2HxiC^HquUu)2wR~xW|0qX1Qi`USw$mJ3CwY%)WH)U@OLF z?!#~(4eIuM)YBJ{uVw?LWiQ8c|8SarYvje)Owall=y7*>z;jO;kk)lSWc-!|dhv4* zw95--@7s`emHe?4_Cb7~1V|rSo$@OWm{Z8V`15EwbMzy^oX}G0$9vHFh0r*w$};%U&lcDgkg|A;CHb6Nk8oegTG37pkv^4?$Psq z^AhqP*Ff*CvFOaVn>4g5n5%t6d$&9)F`$rQ)k~}Fz zPSy;xRr$%&@PIV(3rBswltSlZC#LV8hn7vhpmBcyMui!eeSZyR>~^CofHd41ZO{yQ zAX$T_9{sD}_PmwAet9%xpIZ%V*lvi6Bkz9~1>VmGA*}Nzu!qkDSCM|;jBTaU*-^IwrTgfkQHNrY>Ib`2@1=^qz zBC$?UF-a>ZF6g1c4wZyAU#QSWU88dhQ^4x30LFwg?%sBpuzaeAb-XOC>p40O^+(Ik zKcRj8&*-i&4IN3-F?-=+bS4eQbS_X|MsvBRg{3*C7x(6r2Ir;j-v_xt8zKGuLWt@x3oO%Tf$P-o;N`J|(SkrOt z2_M|eG#f;4_9h?rOVF92=e5B1yeiN+05Es71!q?~xMY1W0-n*&$n z{U-JHAEkD4JU@3oTjskM?yjxU#FY|NPyesZ5(Z;o*C3urG&peNp&3%dW>6nS|Q#aZ{r zn0bX_7u=-laiUJkY^b-c=B*PN`kcax)_}V(m|UM~k`J6Mz3gw&PF+KZ+s~D2xQ7Gp zE3z{*8ojQML!YLjUR}dl?|H1H>CFCn1=tz06TT|Npr=~`pP0`we7PbSKTIcg(nN~= zVau--a+O~z{YB!X^Cz^pD@Q)OsrP939AG-c$@WMOf5obCE z#k(1a=42gO;~@0J&!utxv9z{6k^Y7G$;exr#G}gOlrKQWfxo3$YNIrdR)xP0Lld|( zXs_O)e7lNrxcoTw&)LLw6~)PgZ*uk4sgqK&7p{momRf$5$x+$o&y54K%*rbn=+3B^T)AM zyav1cS=i}yTG<^-EAu#N-n<{A-LoROF0`TB(bFkm$YR8-jpW3}QPkBnWJhkG+l|-K zxG_t~+}#gf5AIfuTqMSPks|h`w69H&=FJ80>A*R5EG_Lko22&g7wIhBiFr40+Vu~l zSvN)6`QA&t&~B-fTLOJ&U8x^V2QQl;9#JdLNS`^RKN?N$yfNTF2-1HZDgX0n)^iq8 z&U?SI8s1f|$Hmy#*qf8LWB&HqWGz=47RzuyJEcdgPRsUhQ*dvyCOVI|OLzVB6uMf9 z^pR~Tv|uDz$@3`LS^}L^9NFV`P@vAw=tb63(-XL>sdaGft59HkehLiFO4gI#;6e4X zG;VE@)(BgQPFJLp>5{aso|eYjb5dKv(8;fp=C|(PAryp`xquXFi-A+{%47a?0e*P* z5^Z=AiO}!LRo2bgxL#~@oXhT>8`$Ujm7DH($xTK2q!s^UO$w7Msauwaq~4iCQh44Y z)_yzB$rF!peC`dLJYg~JMiiTSb11RsH>o96q44uQ6gzDN+0*w^#LN?P4!q-WhqjY{ z-Aam%ok7l&{uFprk6exLeY{PU#^Qb8K8=Uh*sK^)ucxwdd{E{m)X4+8vflS6wtB|0 zHpIiBA=z0x`2t^>)k>dM0)7)YrS&WkI?}Y{eW+nBP@k;6y~sDqB)jBd3Kdz8yu6+Q zi&s$c}{}*&5x_uw{kXn2f4vl$OnJ44APZ4 zg1psp;@u1+*YB&Se>saHCjk#QIFmx1r&1u*SaOX)t}If4T>e~SWq&D+tEtJkZ%Ok@ zX~dgg&^l+qgXNVnx0PnIXBGCItIzt)@@!N??tAq?*-J5JYn%yl^GK=fJ&GFciu9YP zTe@T?SEjn;|D`KAlTAwDxfF37aWZioiKgo);ou@NPK+h@Uf`a)8j^clDR4u-NnH<1 z>t05(2IV66-&w&WPEB^JZ&I{)DV^b{n{!l@qQ}26qV+~#GZ*1ix+qZuD+k4~R6$W@ zanN?hDC4ioD*UAwJ8LmxmnOTWtz+-a!|W`%&e2Q%X4j(o9D4|}?gJsdedGd1A3w?C z<{V^e*Uuc^Y&%CjSk9qFgW0(Mhtf9+c+a&27vM2CT!qPfW-vK#`cY_89|}L~LaxhA z!6^Yp;CJvNqN)1>12G=Vyb7>{QL*XM^&ZC*Ya;^|DF!~$RgyrTA6gC4!L7I&_@&_ZQgrnw3!EP zP#7BbsAFPumY`TTFh;z%?XgD`LT^}2>NoJ#|40u%hI-1WmWsol>#=#9!~M$?A9a#2eJ9~hY-hCz`ZSYj6+p`X;!PkcIu<>zFXok zHqMrgc~Uy(BPmcmf{c1uNbmDRI;Bs-b1WPBdl&gOl_YVo5d{i$BzuENnztE=F1bj5 zHv)OGSWu6f2VTWzrKJU@c`)V)S3A=HcBs=T8Uc~@jWdGiWYs4Usnf(-7is; zXa+xJg3jg-X~dQz|D;}&oHU)r-2un(=6o_o_XloWmwX>E^Qsa-X2nNRJNY+syP%uvbJX;=XNG?ohU-)&G(4+XQjToBfLITjM!5WTEL}BWZbHp0?VKk z@2boki^fdvXri>9BL@6;e|w+~4b+J-7rYGGV}De7=e0_E zmjEBN6Uym#PC5H;C}S_Q1L;tA9EfB)wm5rj%w3DrVJor;+r65z`Lr2(*Lc}#gr6^} z!GUarzzxXA-kP{mJ)T2ja9KGgHYsPv7I^=yQ`XiQ;AKQBaWf}8Ix;JC;_Gq_k7Ee=a;#wLyp; zQWR*5Gpw4GLL0J@{TXlMb6)a)EkN44!sP5LPFB+rLA};b@%0?mE zMb5Td78y-Lf1Gh4byY57(utNIpY z#O+gt!rhO$uB;`0Ded<^l|J;6(pw%>PPdiH9)r7k89pQ(E5RqCtVdsvN$M4bz$bF2 zbPoI>HN+eX^Fyr919um3rDA8YJ$)&BWFX>qKQa@#<41e4Vq1`*)rZD9hOCUxT2ytD ze@+F`|1Cq|)i_t^G5mY-BTnR@z)_r6Bx3!ne56qkvOkp~|N9Cg`c)zCn(ENDRHyLN zs^sh}M`oF#QlEJ74rMHwr|chfWtO}X6io*vis!QtXUa+a+#G2vO^|k@`%=4`hQx+QGJBRM@04iF zJnND1vKd)7yojsq$@PbqyyaVy=+v70tJ{z}(3*V9o0EHJLtrX(puLVEGgy=Si>i^a zyb9t+O@iGpIsUrjjj2Pv@u*$T)hF}c#yGns=s_A_o`U+~Knw-$Rii+yk`(#~eqzcq zDgON#esmqANYlh)^1GncXC>a_6XhOVgu^8pvG?IX)T*;M`S+DP&a<8M*k5?=!lUdS zmB7BXz~M$Da zeARA(vFyQv{gdtT&h!;q0fuQ{`O`_^y0F95|n7 zu0~zBs+JVH7Nd5wrE&AQ)DEM5yMy=aDM6v^$m7FmlWRpi@@tJLe7PCf!RF+f*POh2 zo05I33HhfrrBKVpqz|f3;;-;Zl;crS-|+#uTXClFW83Nn6>H z0!jFs_TWq#HzKW0eX{r0A@77}@*hI4@MlTf@0?`Ud@HrTeu3W%&h=Is_*y>=Y8#is z>*|^EjV{E#&Gpzl`6o_zIGyKSUdD4XZ{?=9_p^;&ecX|wY>q#~QHA$${9l_nuIw`K zJg0G}%RqLg@4(u?x-9xdu{R+f`?q9ee`jEg$Goa9cxq-9&UD*$=DSN)hWij^S8$?i?4azPHEM< zCH)r#`8I-oo(u0Z8+yu?Rmr*?jrzSV8E0^It6Gz{M0@g2?nt2*ok+C%0kv@l@|JE( zE)QY?HzZ#=4~0g9@42}uc^g$DchhR*+FcV~jQI79IA5(PW}{8Oe{W3wSq(^A(17%M zI+-_O$evi8M7c`j$9%wilAH8v{{qE4C$01Iq__+`zebIqxZXvX-jm9xiClC78uU+{ z;Bzp6&y`=qp|R_sk=e~rKOKUW_$bGP51^ml4gADnr}Z)(w`Ur=b3liBy$xFv>$8t* zuo*7NuHPd$@C|dB0omFAzcegbz5>pFL)mGrD`zTVY59;c*Mtzu?vd&yoU01M-(^1+IHXe9y6@z3EJPbS#Byb|U>@Tk?9F!%Lwau%c*kzrdM>QA=Ma zi@qR=Tv@>t#QA!6;ST@pp>Pc%s~JA4Z-@yCh@6r!Bt}J(y{85_kMTxcl%~K-aJ@@p zBJ1@NsaM_wtN=AyazpGTxDqsXPFGHcTgv}D7n?OC2Tt~6-$r;-^aBU>=XiFvIn3ks z2RSkny%x+i85>f}%GkkPpj`G&M0 zts>rXyVl^6v?e2eOA437{cA+z8;iHx*-hRj<;gl(0-CsDz?6`~a+V>zN(I!rRiFot zCT%T|HoP8ke?8I|v9((rvMWe3($}U?N>#+#vSg#Kv!7=nQ8X;=KMqQL>QE^XF9HWD z4A0s+N(&uQ(A~2;V`cWcyKw*AGdMccT5wnQa{q(ijx9gIzQp64a^V<99^1#E%$qrM zX$d>+P4G4PvQrQI@taLK)U-O=hoYc8&K`8lvV6Kj>yu!-|DPF;o060N}J-t<`M&Q)c;O;Vb+9^RI1;A_7qC}s`!h_dC-OD=<--zntY z&(a;rK%vsvDZCcB+bK#;;|kH8aybFUG(mo_HrPIC(PXid)I zX5<^un9MA733kL})j>@bRf&9EP^(cn+}%oKo~=rOWwpq^9NNT|h_6>$k~i3rd}9!^ zjV5H}Y6vU{XN-DI0~gDeOT!z*C4R7?eatHFV zF|s~eKImI_FXZU)(7cxXh0iroJg)f(_GV;`ynPUH0KIIRl^jTz&iZlaJ2%5CYMTxZ znpzwRmxf1Ae%7XEWbI-))^@&A*15lxHRXiTCnUfh@33;_A5_|gL%5egWu8K8*!2Y4 z5njUM4DokiP&xB}du{Bg%)Ad#7x-fAewn2GcDU3g?w5ML2ht}aC{!j7*_#WJJ{vU` zJf-|k++;-~zvrw@t}4J{yLrg>N>cbOQD9I#_=+?p_dMhy7jo1No%~Cq$vJ^MRHz&o zbrDw@7Nfw;5}4VRC9%FDd3#kw-RdFNszwy*+KNI0+mLar6`3FLIW1@eyaBaEpgQ@+ zl_fKOVe;M2N?OWWiMc5Hk?G(Pqfe4WV(fS6l{wt6jNOlvR>3Li^Ed}v8zNZhfg$SYkds3Ur}s5%fsS( zI(8m@1wQf=I+e@He*KHms_szw^LS;g*a_{#E@i|YfDh*xWxf0hd;`RZve%R``lvGh zjDz3hKk&W5ErlQEnf$ueY z6gUP^Bsp90n zkNDDOm>a_~2z&vbL3$`wfVfPU~Cp_QARwvB2+az&v8Jjg< zBA+W6;DonZ_;%^#?0#XgeXuW^Biq8812yq4)mifwgI9-(y`2$jDhReqJb=gKX=Qxd zudHJ5Z<>qVVZvsm`8O(~;(p9*&noRPxJ`IJ`lJg=D}4;{V=Fu^N5khXJ9zsC63q)6 zv7f4jbS%V&TF0e!1vS{Bf8qU*iOf$q$Y_<1`~?c3?k_~XhKR)r3sE>#VG1q6&*v8< zZ*NQ34S=!Q5vw>mlH(03G!#Alm>&6A0&Jqln}YH`zY zOW6rbVt13yj2%p@rv>+?K^YFM&&$r~^z1ZytL!EZmG$kavRfu9yPu`B@G9lpT(69O z)+*s_0;aJ?i32$Mx(}3@^{LX<+*hV`TA3SnE4{~b>{%$JL~MTa$g4f(l?qZn3fy2v z0`l5p87?TuZkdsU*F~YixyT-n5BmQ+q*cY8&Y2T;5cm7n%;a3iOzw{OGqs>u2_U~; zD+o-sFokawpulE)F6r>=|KuiD_59?&g3stw2{I3rgC|%G@_#^WF$%SA4Em7Y;Mda{ z_1yDT;7c~3(4d;ALGU>>!e>}CD+$;Cq;&}x_^ORkKhYXo%#HAQj}6)*TPbtY&+xVZ zzc*cJ)|#~fUND-IKQCbKsEzCk?&YWk3GDilxqra~%mw#x!qD}M9+{0@L)ggDl*Q%B zz;ugXCY+l?^&{A5f!g&W`i1!yl%7^8lXofo(?aaUn4*m3v!F*pf8A`AGPZA1&^If4 zDq_%5*FP7gz=nc2 zTj&$h7c|v5prJ<3dkvVu?&9G5pcWb7Arl@Bc8CAp>xtNQ6e!w?w37`So_EqKXhd150DuIENB^ewd}D1DU=IL$QWq{r`9KZ#ussK=)MPia4YfIltzwqwc4JbhW2#gPX^ zHzkgK!p?(yLGzC7v2%2hdau3Ga9xsC!(Rqee>e2W+yEa zfw-8K{HZgM;RAL&Iv0uGaE_~igICKzdI#M1#_;GW4ty~gv7vk+3OK;$H%3AKS)Gh$ zc>mG3t5rI~>#iHP{aqe17{W4WTd$0As3UWP@Q25e>Th4(*=!+$L0_`2YLs2%KGwUd2?c5w2c^&DMe0f&P9 z7mU40=Dsf?<64e)B9y?o^F9$38b_cxw>NuxPO8>DRJl=7>55@u) z!{=EjQ5m=XhBxqY-n^dh6_DH=9if*N2>?K8{J#wPd zUdK!A-4*a5UP-$-?&O86evVX};fzvpf_lUt`H1a(vPN7nm-5n&d?N;Rd6SMz!INx_2$l2PK z#BJa`3p`~1RfWvA#mT9jo$R`|q?Kv5G(G4cYV?7g^e1Jl|5dpLq-THSVr+SI_D}7` zO|O~kJY5VAiVbX!+QJbX;W_mX-c#NUtZ9o_{Kv~5(Tep~C4h}(g1^9RWft78oE5+W zCQMfPwgJ$Bbq80Vn9}=H1Yayhi7&0;#SQGZ@n|J>uT{?9$KlWZw{nudpf1Y*KcZ|b zHe_e_k7-$dkKQ6rqB8fbMK9q|=1Rna1wM~hR0y+{@lxx#Uy7|EX>WNWLk|%T;(?j0 zMm@i!B>CcsqxLKcF9!5o8B3BqyBN3#MJRL#`Rj8rvP%_$elj=NFS3!j6!*G0Vn74b z7e+yH{#Oz;aYfR;0{f}dfUIx$k*YbF>snBtGI%MA>OhN99q}7?x;^sZxero%9*}lB z^bKx~5hF_~{p@ZPb_vX6^RrVUhFw?NLPI!_wN&uYo3V})ZUd7kyp^@dn>pghdiI@| z#r^{QIo!1cYxT>4FP?|Z{~>;)-VL06j1n0dDEn@0VB1-gdFgRbOiBrgo*Cd>UmTvz zm4O)y!tSOx}`jfD@ITe>&nVW|r;t zCW@Sg;bq)RYN_LJPmf5mzyle6lZLPZ3Z4_?$TZL=%|KjiR{@@$z!;J%!E3cFxoeas zyEgEGcwjo~3zDIbkxKjvUNYa!O( zpf~=WhxM<5wb_rA5qS`^8=o>CUdLR*8zXkT_n7IsN;}ggX-~Td{rtbu{W%|*|6$Q@ zYLGh)*omhaJV3#Ly@#`q4dK6vo^&zZsl++!oNxA51X z*<&8OqHidtb`ExTu83NxHMrWt;2jG6pMm^-HlCa2+{NC<(06`W#-V4U+55!9z8T;6s@f1C{(dFDV9 z7f{ZUyUIBDQMm?YVf`KQ+g;3lcH%tiWoNAyF!C(G;3u%MKTKC<${)bI^cd6k0{eD* zN|9rOv^(CAf!4s7R+J_yKY?S{h=kUN{0Grf3iIqoldTY;WJf1DnD@f&IVbW7Stc1!)v58!ep#@Nr?$~tmb86Q(|pjk=w_G!$4 z4t+Uc`7Dl31m=3`XS`$hsvg?QvCt)^WC6Fy4=nIj71kcsW$y}N@k?HvB5GEJ7W17Io=g%B+x<{TYEF zW+}zmrIM`QDaL^V1v%6+C%hxmvhxEl+RC%xAAx$Q>5|&!q}$kM5-Y8{yQG%mqqLKN zZ+r&cx1$}o2lt@BS%b6*z0os{BKyo#isBg*XflDqmXCb5x{_U|HCdt#au8;U#W5fF zR+fBaP(uxSE&t=|SvlWGO{Rt)WjYe))04k$YSJ=)lUn?JX&tep!#kz1XO1)z3QO?N zgL=cU_&zQvp9{R^H^taJsR_HX4+c+UJ_qt`fS=tyjy%7gwbt7>Tw)#j#!O^?wzjOT zZOD37oxQ1>gPW0)tqn(%zNkOA)n~BFbvZn$ntQ~?XzbiM7b8vuV??nR@B(>^Ib=?c zj(skgLhdeo*Vm&}{fC&LrpM zL~_3!Lf%fD@xD>pWN1O!1rOe@Mh^Pf@b@&RkN%ZT!e^;Rzn1!=uoM^mk^1z9Qk!;O znzt~IY%y1gdxkU*KJtjd5kb?{QW^IZbRWpo)k{Jf*^o`6H^=3g#Zhfmb9Ck{z*F~d zM9g;XpK29+p!!44T@9WIjo3A^5&I+BvG-^uwu7gYQwgzkQhNC2M3#fWWP z62;q`9{Uf}un!7iA4z48{-J`h&ECoxxKTL^E`nEq+9#L+GlLJxdps4eZWsHh2%9%@ zvy<(aGF$A$Y__cuXMPI`-;vs8*QXwRVPk3J`&sJoX~?Nqh5XksCz0Jqd(ab}7(*$L zYdj^Vnnm%(Li9QC<_<3;QPP7k3V*YC(uBX6HQDfJOEcido}`Q56Yqyt{Ya(do}-K+o3Us1uF|qSQO@?i zlqm40a-uV^y|5U&PDjEw{k<~X8k?T>um#@1xxukLJj z13z)hJM5VCDtlNN?Cv{?JJ$*O-ClcyJu^WJ&W|1NWu$R%gVcGsjZl-v{^f}i5Zj#@y*y73gM=R=Mi z0W5R`bTNG?l)nw$dsQ-Wp=Uq)PU=VQOTFJ zxMsNjX*w&XkwT4-oxP`OaQI<+cyaXQl=m|@Wz8B6^xlZc-Y#xBb|*(2p37ReKl@sg zVqc&mTb+BcSkjy|Re(jtCCdDx73OhEj)~1>JmTb!*t@(YQPiC8(Z6+)#?Jw%cbtT| zs{;;GdCUj1DqU}cp59V=#&z&OI)uJ(kFqM?SH{xJEUfGtejLGek2~1^xk%X-?(++L z&C9ES!!sIreW%Ahwm=4YM8HoFGqV$Q;l0p=dTdgdW9_Y>|gaNpARr12#vo#rdC+olV!-~8Cm zF+Ilo8iL2t_8ZZ|qqm8;qwIs%m6rEsB~C9; zcH2lLHrx-2=LZr+>GVNyX1>Q>Hb**hA4+>qZZcXFA}z5jiSpISz0*r(<=*69Ift~^ z1r)b`5k=BWIycOuh`QifuANP>8>V3{H-wz)?a6MBdC;^d3Keyc(I5l7onJ^{+R~VS zd_Yo~IsXPXuTpJsp}exXtX5{mlgiK^Dfg{{?El(~-Oa~>H#3+0HoPZxujiDt>pA%o z^6Tx{Y+nK%7XFFdo5phV?ztR3H-xR6W!ZXo7kl~e)~d_}Z>Np2`xjT{*i}LE%NdXE z@kzThYTD^OcpINZz1%)p7}hb7k`M1RjYmuy6*Q0MR$5OJ`;+IR?u$o{w-J2Rb;@Y4 zQrW=<%KQk7citTE7h43y*?!n3^f1P(*-{#FPs+e-XyEfjk{^A7cWzm-($plgUVC_1 zjKk080^6EJp>osE-;Ji^zh}_ghO^;SiF~`$2M_O9@(;%RcnW&lk|oK18okTN)Zpvc zQkwwo%=4^>XGtFMvQ?s8Q7Ze$QKj8U2FJpMnzt-`lG?Cojpu}6JQTlJ?us3Bcc7guo z(T=T@_QE)DqLI5t9)teoRg8GM4n55;*vY*ey#9TO;y>^6{ZWXCO_k``PU(OCsO;KZ zm434#bo<|e;>dRFyI*@uM8Cy3t7B#gkd9pQrMB#n^xtq%pmYiJ7T^(etOo63H2U11 z$X9+U#jk>wYJs`nZB3?>(bFh$*-ZF$&7#{S@$0qwksH{czPTQGv!W--QwscE^j!n4 z08jH{A7)+b0XparZSw~00*IxDcVdnQ4CzKbc)=rIZfwVqB~4EGVF^b~*u>}Vug9C) z3@?`DYz&zL&T3!wei_f>)QReQ;N?W=|>1nPg;{j%cyYO~KM8Kb_ z18P#d!;8!0|Ltd=ZhFjzSD>HiChdJ*X)eqLKX1FX(1%BhJZpnu!-k+Plg}3)K3P(&QJ=GlYi}9G>48)slOYVw|DYPAL_|L**>`R4N z_fcs@E&z^G5$81z`-DaXHGP^g=%(^j0VWkwn%$9--75#8Z<@~$sc=T~xAVA@+c={5 zc1}LN0-B~V>_69wo0eU`=lWahlt3;R*^Di|tei;!r7t)KyeS!Tatk~Fd=80+V@&D? z&pyl?O3adC<{Nm>ZTFaUQ%SqWX!s)5kfQHy_O1%ggm+p zwMvEW*sYWs`_lv1N!J{@$e&}xwfr7?b6#n^JRr3O|C7#a^z3!OyDf|P(|o+efsIKo z+@IW8ro)qW7W4=v1@=s!z{SyIMVaLDPbL3%>|<#S?bt8P$m#~1U~@@w{gQ=@3OA*b zXNA(6o8Y#w(lj^j6?H>fn41D>?N^S3GM>JAmMvSReyiqykB;_mGj(Yv!pq%VV=o+ft3r&VUf zZ}0|OgIVgQV?r#%4zQ9Q5uM&6%4NXL#T6dA{}OypFQjolBL!|{AYXCRv$Hai-Vilu zrcU7UjE6VlG;pLwk(Hw#&Z9T!MFx@neJp(9$CF4HNIvvbuHV5|=~@ais~qsqyDiPM zs2{FYK`zAX;Q9O*`(ZVB^ChUj<4nM9@^PSOd3HB#!^xdZPF}Z~=jPwRMkt=68tq~i z`geQF1P=E_KR8_S{bIoU3@LRsy@%3miZI6bN1vDF7V!nY-g4|hEF zLDVc|e~@}cytnlNd+|z0YxO*772ggIMg02mRH$QTVkY<9V=fNkEDpzr*LqMC>VrCA zV4`_z4R+Su_n0LPdBnFdxKqVr%tp67&R;X&qk32R(`O(PHAG-=dh*@Nj@=8T;oAuA zQ<66xE$(LYuI_ICoD%l z6fNy0hzk)}V#JDK$`~934)c$4+UIBg=34B_+=CN}%wjjK;4t3a++V;mi`d7mBWpSC z5_E)#9l`To$fE9UPB^;@zUDr5ik$-g?|{;Ve!@QN3h-k_%yX57zY}71^AL878qywM zO7oX_(sp#I-|Q!?7r%nn{kznw?34EAJW?-RS?bMo=(m1=2H~YgJQ@fcTh{;fJmP%( z9`p2j?3cvX{bH?0d}-h@8?E&?#b-!c0dw2`K$@YD6hr@z-j{-$g(ZRE)FpAJJ6YFp ze@6O%xk2MJstS&3OK;PZbhy5Lh7~9-)Wb?(*^d>3%s1YRe)#LCHrCz@<;ciaC_XN0^KM~ zJ>dO4mIB+x5_Adher|}qvKE zVE&(#z3m7)(t2>jy*Vu8QjUGGp1tL^v1lLQ=pq|A@V^D{)Q{yr*_nt%%=Xm)X1*KP zSOpw*Wk|V7rDET`cIM87x=?2_f<|eG)LWK-u5LqZ{b+s63-&5~^>bzZof(?K zYV6HAisznK$mWcl98mF`{O@*dTGGPIdl{!Jn#ulsaU9;do88PD`=6&^up*mBBUmr? zSsBT_*~r!&@hU~xlT#_N_E?O_b{;x{!?nff?;fEvsqZO*dTJDS=EtPb?-h2pW6suZ z7k1VzlUg$Fm1`Nek^SKdJVhDh+_b0dPUH(o@>ir$|S?jdy!W8dLw0 zCjKtxJ-hiDP?zx%u&r8|ce=_#0E#})9ws2&xlWd<_%I3D(Y^k6!V!|pE(*)-T zylh%pwlif{;@)8FnVtomVpos3f1$_xXSK)F(n#Ic3%r_B(#(BLiUjcQ-prO-)KY1e z*d&dY7C5?#r8RVgG;$r1x?m}q1D82~nqu-e?C~5Z^~o!x_4T-Psy~og%M|Fmu1USs zuhPi*2z_~GZWj5sLmoTIVR@~KDEf0rn%XmD;eDJ|iwGA9979Z`w{ce{c|IU74ZR09mS4|G3z+o^dJXb!h>}9G`6ni24Co#3iXYG|CJB(zIxC@ zUd6s=^k**LF;U4(6h(6&jyJ;|+=d?gL>lPjaRzN(OQ+v+?7ZG7#jK_1Lsv^C}T_XZgMWg!Y71Ck9c=luB$kU*neXg>)?}onTAEn;{c2ckf2ilJX9}GN^59>J4Hjdr3 zm$TVvDg3}zab(*i*hhq3X4Yb|s( z;SH6SGjedv=1LxO}=}qo^jIt;gt-hL7$+`Nu$SpsTDX(dU#w&54|MEDhPQUxC&BEREk*O8pivw4C3idqf0zJEkZ1(_DB% z;1cvtP1f3VsN*+EbI)DrwlRA>nxFJ9A*s)>rP|z+`BnwrtvA~>Ir4^_*ZGIF(YHj-Laa?V(v5*=z}mufPlHf%{T$C2QyW zY`$5{{$}gh-~SM1TU*)ndLXn4RoQqR$-WJK>;iw^L&M>O_&y zz@vXog?xwk(#AZ}n*Iy+#D12}Al#9uucXoX4DvhXc;~-KYyEraeV>DjT6h~{OJk;; z1wMg^()wqEG%lW%_Pf;NBFq4@0~i18v2-dTuJsE_>&ZtM+M9~PtuW`_fu3y;ctR&L zlimY7w=ZSMK@POv)q`FEd2s=FhHH=`C*mHaNlT8(OitccQfqZt+Dqq4bHHcJoV$aM z**`|a3<&DOvSP>eChR!Br?g^u*gsvek*h81-3GFDZ9Mm%H4n8pVw4|m@4$TSe`o>Q zUXuflSKwW);d7IAz(>o^-tJ{MJRvXp@@8Xa8!+ZqEm=GZLu1rS+3C|LktuUf^q!X} z`c#y5ntW2UyX7(MpQMhr=lqO$-led#dL!mM|3!)<{QLLM()BCEr-X#j0xBp4= zZ%3NDpi|0)xxeZJ57O%Jz5<8vR%-M*5v1qMM$TGrxL!n%wJRA|)IKSYd$1?mBc3$! zh_5YyIbN@6A1tW!Jm5orf2q7r^RusO6%M>=4L=ja=gkAKBXuf!3oqxnf5)+1a2el@ zp2;F^GOmW8dIhg3jCu^rtMzR;223MMJ>(=zZ?CI!Wl zNglIvlr+cH1%{DL+LiK4ZKPj%uV8iu9%?9O8thv-4ZS|@$wl0m8}D#ufC==;N`VeV z$sCVdRrZRsKaG^mdP{0A9!RIAMt*k$X>(D3{_m)Cen1b>C{gMSo=V@CLS%<)l30rK zUi?*B(TIiVvXlEI>Z^g^_+3MUE5H{({HsT}5h>H%hxub*Z~cgJZN+dRIbIl@{EuU5GhnLejqZTx#kl{8e5^d*XHJ znwlMYZq(9=8Atze#_=GUPi2?)d~aIe$UddL8)k%Lmf`A{7a|XD$C*+{a7MlP`wH zNHzEcT=j^Aj)`K`vqUjsYfuy^4Nl_(q+o8=5^8{((}JytJ{=k;D1sRf6oW- z`7>cheWn=UU+EF+Ff-iJUFwUbqXx}@oqVxU^IntoR^-15;525xgU>fXn!Qk$?oX0- zE7VnI9n>3XD7+7`=ITjl|8E#}qkQq`|4fxy!c}m*@cVC2Ll}N(e+2ebX09|+TfiDn z)0W6e!0WNQ1@WoC4eTyYk>2^>>$Y-JXgqr5yTwSml$XrL_&iReq5tqMv!jdjt0__s zT|^9-ia6nwA_cRE!xzE#Naqoil7i;*y5PO7QFgDp%C7fKxhrA6(Jxh5YZuFYXz8)t`aMRml-G|;CO zVyl^Cb3J=* zrhl0zjfjU*AB5g`JboPeikjoT)ao2VUTr1qQ&pukdo}i4-jZgf-=+9#D|QwQkxm+5 zS?`ue;{@up7Rk~)o*7=#IJds2Kde8b8xt8LQ6uY89TH|Oay>!59WBT`7I$t|E;7M~ z^q)emY4HvkxWniXfcYfVmu9PG5<9Okd)?&`-Kz%84TSw-+m*9*tJ2`PW-o&_v{WI~ zosBtssXL2PgP<`ThMiCoQM*sYJ)FjYeQD~ zEBO{R?M-N-H%aaH-O}859cOn^I*WE-7s^#B&L^X8{fJz<4IT(h;8WQ_+VfW7j-n=e zenC3(5ZhvwO6xxQ%`{u2;kym3S}Ndpg~^$YI_2F9X)ST!5150zU!alPf%#3DD9k93 z3q%I^#-=CZZUmVPGLd^~I?Qgti|DcgpBw7fQw@O^b%Wntb!pZ?kMuu(P+w9MJmQ{8 zS1Xj$;E>YyT~&d}>DW9}fXz}3IkdY2!%LUVja|_%!uKk12)ovS6Pju?J4?H>Hn%H# zD>r2~^kw>+X)LmggT_A_W~F14apn;G)NpoD$TM43p$0h@6t}6FIxhl;|s9l>LmHNEK z;D1C){mW=+oH`^0|0M-_Zf6Ys+oJuXJ)tZ7$M$3P_C;E4QN#4W8TrsN6g)1iPRK!P zz++sCezQ?s(%Td#!}SpuTS@6#nt(IcMd{;AWu4imj2aJ>aTxrqlJ(fC-vRquy08X)e&ALM zU_bDis6T?6R-2BUQzJN(r#>6io3VF!D~>z70JX~k*7D_mm)TM5TH2_5%PX=wz9k#) z&Vbup5)ge0POIH6nj5QyYd&{U`m?Z(2xEE9M?Zy zYX6)8#sN%v6m$od)05fcigem-m)38;N#CXd*d?SZ9$ITavPXHf^^2CZ@r2) z{2sOCs(+<6517cnX?XqB5ns#UJY%K)(|BoS^PzW7E5-Vx7&D7dcCK!~A%-aXYYS!0 zj#bXuB<0SMo3-IhkOyMfZr+vk9<5nx*cRN+A2_PnbdD@Hm(OLM$=)s_z)LE}PMLn} zEFZ)1aus$1HeqehUgdm$rJOT)u=}JNJ00Fb8xaN1BAnsAQoZ@ zgEYkYLo!q<9lW(r`=5Iw;X5nMKhR_N2I77Hf*p9@Wf(cb^$zp3UJs>+i--Tl2C272 zkFp$`(6qo559ERG;4|r@T!gk`lXT)wAb-7=p^xb0lkoXI#^-grjkF(kme!z|(0weI zPP8SB?tUqXb%d7bkw=vNJJF0ztL(I?!QsiH^zs#;+t`b}9qHNiE}8=qFst<%z-516 z?@}-8s|IjN^;sNWW(9j!FM|i>cy`i3JL+I}W5!wBl((^W5o({Tnb=M~rOfT=q08yX zp{=N&3ta+7wiL7oGn9y)fZedYgJS0tY5t3N*;^=c#EPIcX$CM@_^ThpoiIj7y?i!l zqL0;^0n0t|0r+=FicBA*&qkjcMt_=dr!=Pw#$7%v!_Tr%;CX&BKBQn?by8|$kfWBJ zmFC+&(EC1+@Bu>4kKXkz>hpa!5j$5%CxepOA#m#3RwkoJUa|^2lG>$X!0hmO_^wLp z^keD6jxbl@Y-C2&GtXS6K(_M_mA+;*V0Ns-E(C$_BiJS?ta)~7QN#!--JD8 z#(mH~rw`g&Mk&z^{d>N&tkucI_LqwA^+a#K0^F3W=vx{7i+}n#;^lJoy4GOlI`91rC-Daq51u$NaK4Xrx*OPu{1> z=@P+aqkODytHS;HX_3FL*t_W=(zK3PApc`>w zKM`$`8-JR_`sG(e2~M_b11GsSGB|pgq~}OkYw<4)PU#J=&B9DSJO~~@3uW~<_%Lf8yNZ-+%x$x>w;$?yO^^aZ~@mplr-Q}!#f%1_LaIF zWuf)NKYwCYta}#t))H#h*5I+RpK5utG2h}6(5L5Mnu`6!X_=B!(bV3EXFCJCK9rr8;XZu+zWEP^w+^|-pUDze!X73dtd-e#=v)v-`0JxG} z4~TGYvha>Oj2)OIA_nXev73NHEZHOi_eTncR9R>_x(ILRd=a>|L@svaHQHlF)`1_CYR%N`3QLXp zjTvRify05j#sGG+BM-2fTr7C31k*M*V&n+CIpT7{dxBHr{u=aVm#Nx03;bg64)(yi zeCQdl`AwAMh@|%NhScb?ol2O8+@EtYsmcZPM;d0FFS0RXA@XT}U)RbF?|I242z^4wic*ss! z&$HYl6Y6rZa56X@xW@xTNbjqOz}Sf*>%I9V6qXmz>>$Bs@Swtk$@DohlfN|;rj9^s_@1(R$& zj{V%6B$5~SN#(7w+8p=o9$>D2pubV-Gm5XGHYe7x4agEfUCE;Y__IG7i8`HEgcXM|Bp4RzY~S{w~5iGlb{c??Rn) z1O9;CNU4ht`5_)E)kPgR=L24J9{!JKs1I85K%>bl?%*1h@N@~&liENpj(XB8E0a7u znET0m7WOQjr9WB(4RkLi?Qg-PHTjr&>jCvv&H>z_E%W6V!yMqF8i!Yd+cqSUyzMCK zzg5QW7yX^Oj|Lv22Po;HWZqem{0QOXtJAbY`-9_){^Hpc8VCap@V%={GJkgINl#>T z2j+>K;OSJ2V!`c|u%r75kBp}@wKV?TOD%x&PG?f)#ms#l{GP`>nSB7W8g>t!ZSA31 z@j&-KiK%mkFh}Rg@Gi%norbx5)&`fIk{La6lB`c&1i$!n?DH@KoZJ8|g(*_w;8W2u zQF!*j({AgUSNKB?808B z9!>RTXMs}c~sR)o$VJGHC-1sr(_)m?8)(q%oD{)hf3 z?UqRfETQf>xf$t#S`xDcI)HzG&kc~Z*K)2!UeV~+z+=LNIbZOi9QN{D zdkkziVhjrlKf&VcyDaU|KTPcs2M-zGRdd<`li!MGe}_3!zcIB<3UjPo$^xM+uonhD z?+*HbV;z_`_jsnAT+W;`hCl~76a1dnrgZuQcqXe&wNfW)e`^Qd-xjj^c@j6uyy9Bf zTEaQIl+cD}fKTY;BxQr2RA>Z@Z6=p;UgxA<8?I+DWOC*?wfh2VZdidCaarI~4;`$l zJhRV&UsN-mC5HM~^ra&#r9lF7d;llhTE^l-HnKGP2(uSkETvm2OUrST#UbZOl}0i5 z^|p*W#b=rq!|Z|@v$5xuMp*Ek{hTB{*w3Z-A|f?&r0|wxLJjFD(%;Pz2?b9JZ;Nc! zn29B=*nVX!Z_%Pw|1HI=L#ZXK`7LW%YF5=+R={n=WpuabXKMA@Q^bl$I0;@LGUYo$ z!}aST{K_7R#9XgMntPrgle3zR*d9!Ov;rE5zDyS{!5KM1jq*>ZR$>aX-v~CbUoIu} zMG@smcupm*);Z>^IURW~lbJ7R1&eKej>T=b!qSq?v-p1VP*XdxK->}V=73YZ!hE~2 zB@Ns{f8B5+X2Ak->eehmb=E+hc%*P=fTO)3hj8;S_Gz&FH@;%w8L2EH4{|~^KXS#-Fyqlx7G3HV^9(-066P<27Pc*uLT*rF{6Xvw zPouxuL<85BL#v%hR^#?_NAJ4A-NGvpYG7a4Z3pTLG)E~(z#^{--Vt89rHff!QqAhT zxi&K2s#_Ti!!7AY1?zj3DwelYq%}rwVx{zIX?<^9-|AntpcQO<8u|mwOeqBT_#@>1 z!UvHL6nbt+s68{m<7Fzkns>slWqWUU6yTZ$e#G2At$H zuATvA_^ArA)<(gTVzx-_J5lI02Mhi2U_mbS5%#HCz`kB^QgAae!M&LI`fyTuf@~B+ zkNx}v^~zb94()8hEuGokO)N$gEbudt`4;@c^rM1#d#AGWd5@uG`XB4uFblGsawzGs z*_Dim0T%nqV$mnpF)90I7S??iOBj>DVxFzSo@W>fJ}<)5EjfX$mxjNB$+eo>kYDWQ z_Qe4A3@pDtT8i@@pH3+b;w)6a#YHX%&R9Nq4qK%?K#cKM_bnAf1yLm z#MFGFDcOGAv@ceqo|8+Mb|lCu6sV#c>RMKbSer|+*9Mh;QK_&>ix;UO0&SXP!)ku(ZW5b zldyNV1<4Ey?*%lBbcyh;F@*Lj5%X%Y2swHTezPYw{#4%Zf(ruZS#TeI8P=+>O#+hJ>&eD@LJ9T57ajB-rUU9%P+Vl;43+` zsPF^|V6Rk61h-TXwx_WOnBAd67>j(98Sq(LAkro;710Zph}5d!`bJ<+_X%^_<$pwQ z=y{=zzJc7FyTUC$73%5_A}0S2aj0=NE8=9R6sJlt{zy*$xxZ=>wv#K_nIW{Y1wY`ggUbKW!j0&sPoA3*yDqjbs{nW4zZB> zf+b`!!SmS5!e<_35mOGcw6Hxae#Ryi*tZaRh*9ts?7}?jO0(dA%uKy!V@A=bR%kYK z@MWOcIwF%vEoBmy&m@`an}oDGO6LED@8-U&wjt00KZD;XX65iY(BhP(_UbC|+D*Xj zE1#cexwwAmFgISk=0T&haAdECe$**^e>W741HU5|cdUp%IY%Tug>UPM1;Tf0gYaeE zEcAxxgU_T0-?=N;6Fw1A$xmY09OR20`6)c9A4K}(FQW6N>{hT~b}M1uR}s7Vo^Xua zBiwBoA={$3{9nd;`s6t*BpyA9|0pupQ&@E0h0NXh6!Q+h$>M5!Mdo-RCF_}-O7-8f zDXaL>kX;YeWo%>D|f!|BGaAt`RsiQlJ^p4<=_gRYk z8u)DtpCtkpfD7i%CCIj!Tz}P<22Jb`k5*+q=Wp;2#q3MmEYR)`@Ix1|@b+7oGvXKv zDSM4M&s}6;Gyj2>2U(nhR)SMHi>Vbxz%v(~Hrv9PRuTM>&O5R+c?{fqEBIz&}#KegkxVD~+X zs&CM@d^`jWWG7R7Ta}Zy{ov<;`zP-;9*oT<+!w=y^sTyZN3|AqgWlkNjTR2@2t6%l zi}>2BL`crOXAo{xWFlqil@K1&@71K4we;zi%{Q z`t-!#n#* zq#O+h?QNp)RUHH!L<)3iz{YR&-Vg6`*SQU z%Sje2e2B%)@v-oiE0~rs99kG)7dH;O?28wm#& zn18&C%o-10;}{ma)*IS-%qCqbVQ%_N1GP4TLs=Mjd72p*H;3E4J@8>#1D)MM=xYDu zq<%9~FZe?ad|L?qND4KsT><`UQ@zJtU{J@YJ?GvX;|W{I%ow8P+i0|J=+0kFUU- zcfbYD0>hq*{N)_@SGRd^{y4>Eggxl+mbN7-=xGyRT zGPJD-`!quYb}tcu0kgn;9wP$h+lavD`XaEP3Gx)12ybL3q3s+j9EI`kHv@1E8VbE} zX`xpQhYk<8IqfE5I{@?kwO+(tyderb%wi>6FJN8kSJX=Qo-Ps_t`ho?LV^@5VcJ$0 z)n=bT_J@j?YWPt@Tdocz6o+D{^x)^9d+u4`FJ;%4SKx|YSrYoqGY7^8CQ;!k|x!7TWCJh0&>v@LVe`JfE@wJ4wMzyN;8;CUH`J z1`nj-zvaRrxTXo}e_x?r)J1BmzrjZzBb<|mh@f+j(7OK)UF2Y4beSlkH!KvszBogB zkdtxxnINZLh{Q$@M9MZ>xU0+&D!Bj;wX-f;+ev+2;LkY@*wDv0Oj`g=%j4=y?}l@G zBs+F#PAU!FMg!ThLjME)MulILG;)C7@{k$a2)Li`WcAfVYE+p)?RnWT_cQn<{mzZ+SGYE?l298$E6Asd#GI%-p0z?7 zuuO!J#Uf_u3ZZ@2i2Gq1Jn^=QKuPEfp~KI3un~Mo=ni|&6~V6HKQx&F%OWR7V$S>7Sm5g=s+TMVkLI_g9U`OdcHs8HaIT-L1itf9zpnIxuKh42_1{wW z>fewL(UsY`>oN7*Q))MLVTag;s`Cu=I^U`17<7Y;YNF0lCN2Ggg%6kt{x-0{v4i1f zRfPqiyJPe?cDdtJYk}R-7JSFU%fZ({WzsL;Wb>vo_mzoEoBKOBW3`d> zlmkBd`@nywLrK+DF12C>Zv3&4YkBwJ4qwGd2zR}n_OgjDU)>9O!x$tdgU3WK01s`pXR}bnR7$A$hD4_d0+$j-}s^;81EA5 z+%Cf2+Y_2|^o*mUg#DEYduR>hFP26fD2_R?yztDaAyWTrDm?xA2;OV`31FtC}RDU8T6EP=uJ>a*uqaOXUmrA3cy{P}3I;J|2xdyyHZ7c5N_cFPH zo^QJg*-Gfylk&=>={Q+`^cFjpYBD)Kmy&}IF>@WK`m%iREq^MLG0Uj5^(6M?LHNSw zVdQERXzAK9_olJ%+L#Z#as`W;F$=qUjk$B6X8aBOHt9CiFRi0$uX$9rw&HvpqiWb` zYKLBdW+*fBRv12Y)F^`3da6|CzbJriX;`HpPQDN5B( zz(JQTrleO>c%f+)M~c+QF(P$a zSK+%{S2(BDgtuld5&wFc@T3nFo&~@JMiv!Dc!-c*CBiGdC)ZQ1$;QN0;BkDX#_8|K z*nCXYdzqL~2)hFJN$6#=!M}lE*K*25&Zhb8g(2{(7{=|M#W=~-1AXE<>?m-zJs3u% z;j3t1{2A!do`5&<7WC&$Zl>2Pz*Jg+*=wpX-&54T zA^Dgz3Gcj&o%8dHI1@)vJ5E7kl|rR3^g8jcsqY>3o9)2Gt#}-Jkge3dI|qA`-Lf6) z;DK@rxzT0~W{H_xU)>cM8<;IB73J!~s$6vyhF|ReRpxG?gcLOeFFc>CtQtw$@RDIxU*Mo?3=UTzW%_Cjc1{= z+{elCvpm>71bb?{&l9r=Y5Y0hc)Pega}K-%DE0^C;T_O{t3%qs%Q2CA4i`avYLC3& zvBJ@Jln7j&fNY~)!jZWMxCL9VH>EB+G!JxL=kN>)u|P?1sH5vc1A~3i1#tCSV23vQ zGB^*1z>i!Buj@Q6yM1%64?}%AiSzzBw>JELTj}sXyV9 zHl35K-~8l{ywv_@J~etCrt0WNRJ;3=I=U7F$KVAxgDsIIanH01HsSiOJ0@9n#nfxy zyGZDUIjtdA^&jcnj!xx)v&Z1gF#vpvGA?~v1Juwq zsHg2@Qmi68?iS=EN^G3dvhYguQ0crz!nSqZK~sQP{)rPOglD^1^*n+?4zxK&$a{RF%H_^ zG4L|Q{ksvIirK*DKNjP5j^4O4J92U}irdyk_%CjTUrYzCS4)S-+6+J0ja^?^VB$HR zQ6uydm6Cp9cau)xDGbfg9ZEioqWad0rbbV4_u_0qmCK0Kf6Ky`CMz^R0d5z;zMA4Y zx><&*^Pa>$UqTM~Ii9+!6!uKDMPNH}3v<*I`h|i* zn(~0#O_TpW8&JC7cR6@`(Y*xA*Q)jHTwTn86&WU=+M&9&zCC6i0NPf>pQsqU*X*a&Oy%h@Hl=K zsTajN(yupHOTOXS&3q!T$InU2rlvkJLAK=xU=}gRu`(z*c?o;iQ23UVV#d(8##e)P{C1NR8spb%6{33ic#3=*YTTO+Jy)2lcG9?g zcN}yvbxpNSIZEaXF|{t>5q`z*9fQw!XogIN4)^QkZ(Q{NYukSSoN8bPi`sBIq%+sv zALaq?OHTIO<$+k7y^0sOJ^wkEMqWn#!Zy^ASZ)_>3GZoqZ#&DvZ(==_TAs$c7yu9R zAm*Hb$b#%gNzp;FoqIM<%^M=LwuOZ|6f<7v9d5s`!bxmyzrHv_HaeZ7?&p<}1puDG zfO6pQAEA1|D#(%U3eU<3@Mk>DiDL-vp-h}?E+k9eu!Gu|4ST_S%-D>7H_ySOl833j z&Sz@l1eXrty;}RmB>P^;_V%n)Uk{wFemC6F&=^FiOlsGjxij)&_jOuUYyH=&{$c9d zzRIK=dV=xsroE&H{LxyNYFI9Cq%jvPS|{tPOT#sDA1;cJqC>(H~gci8g`=)UWalg)y;=&}N};dzfsJwYp5Ci}3rX z1D*U)zy9kT?%0j!+m?V+-qe&F%Xy$O=8{sVk#$C!Bs8mE_avF5@dx<8uI2VEUh41DxyHNFtR6hU{(-;Gztq;@ZSim^)rXCwdIO2tz0hCZuYtMfgDKTp z!;LYEIC&ju>h1;5#Ng-2_FlFRg-}wU0kryz1}e;?s@@H{y-P0rw+*O4%w?bINcE;T zYmJ}FEAowTlNz!jFlI?l}z#rOz^Nqf&C+?QgIgtM~(^Tt)aJ_$ju9n`$)rBUP z{(i)z);NDVcEkUq8#IXcv-_$hIo8c27aicO&!jr5jCyd|rJpe115yjKT_aQ7Q4idd z{Lo3i@!KUDbFJ@U-1!`Spj)^eI+{!O3A8Zvh5BcRAdOaF_go$G3G_ zZz=Fr_f}-VMVXkoxdtVdTEY8I_LKaRO)@7xC%@-0$@Fo+FPflt+bJ6_E`nZ^QoQ%aif2=b6q~@9A{szDPpSJ3qZt7!~%IfHzm`^KHecCZ_s9VG5P)5F6 z4fMudu6B5e^KzQ2v+#bVUvTMTY|Q#yfD><_YRwH)>Ntnm*V|E&Elk!ER&!r+h>+G3 zp`ZPXOtaF+qx;Osbi6y&6OjqJJ&DvE?y^hGhX-yN)h?ipEGi=F>JL-jxq;g^AAq|# z9emY&e*2ul?a$~pMh?e$n*!eaNAM$y!bcz!ecx--e$tL=x%{Zn&Cpx9TxuTt`Wf&N z9xb87^Mk6t zB7}YVAFg{7WWCZJnRIGxl3E>1()tgY+6DLAAoRp9fibnf?6IT=G-30>7i|GfHQwKI zm$>T3&-wwsC#{;FRJmvB3tpINjz95z{=~lj0r0Ldav=AA+`uzjD+b)xvH{S7Xaq#K+4e!)SeBPhPpLi|nW3OQ5>*gY6 zds#nsPF8Pb#xu>&$k$zZ(OyOGJ{fv3r@Bt80pY*&~;N%s#_tN9tZ>c?oHs!83$ zi!slC-rW-VApLjvPb~tjvXqnCi@4o+7jW{2JWxNA2+TL&N!5w^?&f9E>+hIDc2aHG z8fduQQpb^;;F92ucV0-+UpM7i#A0OXjDUyHO0Jc|OmQe3cdp=MB8i?4_PNS(xt+yrT`vGt%T64SdFTnc#BiyRw19 z--t_m757dkD1zgF1NN&bqz#yRo3(;oKaom}F~gmo1e$$rN$U#0GZ4@I;z=Gb zp5Ts2H0?KqDEZP5=kJzmw?aL-;)lnq1l~e9`2FvrWa<#pZr>eQ?AbBPwSop?1Sj4w z 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 z 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%0DDXdRUZ1Vi!5d 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 za(∈PeIHMbuoVB{M`Y96ia=Wd==_W(Cap5meRuXAbU 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 I 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 bnMsLf7;u{A-B211Vn&g&`Za27pY5LB58&hA{Y zncEYfX;)oW*4O5i0%5{&vz!RDEe$Q!FIWRzxmLUsw0fS~*{?rvcK;3!@;|_ha3h|w z!>6(Y@a*k*pu=LU@6*t?%jE9=)*>IR!L{o75F<85ooXet+_JcO)*{R%`QdT*A7WnQ zd#njGLY~`|Z#d^qALWkkxkT`fB0`&pns+S5XHRLwx|{7S1lX^(w_Gw0`jxwPjk zGzP9BpG@ZF%%)sB`2~4kabOG_s86Zz(&X@!yN)^C>^0N#z#C+luFU*T_h|K?@7!QwvS z&u_0u8AoTqSIGshxZz~p+W}3O8$4jfK;OQ)&?o4LPwLt*~i zQaJ7Pan_vyXJ9e%%;7#ZsisX^Pz&qhG!Hy4EYy52a8CS&I8Va)@t@s%RU1Ab`_W^) z3|!Yv&W|F~kUVpC5_*qvw zsTA7={!`JFoq*nM(bmFR zNfLp4h`)xW+1aZ1s99e|&AK~0%u1nuGX(wUDCD#Marc;wn6qloPiV~D!`E`PTng$@ zz^3q*TrE>jnEfAc_uduA#UFz^_b^@Su?9K?8qR^!i2aX17t_N7?+&BhumpQl;Vc64 zmKtVb?WWn47xCz~W3J>a@NOCmz1rR2ZZ7w-VB2)oVX0lawb5?o!dSG5;-(X4?a(nk zcLz6k?_t0_RVTG$17a=hq=ikOKsNf>{YpZ6K1Mgk2(X&s_rpUaPDYDCLvbv^` z=G+G^*JU0!u@37z$?JJif->%C6b1aN<&=}OfvLK?+jkypKT;^?`=S0WBX<0xJAYIp zrTZ3LZG$~Mont@uLSL=6&|YB8uHJ`u6Sbp=Uw!NZV$~ltxmu%=@NXU=v_ZW@;6)L^ z@-5-abuyi;ivV}A5B!q|HT2@bABytN`wsfpw>(_!_lT?AOA3F# z5W$|}j$0p%-c$h_%gTt>Iwsi_qY}LQQgEN#gWe6!gKf<@iwj3wQ44&kI_O<>@v)>= z(X7Nh8+%TmSls}2?_9mF|@+SRgV=y}%wmk#ITx@>NJhx+VJoV`b?1KaEcH!Z&{a0&76 zK?nGUb-CoqDO{_o3lA@b^91)&JFKn4OTqKa_Gw!`!=pRR9(bI_oh9-K7kH*x?O4<> zO6cn7zoDm!I$h;C=>5+2X|-P18P5qWOS_=)|Aqp5PW?8MI$yew!~?vtmNq?E!Z! z&C6PCg=g>;U9+#UtLHikmatb?$|Vc`LP=;Fk@xTZq_YRV>B>Jca+1ef-Ck1!n~fBn zyuF0>AwsZ>D6aA&h{ryFuiV#Xty};Y0p$MEeiF`kSbIbLc6KWO9l}<+IjJ19LNgw_k^6aTX%DMk^)9kIY{+)p#LToenn9rU2o9;{*MVZ$P zc&xr4=eoBz&xo{T^>oLScET(_MKEW;7C-o3dX&HowM7NPI>ZWIPA?H7pA#|BD}`AT zchlTl6!3{D4zme0;Ui+3e7b*ZdvY~z2Jb)zcO6_K(sIEw?eKh|5b>x|Dtl+VS1st2P+A?l>3b`>|$m z*V*2Al|_@$7yJX>?OpBCncS$Sp-qJ)q`tmPjEzWh z#19tQgXto;q!-SOOwLyJ*Db$}pvZ;bU;Q(H9D{*hD)|-TR8-Dvw^y*zBkUe)2z-Oy z=-Qwz+{}YI=Dj9DLfucRwn<3SHwbq$;^txZbpOAT$rV>h{!~DCX4DsfYav2&hoTmmtegBEG!MS{q_(TLTKpokJIA1Bv)reBovBM#U+U&j ziTv|=lKa3!vdWx-Z$rsl3H$1&y_9wg_^)YY48^gB*!dau%tjhKAJ%bK?+p}uUdE8- zS26-y22;THhHE+J3Tu}(f*nOnAxc2MaG_87uu6B9;iO#rNB8_doqfntp*3c5=-p&t zE|2GG;x1jOTT7QhP-nD02i`gddg5I~Xgl!N{#`5F*4!eH`;ZYh(uW$!39L9;!>~0-edgl0+9ozR^*nH3ly4O($V?o&_#p3uu$H!S6!3 z6y<>i+M3{CpCm@#K(B_ra$L{k3AC7k6J3C|9A)@`^# zV|L2Mqpjr(R{>~p5Bo(}+Ug?fQ<4bwC@7S58@PGC0OGE*= z)vQvoR)OHvQ7GL#@T;zke(F<$r=aik4|FPDb4#9n@N7yF9``-Mt2Yi!!cXFl_LZTu!S3AQWeC%Li(eS z2-sF}>6e{6cs;MM>?!@#zidLzzshHdN#KVgSD)Jfb>!{3 ze;>2s(MSqoYT|@)ePKa z`}MyfsY_isazGy0^0qDf@TPEc$a2K>2RN&rU^kns)7>}cKqvkSkNbR3#GXzPTC;!c zfm3!o=Ma+Oj_c+>^^s#v{-5R(bQ11*G2ZB#))1Qf0$O>00;kjpn5arVDeN5ggp?F) z?m6V{H;^CK#a)WtLz|w$v!|#qi(*Wb`RU3stf^{u%^@^@Nu3 zHr+L*35DK7UnXQD&YBF$j(bf#yhRO1(HcgeLyQr5q>2%o9E)?Qgve}oT3Fc(VO_nI zGIKREYWwRO*~?awdu2%x`s5!G`}PU&O{TEiye_iqn!?QWiF^L2PSWRSV&*I!Xm?$> zBg)It^XJ0Rw2yG~t17e`eT3&>eW6|0=5s7U?fZBiVrxU}j<&$hR9Oly!4$4MSPFf= z!#XSa&Bq3hfF|7Ex-(~UVp}fiYT7C8KR8fGoq7p(=}`0-(WBu>sAv7nwOe!GVbaWI z4p_vUVwlJ-w^zh8-3On@VS)`DiyGKD#Gh57S?4a$=7sjdyth8HabyiXSwA&k8{cKm-5S@FSZJ;txdNc>r3XccDlN0zDOAQ zRzz-kC|KP|LRlCtv}tj>LMmwmU5~gmnG_*VZN2*o)>dR0$)8ycW0IXKW+*zW7C9tS6AUb6D7>V#vfq?SI@am*_`ooiW zDM^|8DM~p-(%ui00-j9L_wL3Wb&?V0pK7EQ&S{vX9&%3k^N z{0-ba>>r*rcb>4;+%5u@7of+47-}J6wbmO+ZEy&ha{t&^dM_WVdCg||mQOT?~SDMHt;60vR92d`=eV1^K7=-?P z3DjCQ;9ib}=KXuF^=l#6*?uB>t0uDdZW0;FaglxWoQU}8PvMFmDNMP#aKC)Wm75JY zt8If0Mt84Ltv6Tmd_+w)3v~vM&)HxsSwE*!T!rft`S2P%zb{kF!N=fghZvIvlrYQ& z)r?jfS{kn8$ws9KGmVIn6%6goZ2i1kLjKyQh0Ge91a5q-?irp+?$%dzPs!>cwu?`s z8OKHD{w(pxtS)Cg7IMFLWo1{?B5n%ws5`=`5po1cu-_K`cp`V9i^Wqh``Pjh{>Pn>eoiNmyhV| z_sVuNY!ZB&Qhm-(c6fbnC$rfFaMy2$mxP>e?V2N)OY&NU8S z19vv0fnhFzFL?7TC+VZc~^X_48Aau>CKbX~#v{g6+Z;tqSYu zqQc#yh%oc@7w)$kMWEw;;W)ffI5UR|e_gwfYMn-Xu#@h--y9yev(V@K3AubSv`xq- z^?9rZ+D#|VE1eSdUZdEdmnk#m7`f&?26rjkNcP)}#S7w$34>P}$Co4;DZ}d;>~2M0 zY+@@pp|MNOXxC5ntlo|o%>rGaN!;^hf{1N$RHzg7h^!9Fgx3ACm=s%F-sLMNXBK}Z z;(duiy`n(H#?0yz>7r)w| z>r2v^b>#nVJn%Zp?CuS#MfT~ZV$x?OM|g6}$)k$PjmDOj*(D}tL~Rll6KC(oHC+9D zK37gI;%aN;1`}@cK<(xtxJ(5v<^{C4*X!Cvl~DI1b3rjujM0eGFY2BagTMpZNvRjl zQ2dy)z@Z$WVBzbu7d)nwzryp6`2=NpWu=r)G-+GCG-$>orC%jGf4M#bFiJHu0i3c`BEzO!xr3s!PKE*HXbASN0|G z>y%L#{GO5HjgvpOHe%Yop^P<)5TCRs>1se%%%Q*!Bc^*{rhJr(xy zK;gk6J`Py@JgFk9-A%!MDJ37g+)PdgM#;557nGIJJB0erJ8p)_y8oB5@Y(K8p5Apy zeZEX*|DLxiWkOMZ-{jLa%+j?V7k%1J)OJ~qCc&ch;eUI9QU`_`SF7YVvcBFXWz!y# zTHmD1HWdwzy_2!iGRw$0f29#(8DvD!X^Og(F8VzfE>CMbOFsX`CY$45iz?3|WNBhs zIVH7=oY3(TdfBM+J^ZRGYY-d7AP-t|R9Kd86_$H_fL9oax=kID&ND+X+8f#BRD;{N z@yHiuSgSv#Y(HXx8n1O{hvx8Ny(v8LmxQNdyl~|S70ywu;P1XlXwe&l^WJ134eciU zO@T#QP)y$Sz%IMm)saKvve7G_EzHI?cu`R%A>P8E*_V# z8$RPNgyW(YTpFB(W#9QCA1)KQR~8G_TU2g zgK@wu97POr*QeeuO_l-6DLyrg(sCarGY|ILe8d57kzaRvZnxF~?z1y;{=m(2rA}9h zf4Ubwrl-(jenSyY8X2+uem5+>orba8G)`u3GG0t8Y&b`i6_coktk#$;t6O@=jfU2d z`|ay1C)ZjcmmQKIOU)X{+9hC*udb%d{_`mCa4*l;3w-k8QzGlb63o-zT$$QWci>!% zh`&N9QzDGu74TfU{$xZ-gTb|KYdC7Yr+_<2kII32Ko6H4qKusCD=#;StSc8YqU13< zp=EIw=k|u@@Y*V=v)%omT`*fj?Aa|G^%fwHEg{&ZHK?N}@xZcOoXr~qEy@lQUwj97 zCi_Sma+s7W>oA{IGQ0KD*`qqpNn6fYPSxkU)_{`6tprE^79|xdYh+ehXnaWV8j-f6 zMv;L_4ENSmIBUJ)?> z_o3j1d}Nkx#$Er_fp2FuA-zQ&+M|zMYKFY1c1LJg09)WpqX@;M$UN)8kAqKgnFNYB zlT41bNx;$EBKV#fZg)?k%Jq3h+)VI2hy6?D^=f+H{9X9Fg^Pf1i%=`ol&3vQlqcuj zDYu_APcC++h-{5nBD7t|BZ`bbT{BWB^;$spZwxsqPbAAM`LQw^68%^1`>dD8tG zd~*H<_|(1gIhR$1?#e2PGA~nXtzt&8l(t64r?ZW9u`7&8LlOZ@N$F;=?4(=}BtB1zoY) zbS>u%^sirXXJStgu#OWJ8GC(CX`hmCXwcHb3n+I%IpA9(6^U0wt`pZ!rmdOG8YFV2wNp|NgC%bPP6WL8?3rlD< z)Y{Y11Eb53c4I2?)PBTXJVd|WbWfWq?EkKUwQJEgPhJ8bUL54d7!AzUwq34i4}z|HpL!Rdu5(tes8v%%Z@t%s46 zbDlBv{d&VvKiSyhnrkesTiI|tA4-9L(|A4E+!z{Gq2KDVshQ+X)Gu7bb) zI-7C+=tQGQpB2WzoGXm7apR3?lgk>8pAml)*SX{64I%ANu-oLV*HzNV2Xc>j;IM+pVNT{z2BYKj?h# zN=7kz4MTrc+)#fnXhfuZql{IEX)gC8ryL=it@6s*g=6LThVk;F#?$4vmJ{S)t$}jV zon~^x;#{)pLlf|+>q3hnKnDvIF*0}&BVyQB^!P7Q=#I6B?<*2pbqCsulZEB!Ns+1E z5Sfkk3rmaXLaJ{Sf%C}k9EZ61p*rxG#k_3zGn?u}4Pzv*lcf`d61!c5US2O8*T;## zKj_`;T4ZXUBium;>P#6ca`l4jdL*$vK;vg-k5oF?*2Yza-8yt=2SCnH4_L5Uq zE|l*qULyz2xMb^+F|uB)i9F`N{IVH&T$t0Ia{sxWls4urRr!$9Nd5i-vCJLvZ~KQV zt?yA>yPYJ}3nRAevt9ikM(q200?uUfke>AdSdindC}DDWLwTVXeAhsu)wAhF@`eQC9-C(*beU%K zm>z3P%D^4%JWAT3Slu&nt~j|mT#oDRke}6(SJRB_s0D_9;3DJEmAOXBv++j4O4-PI_8M!v2q|Y9L6`5T zh{z?&o^3Pblf%}_>soA<-}PG|yN|`oj)EQJM-R%#arqA*zAD2#j}}nG>3`__5@z)H zC6`gVdMIMJ{D!64E6Tv$)#`^3%b96Yfv!|%HihQcGLhQ)xQJT$2ppb|B5v3xk-F=f zFsFVI)~MI$^W7BMlLI2K2KBWi+rbTfqDL&+LXPD>a97ndCM}3Ho>iW0?5ViY2rrOi zyqUAoi2pFp2qXmBVDqsUGs2>Irh}*wwPN!%Ep%Z>}7F zeWYyJ(^}5nkGS%Af?%Qddj@=IN+L^#a%6Ur zP&eJcrKY__~p@6%=gr zU_EZ#ENJ~aqQ!?Q82p!ZhV{-Q!}}WZ;9YL)Z?N1LIzGYp`f-$DUDC+NUX+D+p$;ii z9)Wj#Rb(EiFK2ZdC%5@JPo6wELB91&le3PDmZjG1D)o?p5H+z`%*;q_M71PeL_zshioa9 zL(abOU6?W1=&5E2rPW^|>f18(SP)k=CZA`$prj}dxr$@|Ua&$3WPtIm_jDt8d7hE8 z1~j+Y&M{gw9b=UJ9&K0(e2zU~wMtJx@lW89q7Oqbr& zrpOB$DeAy~BsHjMBv$HSc+Nq~q~uh?zhDM(-DyUv{{|Yn%2|!f0uRZOKN|PQN*=7S zN4(1{CwH{;k;nWpRescWntXnqQ+5v@Acv+Yvb%SKdh-)MH*fUUwSiHT`2x7Bl_x0JJeM(M9<&EqbTGc|>}_P{>}O0n z+}n7xxT7&fG%(JO2r)9fNzj0use6{!7Ew(ui(O|b$;ujs9G`cX{I$R^Ik`Yz**dwk zoK&ugoPZjGr?yp?*MHU3Z<7%Zdnv2HOX`=9&j`S~zQ?B=hI5z~Izp{Td0jydeA~sP zO#?(^&x7!={wRu>;c`l?aKtW5j(ziwa8*7fBJXSwo_)vx0vTM3udfI8*Py5~vneC@ zZt{5U(AAX%jCXUY8?oD)8rsjz3_ZNL!OC(YCcKVu@N5Ai^7rG!qEPoM`qCaXaIWx= z{Ul!0x60Xfy2vLl_K{<)ePk)Uqntdxk?is0ktJod(6+Vb&hHgS+3KdK5BDhKXs8jG zQqsuiRK$o||AJy!61l?=ZzfH%OTDTKSD#J7n)`)tH@C<``#{sQO}HF;^|OeXohj6& zdxfXvL=kYF;M%Gw(5P>Lnpz^IbvGz!>~l(*8*U^Xs$yh}sB8E)*E95=>l!Kl)-a+@ zmN7D0{YUCL7v0exUs23BzcsHR?Y{(!+8k0`D|Kbs3iQVN2$w{n%A-)({~}UO zT-rcZdq6WJq`aJxQb=|dd@7{)^+HvSaue8Tr*$g1ivCHVE6-7A%NLZo)7m>4zg~$mTv%vp3E}TB}KabWFY)37h ztB*pN7q*~|=cmj@pDFW7F(b1@EhC{p1H;v=mXUO=w2^$jpb_K#ND;-;$ayg zAPzLAyM)Ez7yYK?mopQp$Vu;N$!ud4+4O|tzo8Y;()VcfOU7lR>)r`DyR>*si zQRI?{HMWZ2dJVd61q3TT&F4HDMX9*YGLQdFj&W}(bYy-bV|h7aT5D*9<*I0~wuKA_ zy`u#57}(GiV3$=PHu#232}r<1trccPzX;96%FeQ|2fyd93>>_3uPz*>)uD`2~0x=1P|)-UkM?t6)`< zMaIBW!rAt=kX*N+Uvy5S?Z6%1)&*=Ua>ql+YvYUP?!|SX|FVQ)Q;(2y%^eD^_(5Nn zTa3kDiW`n@g^iP=avKphUsA@IGnDmVCHapdj=3<+7qEVSuF@3n6Auct>~k?DD@0B{ zT0p+~C9f?-yZ@`emsJ|lGbM@pIo4&&}!6u3B?q@r(i_OdZ_I=%qU z>=d!h(eoUBM}&@hBeL4Q6-wXdz-n9*QFD)o&;s*>XJc8x3aznA+xO{?Mm0zsGl5)p zS5Qm=H^r`4OIfKi$=@C|+R}f3`#Tgm?EiAdmHr~?#99&G)hnDs&Z8f4Rz$TqCLFJ} z3H71|%sJ{OyN7|BA?xlR<;XE>Cb>HPN%7JZQp2;z)#3w1egBA@=rv{2eM%d0hBEJN z0VcmMx&M6#Z_-{qEhL#+iq}IADnWR{(nVCoiz4=jA8R&4g#JhuX&3(#>Q3NO>NJL4 z*;K9!tE#IxGIgg}ivp8}P@wi)ih8+_{40MUWl}xdousoBi+sw_-+}iACt)VEU7jS1 z)W-XTyU9Tjb!(T%T(ce?v9pAB)DB&ay}&%R@oC#1>Vfy|f%{!Vk^2vllzNFmhdrUV zYgrVTJ3#(cSFt~iQ|j{V&?-c~D=LJvE(LV+-{#zf9;@rOSdlemt%y5)P{bW0@Pwfk z@bORK=#(Nd)-4cbjdsHQ@D{u{QG@xny{>(`raN{@B-Lq0&ccI8DbaS_^rd%kf04vieU3@l0}_!;BufveketzIN@ zvB8v;X9Z>4+e@Bb&k^f!6}pP&NxMT7EV-Agi`?YOsgYE%8v4r`{CFL9MPAK=L2zX1 z4-}CrmJ2B&MZ`beEc_3^@BOhDI%VjmHR&eQ>AvyHY&Sp5Mz3mqpdLaeFDaI0&39B zkT*~DDZ3iz+Fo5(H!yNv|A89(NoWVQ(zVpZzzOEHE8b{swm!!#QzhYvX^vh3>Zi?{ z3(t$j!u7U+2nI{xez*aRk<#$*((RmH2 zsVm5HYzg?5zfrK8ASwPSxTsTf*0n7>w@Seq#Lr!4B8B^NXQ9R5eCya%ctgT2+&aka_w zqCaU#GbrocY|0!ri`*e|psO{TJiFt`@iXeR_bp^DIS#J)X!Jk_z+0~c`l10YS)dQn zq@l3%ZX&d^t%SRNPoc$k5#}oNIIevImvKM18q49|)DJ!b+wI!MjqvQLshdSg>#Qch z|7flcSa;x#tHW!qE4+@|;Lk1KT9%s!CU58NI4^Yjk8$O|G5B2UgT5DjHy;?YiyhEU z8t>CsEnT^}5E@h&y81SpSiz=bxz~l9kNT177)sV16GaGmcglt<`;Hg^ci!>U5v{}~zre(r9&8{D%| zoV}F52Lz@kp|WnaD-Pd+Gd^uHaH`fvK32bzkJU=Fn`&<^OI{6KFpnJfTdhQ$wR@raZ=p8btqH03WOBU5nK5+~xo1ry z>*`75H*o)asYRY`MMz7@(gSbj>MZ2GEl_Gb4`c+P85k}cQzM~?&;_~d&%!cqq;M4m zmwA2_q1HytJS!J8UeTY~P@gkC%&T2|1B}2spTcwMO1UUqc{xs3iUUL2`LK_D$!%xy z4ZGQ(2v`1Ac%bbPcx(`7eXnz^)JZPMo8e_;p2QC82lyhfN7#Dvpu@!3I?8#sW$X-XbDbbzKXlA?A4|KRf}TQkHJHJ%B3@Yc&8jh-4fi2-mN(s zv(K*eTLW*Hkys<9?mwB6tal`0BRN_9U7?cKM)xGe)>*QsPC)v$g5!l01c;E)^w4LDPMce=% z=L%PKAD4n|ZYGY0o>X4UZJ13{LUp!hnJ&G)qFXBFN6c4=fBclX5e<9$ZS z&{eNa))|FJZF3)dZLGoFsXli1jUArz+`NR`H4xPk{O@RU`|MRT1wvV7q6L~Fn5c5`-Pu5_LX zO#supd+X3&rMLsrl6tJqg9=QBTx0=AT)~Q7- zmjb_)&ZK_EyDYL(pl%g%mB&0Bd#$TOck1T;iqLj=ZByD=Iom&zyMLf3h|f_AEt?;5ZPP6tu%tn&3wzso8oCaLqS>?nd^799@7CvILrbD3E&(H%4vg94 zbhc%@dgOdu1w3-xjkVuFC+Q?O(X(~!K>!|l&EYlJ zluM5jxQex^t-HzL^Te%#a|rFu3odQH06bnAH&J4Ry}P_K)zfv)gT~?n&G$#o+1Egqu0>-XV!RFl-N(KI{hGcRgqCvA4dBhGtDO zXdhv3^UgMQnxOetLsvHatSik^bhZ3N_`_%Go>O@!@Owe>YemSPi8@}sf+RhD4{h2j zdY~5e_T}HfI-8_U1Qr(Gzdvy%OF_r-O3VEGAM8HqUo4i7$~`tA^FP)YE18i}5BVV%`$2VQt# z_+p)~D~BVn7P?^{%m(*75qNowVZ-^HO-F71k_s+#XK1RI$C^F|PrGh*)-fmEB^X2#)s}~J_y4r_%3asl!E5NNUDeIz7j*YQAL{*^ zp~E>7cSc+2_~q8whH21o8gJ8jZGfNeH@nikB6LAoV+{@A=A2R79N!b%%oezRt=vp5 z2aV#7cD7`-orN^V7*@5je0A^)c$bnEJ3HUj&bsf%??2ehJtctMmAF=+5!e2U<=T=l z(6ku}-HNv0O4i}ZsgmF(f3vgq%k9iDAzDkFi~0D+r%T-ZwX&UkT4mR2f3}YY*(2KF`iJx57HYyI6piWaHCVqZM8j_!z|+;pfNp z!GG-%bjER}>lpWvy@2;e{8990uKe1IoBi5yrC($CK8-f}*y1SYwbu5s@8Nc> z3C4KgR=YNAvt6sZ(XKt%Z`ay+?Mz>fH8dDwjOTTf2R17TzS;G`SIlQ;Pq3~&b%I{) zT0A%Ap}Aq#cAc~$lIn<<6h1<>b9SvO{_VUy z@VR$wOnZ^e7I*fsxBH-h_R(kdh|smRO|g!8f~PfF*Bbn)o6=NWOPQ>*6QgxAtf#JR ziPD)~K-botLOeRr$L?%{m+29k_LpY=KX0&$m+V@Zf9!0|D?5Aj)UHjx3ZKwFv44Mo z2VXhdSJ>0~FE-|X6wMMAr!y@lJpZQKSg%sp_tWfJ?>+F>yn;CQ2i~(3SB}(#23Sqb zwqxI)D9E*LjI+P6$F5Gbv;13ZZ1PqwyX5dOa>Kjxy3gE_SJx(&*Ui#3b?ta{-RxZ* z{+D&Z>uiX%R!ukGS>S>C&BsPy9iB{scjFx3tn=gU<>~DAUGS*RZD*$h{F|qPTa{?n z_O5{!_$)hH)D`Cu_CX-OorT}9v8dr#N0+0RbE;m(S{-Fpe%Xmhcy z{prj+AH_P>#~iQ6SmV9+@3pZ6#L}0s{_{MsG37NhJc2eh_@#}>Z{V5y*2dJ^_$=>h=K2QFFfZsy!)>h@7eHX#khQ(1fMvJzl5*&A$Hcw3cmhR8!L->%)SxL z!m5B%8-}yf;nU=W(Cb|7(<*KSPYrQe^V8@{XZYCibYKm)!JBq0cMh;-&pNN1Lj(Jbq8 zG>a^Pd$1ACTioI5R~xHEEd@O97kA)BLvD-Fy&&B$*v-5qd&T_=0lYDHf z3=DR4A8Xgt$Ns^8kHGtDW4$bLSvpfrr?bdXUdDDrvE%z~tRUW{_Cw6UZg`dAbH|pk zv#}V%@dd$|$M3goz}mWt-u-tQOU0UUMB13Ox{a;=!^Wy%?PVUbu?Cpe?5*%r#r(a& zxx5v3ruvtS#V6ZX5`GuDz{b*!N3*LDUe+YV%WN0DEaS76y({Nqy<7U&0_^*Z9kA}( z`B)o#T~gqy9qD67@Z3e-fv*$rLhm1YyE-_((OBcQbmqif4fc#?*7mrUm)n@R0{dX5 zjeW=W{CJNec!q;GpPF2;u`0Tab-Rr}^To!(p4r&foi=uUJibpyoaDB#oOs4LHSznf zXr>g3W~n)B%!B{;EQn^oD0~+7W8hX43k^g=V*+cr5XSE(FN-V^T^C@RgiuV<|^%?2pqnwh(hNZ=H=DPqeWK*sH&<#yjEfWH!K9 zzsGvfqFKglJok7!>$@f_csrdX)bO&H%jryQmd;!ly8wQcnA6Lm{zzx+;Sr_{jbedC z=`6d2mp#I>oy2=J>W2B*iRTT%JGcOH|4&}l#t(gs>)=`4#qUnSKl)cMGY6(KcQoeV zD8?;6&W>Mfti?sFmFG4Vd&|adorVALeXIq{Mcii_dv)2y${)0`F>A4Bw%J&1jJ@6# zYwB1uOIwTazZK2QWq9^~k1(ed&$sO`Q&vYYe+B$(eiUQNn=(&XyeCDmV8L{jl$Oq} z7WJ~^I$n0Z8J=&3mu05lSxql1p9W7|?3=4syznaWvI&@%>@!~WhX?+y>#-kZBR=hl zJ+8z1vJ~bbJ(@Yn;vG~Qo0W|5_hGD$+1QE`Sl@Uj_gx!nk9YE$HdcBg*4rE#3r@tl z421qcX}p6#EO0T6X=S6BnSy8iuPF=G!FZ3sdpH_1_5ZbWKX6}9*8{*WMIj_1G#-Qy zl6t;R2_F+eXl7d`H1ZR}@;{r|N0KB-{w2vX(vr}q=lhiKoj+;jPiPjxGt8PbYi2ZJ zzxTOcya?mg$+bI(2Z+;i{eem>u)-RbXb9B))o?d;Z+UT8_pGn-O*v$AKE)i0Z) zt@zh`&1quW+5Ya*9g9=9g^SbdMPiEG-Hm6|ql?qWazgv5#xPV2Zc6@UdA6fH>?Xdt z!~aRXiFgtdQw9%6pPj_UQ3KM)<$(01c@2hLl|^aj zC*(#iN)4~IiWfGWwKy#>j!$kBPoo#76L0f>VJ|m6^YQL;#&)xK`Uzcl#gELNIqms! z|1?Z)9Y5CdgqWIQO|-1`e=KicKZe&Vv40rHPV>zR_;89nr3*PZp}4}|)S4wGuM$hQ zu;E&^{ECgs*gcM&E9CU&+4vQoHKX5a&faQGBc586Mt#004U6&oitHP$Y3Vxq3hvB&O!PJ0R`7MohhJJ-=iO^5~1NfCsJnA6v)o+dn3Y@t^lkZ5tcM-f~cz ze7Bgr?fv#;YX3BC96J7J^HXkPeI0ARkh7LWyGgD7?`z{T?($2qeTGz*)IXDv!i*6;-Dr&1bVX^`v^P5PddyPKWa_22KHJbwX0jxeErQoPaoaDLj63?Z_k^jS)iNx zuoii&lA52iUUngSjPaex);rj`YEhaMc{s*nz4Y47n%Kcw*ik;)NM2qcS3hX3jkDO* z9PZef8lK@VIdkZZ=9z61o)aT0`=`NTWx`V9VQbsn{nK(W-DY0WnS9>F{w>7Dp2pJw zt*`O_ttl1nH>KuXwciVWOjAm=YHF~i%bVEUS+3fVoqHOW{jVc`PWhF2X=KM*>wJH? zZEW4|wd2s*8&`ogeo#iOlh%24}+@ze@q>(-Roo@Un(?AmWpD*t589!I-TpTpUFIUkH@%KO{U zZS}*RbP}1Z&Hw9S`(NaKN%wBb?bvsgxF4hM%h`Lp@)>ds8%y(36F0RZjrXs7y}5Ft z?^fks>tpLh&iU|mt!adHI`nw_9aA>o_Z`H_UF@FRn9?+R`(Ij9?}}K$ zOQ)0D+xT`7Cx`L--o|OXsfEtR@HSR&Q{UE^<+e&HZwE7trHOxL8nbgjgLB%5P1)X; zZ3h9dGnAi4T+=^wlZQJ`m(#|<-)H|G=wc>~ZAwL7K3KOf)p~?&V30L&Dg2Hx^)a>s zti_0fu52ABmimj4L)i0`z2tHBv8(L`@B}-Jzg&bi4gM}TkeZ_vSf^fZnx#{sI6BKe)5Yd8c%{C2>T`a@-YX$H%Ry1P$x$ zb?jYxs5sl*+=!dCY+r7D4BZVLjJAa~tIWqzAl^o-rt3`YuQQH99FA{HHNG5iTT`mN z!H$@lFRYn}r#@n3FdNN7+bjHUT%*xOZ-h3N{R`RqJNRPlZZQ5k`J$J4cQBUjr}?PX zFBKc;Z)^76s_)6IDUDN~B=*HrZJl`%M-Aqrc>{H0oWH#_5cvyj#QS*nD4Tq<+Nn*cIM_UN5l`>1<)7efycguDovdqla>Op6!5Ehq zud%0A{73lV8hXAo7xx;=8R+E#e-qkR&d|qVuy<>ccvG5%Ccc{Iv*T%YtWo|e2>(6{ ztM~3?Ud&UoapbjhGdVF->w|WUGC!ogEvX!@udVg9A^p!NA7~7djbRvDIx7#xo58l_ zP1>P%Qm-|pHt}EUqCBAD??`ATjz-Ku(>~KCwxrsIt^U7`)|9qm=M~^r`ZfP)2xv17 z>pu0@?+kM_P5E@XZZ)=^XxD--!31%B2|J&wrgBp{VVb?qP#2G50o3oegYmwb3RDZl&5I31n(5EM*V#B?#lh; zzs>k>4{fhgFF_^nV=49HkNYYq+4st;;kU@1Z!V8%61!+!n^F89;>>9MaV7k+8^n_pK>ue8yl_mHS zKA#rR?}qAfd5ulwOX$PsEyU6bEvdXm{YEefZKm8J-?xd~5~MTGZ-<|Xe&Olj`A>8{ zZJbNctlfrN)NcZZsqcUuYhZTtS54`FYO0B$@KO37*aYoe_-n8+`gPjTU!1NyO*^>Q z6aEtaRI+EmbJ+M_an-Xm6bod!Kqki0UVjge-BsORwTYNXSAgaGas<4D z%uevMD!+k>DGtmC__*+f3hcjXLtWTozp_Klv;)gJ}JwS5hoNha3a z4QPjIcaeS~w|t0ae=I`RJ81tx8wMuRF+?9#C^AmAooko5uj>hi}BA-P(*(=flayOHo0zTI7sqmN1zJ2ljsN9v@ z`|z7!E`F@%bHEgK3^uO+D9ih4V=!8|Gdf+x_WZRY9p6L$oQ~ek8EC1U`m^-3({^c{ z=MN&(&j}&_8CnnWN5bqY=fhjzFMxjr{|1h{-V925hFV*T(C<+{4UTc_M$cS$DZcnA zV|)X^ipEqv3;!7e|4IFv0Z0Em^d(lxzo`#~CxTdOC(zXdR%*Mpl2R97{*!nL|HmGi zE+yBD{vueWygw{o5+(SL8HuB!uj_R6|MWXW9&k1b|YKS+VGvLYeTgUpLa&<3qJ<>Dt8Q<=!yIj zV;Tb@w?!PyMz>C~e;Vlf3U97HVo%>o`AWLig9*x0@ZSQ>+VWBC^D$TDXJr2l#~d$1 zKLrl^zG;y^;f9KSz#w%wKCjo2=;4nq$s1#u0nZ1=(=)@`i?h__+8l#7N&k@tT9oOo zburF_*O#vKApA94y-j?qAoCvi2h_z&I*+eoUiyJ6Kxe#d=I;;^wpH-GU|aMa=y#xr{q#FJrYK*6Hir$tA4M*~u|Adn zw&r|3jchyPc@XbY^jH@si#NHWww?0S#=6gT!N0zu4o^dC0+AaE^_uctWFy`lC(~Km zfyxW%jhe_NzH}fQaTR-n zv8SJdN5LiJ=u20E^?=RgmTYS_&*RnQ%DDeXKUQAGpR?)ulXApTze@c%x0~_-Xc3Rk zf;czC_#Xz-tkYX6#z%Iz`imgu@Dp-d0&Ap9^dE=z5uUM^k-H@Tux8>YHgBdoG{kJRIYhj<+El&Z6l&ZS9*eXN7Uq8qxHZmbK(O zRBVG5bJUEM_^O+7>~BlxTT-c?`!7%*OJ2?^&%pmKoBxj13oUdVKyEB3m5;!GpIqp8 zhA$%yzpvbh?C6E5i2X|YJRhr#V;;IOq(a|ME6a67x2F0z>bL0Or^7(R@n6(Cf>=}J zYyIf*{E+)zr~Q+7%Z)SgV#Gz8II+%C%ta^gu67YCKLS^gF|Xw-{InV@MEj~LzTjOz z0|>jtUwH!@IfX4bcExj!^~rqwXF48G|Ay`hD)Iw7kL-Bm!RWCjqtEqZUsty`=5tAJ zbYm$i?C6drKc=`+Yz00#&c_Z9L-%|9@*QmZxfE*X=EY6VK z&@V!}kIiydJ||zNe6u#5waR9AcXF%AZAb20yn*UpqOVcD8t$p@Vc<&T^Fc@DW`5qj z>Yk6zuH+}8bp_UL`P)kU`Mhsao%bnw4kU97*ci-cF;;rkk&k$aJTjENTj`2(f5Hnt znzuMBmw!;71lXU~!XefA9&?WR-{=mV>_}6zF&FvFK8(x>XiwH<(rEO}m8XN)Yg+m3 zdgXb>xUD!d*1RwFuh!!~=6y2w3kdzA;aK+%0=AZutM#+?zVIsgV(j*WaykAaeX%?5 zpD(M6gM2pmtG@16pFy_SJT4(8UdubtRvZ8Hs=l>z_9*(a%I{Gpw;2B?%Hj83+MJ9Z zd;7!6-Ozf_|1^H-Y&{X}R%LOK&yNYdM*EY&Y_f6oJJ1|mM`lMbR{0GOYich3Q_2r3 z8(;ZP5H<`#Z=k189>Q;Tps!Mnobo+zEVv7F29b+;SL*%zQN1_W=fDrxTN>LaH2U*d zcRjo1+B_CHqMQZ(Dp$lk#_7t-K^4p+6KAJyv|Y-M^T1&A=<@_{Em#be>DzfTpE(;V z_4C1S^!fUYxo)qW7>RRUnxOnQ_-wEePfm?9TPmf*@?Bc2(~CgZ*%j{y`qzWV z8?h!MXS=t{XS&Gw62X$dns#Rcfl2)>5pgyd(@?)}X#^*d$^v1tL zzZ=ngu<~B;1pIO2V$6|)UsXOF?|pT7J3Xy_pK>qlW8I%(O#8yypsi%XYB=&l%-vM| zcEXSLNSiT@&R#&bjZGrWQggJHg?eV4BAU&MOsF&*K^Wi@lLM0pPoHb*Yona&9? zKV}~fMh|;OgMrE;qKqH9K2|=N{4{pOT&#nifrp`6yZPB+EA<}Q4aXB(<)iB6u@-sQ zoa8wO+s#Y)Z@9KFughXHe(p}@O1h3lA7f7Br1ZEpL(wBYU#s2;e*kWkV9XKM=_dFVK}dt?2)A@9aBM)`PEzC>H9U98n-tNH$|^lVku z&|0hhW_+tT(l5VcAK1@}UGdkFkF)Sabi`U}17a;=C7ADj%gRw$sPPvW3Gd>rv| ze$>TN*w_`w;pIE)lci}IadTXV-(wR3@cfPkDsN7DHOerFJ|-UmcJp}RP+H8tGH z{y*S#raRVXtcjSz9ng*H{eNZb_Tu~u(gE)^^i7)b`LEbXU(;LieiD0F_Tw3_ z@fESpL_BqEtlufoU5i*<&aPW4^*q)Q&s@iOx3)3)pX%!%IP%f)+P9mdW^qS zA2DaKCp?Z8YyCm>-gHd|5v!fi0?yvk= zi?M*1yTibN;8Z$phF5A6eto1;pZ_js5udxjW8i`07Awd6SiAW>!gg@1c8) zExA|V$gvUY4R~9_32hiXCE$m6PD&S{T?iVilS%4lD8Em>r}^v6?jzB{|5wo&zWoQC zFT%etPn+Z0yYn;rP&B!xwjtUm?MG{KKH1smv-S6LTCui^IPy^Go?NaxbG_NA6KjQ$EW)9fjAdUMQbP=1ka} zmwPH-j=r^W%+t5?nnGq2i2U?99Y^xpZQ2Y{pYp9uA@hQi@xOQ|J4J8?+gU zPDdM^wUO$VqE8}szVZX)E=Ipu`Ny#F<-5Go$QV<4*PIwro{LyR$0^HqMcDoYye-V` zyuQe#@LQZo#A7iQ-PmJ4OpnpsOZf=4M0|>k(wyeLTJYIgvnRznpZv@|6>TCN?pGuK z)ne@5!5fHv8M^gb#(YK`epmTbc)IUx_$Lhq5nJLko>$6Q_~I|Vt0*Ey?!(&@JVKA0 zo_+}ysmGc%{$xz)2<64bv0)|WtXSK%_UIR(uSHue--+A2erxJ+J`t1o9p%pIpQy)L zGlsYyjW}r@#}2rRSlb6oajw{)$#+rYOJ}j0;m#m(-D5 zWA3Z?-^II-{pLUR)HGc#lJm^^bJ$bZFZe#+=l0O{YW%6>o<@^r z;=9(gbCc(7_&o4mv@hAbmA)0>iz;sprm0V;0{HZ%RD2(FMt@CR zZ0C37|1gdP%EQ2=V0$pR#oj~D)yAXGIA^y-`2cOjO|c_9iajl4Z>Z*dKHvKcHqI{O zI^r958S{Ds=%gO8JBiL4_~2Oe*b}~o_5m2j|9g<%pYAK*0)H&}Wy&AH6?(gAM|bK6 zrt#Ymt@Zhs&(@)2_`cWz?*uSc{b;Zk5U)8;9cLVIpJhGfoW4Y#5jWSNuLiMqhMixd zMa=C8|BmcjFpsS-smE9b>+d46Gp%8{Esya%bYm%xZM7EJcD;H(5YLX!YWo7%uA1*2 zbBrIZJf8f&$h@g+&Y~XQMZ`OVeD7lo`A9FvN^J{z|0oviFQL0!Om->z!`?Wb$cN>B z!S8`RA)mLqYWq0e{y<)gXa0P@G`hu}jCUS9teW3V=X-^ihp=H5-ml=hzb%J+_@gf@ z|D|Wq^_|}>ZKys+eH`8jeN0q853jjz$~K?E-YIz2WA4j&DZNsuf2&}f=ezqjpTwH% zplx^X3Ht5uhvs}+;~T3UYoa$j-9Vh1OT5j*#zPf#@C==i2Ug=<2tN$Qpxs){@0s)a zpS_jOpzCw^4dD)WvkCJ&#yP4==-vc1#`et}2xD9w- zif6uJAo(TaW6pb5^1hhQp%D{J3Irb&}rl(u#XVxk3+x(zy-W$hh`z6_J)PGEFr1Bfp{JlppnEr^xd&vBaon6eS z{Ws1|@t!O1ALgfM@a@Y4vTu^TjP7yp$?$AAo)w4a^C)#|y!;(n0`t%!PGc`N_LBc= zkD$2Y#WS)5t{pcNM>J zM!vLaze4jKDaW#y$Y&wzzg&y=F#I&U0*+WY7F+YM!8NpZ@NZ!EpWq$AfoR>}O|)^>nntJ(rE@%;TdSAmbO@hc zti8F9d)(C1+<#Afi8(nMzQs3Tf>+3jpD)>}SfG;k^X^2gIKBE<0kceTNDpX;-`t~u_j_|=3Js}%u$?EjWeIKj;`isy7V%c*aOek z_I+({#s4#S4*fv1>%cxBe6%UtSsX|1be4#7MQLpo;yKO=j1_CfcL;vumYu4pu(r}k z%5%y84gGuM;*Nno^L?0@&EMqQNY_H}5HQBHb)BE@k(aao6A*buOovVRJiJ`zSwtsh>~y+4Eja&WmTodm?`zXuzEuhE|cwThgpK34g5*jyBcu{XwdsQMgzJWTIK#<4S+ zH63T-{H@(amHJs^Tfg;~gdcgmEBRleA3~3uSi3~uF;{nhX&~0FamPJV4L|Pe%6rr! zR_QNCptB*KVN1U9qpVc^9I&CRs`Fzpl+5OJJBzJg&+fHT-9Ox*|B>)m7%k3f(Wd5J zB)(tH`^iE&&csWw+@7D`KUWSPhL1cyr*Saf6vMUImwvX!{X*N_@EAP#s(x(I|4ey0;Eyy5?4|vm^xuUJb|v|vb}X7POQPLf!ZSOA1pmmZ<*^?rUVjjJ($mgK&eLM6QEA{Uzt@-@ibQ(MOJMBmI zXL$C6yp|&O%tnj+Hw-i@?+b^|wt?Shte@kj`97qB_Oa&Y>#HOGmQD5N*O-eqV>Qtg z`7iDnRw?&3zLn}DAD&{`Z|W)+v}%S zQ_cxFx9)}aFuC>cI{ggQuBfmTECLJA!oMdd_o>#;CL=1&@5-Zq^UsrCsd(ukx)t zmcll19M5j~F5yYM9h9wwat(Z0rGEaf_VYYO&g!V$V)O$*fAlGIzX^x$&PJOK*3fen z9rvTn0JHRaxB4ve3|->49H-4RxK!T`gpbG4G0_-yY|61$?62LM+Bd** zQ2rK0uCMJ!{=4Mmg|%b47-$5;$ll&kzc<^P{!QqW$Fh%OkMD+OZu8!D6hAFQi~PX$!rfB-y+hby zJr{8NHX*IY|EiMj2I4(%@e8!m(4Nu$b#SnHG6zFMn4a#Zw;EsYz$wCcd+&mv-g8N z$&4hsJNTxOpFiRqdAb!`0A`Ypxw{4LY_dX7Dd?Jw8aC$-aOW zd)i9z^f4W4(Dwsj!yvd>d`=TbOOzvqj<1So<+vN@^6lD*d2u!?wxloC&ItO=OOEwl zk#Ey(7JMgqg&Z3rzVi8@GkSOMN~QjtqcdcR`E0Jr8RWlKz7GF$cza+x`M&lJv{ht> z)aN_@=H+4hPtDVHaPZrV=_xwQXT*8A72A?>Z{-cx5a+SpWaiNGzIJb*zl|2L&;VM% zo?!c`X9DYBAMJmkUTPCMmV>W=cBz3(Cu18+zS;6SwD^y(|wKNBC8j{@^ux~nb5 zqlIn5$n6a#DNi-N-^2d^(VsjJzxAnwy^o`XujRv>2l~^o8UE}>>leL;zCQU@<}`hX zhhhkRKW$@8t)$ztZK>bFJk;1!{2KfcbOUUPwO7P5^4{PBzh9mTAENEy)%y2j2a)MZ zrU&|MXdmfgb7g*xcq*I7cEmpr?u<4YpPl(PVdgl`-HT{^mDb@$KQV?0coBOa8jH9t ztc{#wPpsN6(5rP`c`dv#UgWP*Y|b$kHMFbLM}g1H(;jp#pl>_%$gh$2Zd4Dyo)5MH zUG*a-BCnJ);fKleMvJ+BTL1hM&&c^K)3I7#Bdz@VJUzygpC$P>-ksz-VUa zWL{*$^>D=T1UT018T$JUot^1oM}Dr}678wr(~;0$gZEPJsNI2hZz;zaFP@Vh#+$6( zx3zvQjF_K5Z>-B-0E}K8GjKH8 zU*WL#g^KeDo8+6=yW;O{AdA{TlT3;0s_c&h=OmH>y9cZtsq7^z-)#1NHBGkk8TX0pi_sK3@(nZ`YDPm%eS$ zL)^jxH@UTeozaT3pS`P-LU(EkBqj(Zx@)JlGiOWm8qEtu|X!Ut(1 za3kKnfE~%Y&iUe4<>-SS;!KtAZ|5jCp~YEdD80X})Sst^qHT(IR3)FcN^ucqz5M%* z2Ipq}iQkgNJHULlcjk%xG2aVbNzYW}lRO*kqg~{PJ;;6oUR9ojzZty;D91dqJHG=w zTKP5OHs1UWc$KDovGp2~C_;xYAJo}BfM%y)Ld%@lC!{@PY&DE}xzWFoWhsQlge*Rd@&aRF23ViFayq)}~ z>TxG@1=?)jZFmrM_2wNBL20W8F?dTSvZ`{4lisXw%TlUHG>a=Zh8O*`D)U#LHN+yU{&e z`6T7-$;TetS-CU(3+*;z?;vd=79!tnPG*Ywe0V+RN&i!Lm%?9zmGs@uXPvd%3x5z^ zgQjDW!UY5e&kR1evkADj9(p2A9^jF57^dZ`lc*DW<$|K;N;E2^;%ICrH ztPuVZ6Zy`rv+^&|#ZP`WeuHxOOuzZ;67ew$?Lx3X+tHQ!JMJCGKgl<2$oq5n{cJK} zYpLy){KB{SF8pG2agyKlzJwRKVqa}HfM2TC-|fViZVNhE!ChdA=aGotE0rU*;$Kbw zXDM$0)`Jg#{F~N+qd+`2iItoeeyHA!&WOz&mA3&ABO}1G$_?oA#YNENi5wHN;brRk zfyfUxgVQSY`5dO*U)6_+z1QJ4)CZE+CO;P#SAMpB3w;EdwU+lEjc`x5fu22sjE(H-Az2F__l;h&QrWStQ8^}+wH{@?Ck7vIeQ}aAu&O;L?`Tp-e z%46x z?@V^YIvvfnqNVO%YcksAd#VfR+h6@;^&_>B|MEI>PZ8rNcBbDrU4M)R{<_e%afMm|rSfp!TWvMs(ztHu2M z9<5_bzMHIFinbxzF#Y`tUElfL{TzM7UVeyroGW^#{Smt3EYejy*3$yCB=5eh9P>I3F7vt=$wz;$m`mf{0WR1dF<}js)8N~|Z@@V9ufcW7g;Z;)*;C$XRZ!2HHJbvBwcUu>vdx1&+9k7fzIEV7HD(8=k$eLd>Lyn|9&UdOSE(Ti|;z}`%(Ek|ITADyWXxi zyTH!SDV`_(tsZC8nDY|E8vGJJ&NdyC!*?@a&rbOpsKhsqD!0)Y>#JEld<{SH$B0UP zX3EdA{2TX%wLP1>k5RU+%kl8F;9Rf~`g8CM?S2V=*jT@#ig}7${ta5#bTWK2I3FBU z&3(mleeHPm&85>iO&{a^Sosb3X7E8J)sCS5f6C2ZJ>IY3Ch%KuEI14}1LSY8!uM~0 z@Oisxj*WQUET17W2HXN-zdjakdQ08|^4>C2xw|%R;;#Z><40%{!D;4r_tyG(>?1O( zf&5p#N9H8u;nn(g=rP{dlj9sDFUHw6Vk&ZTu@PFEa!>LDD$eR~g)FVc%IS9 z<6HB2D(BeC(XPag_+Fw-oKt#KJ>HE{1*FK#O%NEC@o$&^$zp9UIl}C{gU&XHGY#Vj- z_)aF@$seTrJ+g5>5qna+m+Y=iHi^ITUi^p)IV<1Wd?j|Q$5MP1BgxFucPki4-|cvN zqaC2#)7rFv(fs>!^*@8p)ME{80b67FOm!3b1m!2uV~pPo8MF_FpI40^bRsLR$~dzgu1#m_B7DA<; z{8E1K+@9|*BChx|Jq{Ydr;Rz^$ya(Kbc8b35P;UnY@xeN{3*IlQ z`B}Dh34Sx3G4EZ$8n$eQ-=^FITi+%B7V=x6@8~1GManUFJO1&2Z}WRtdu7~t7m-IJ zUc)~x&@)NiiGBE(x}Ev^GqIZQWv!9;dmj00*pB=ZW4nRQ*!Sd~Tt5W=TC^Rs;rBA~ z;9F=t_$1<8ySz@Ux%l27KSS9A50)gU0pYXj#CFW-MEb*Ty~w;o$LHV|)%v@E zyOm=f+05TJn{3>tl8^N%mdbw09kpF=uI!gJXNowZmVcpl2fW)oQ^ft2`?k0j%jY_0 z-u#@qME{et?Zv0#=(0x2OX(LU`9A78{CWDF$&PKm)%|<4Y3!+KXD!F~81Wvme4bB- zx7P1i{!3;)zZGQGDz|T~-*w1)@y<2!e>o0)OAxs&&iz{}&nADK^3reVVlBVhZ*M%I zQ+`S9@t4cB@_sy<$NQbKpfhqnFY>Wwwqox@{Jp^)?EV%0I{dBhHY5MCb{)VFa{chs z<2`nIk?zs@2)jN}-WUBXApfWK#yFMzlj*ci6wYe#FMcnVzgHSWCeHEm)a?cFZZFPb z#irzpKi@Mwfp;dkIP*OSFC){JOgr_F`jcadI5$q=pBc&>l%#~#0wuAeBsqfPA1kwe0k@ZWgQ3RaP80?}?3EN{oREwSI_ z*!`9IH+Y|e80*_0;^2pH3CH=tI>_I2ZV&ItUq9f_ca<*yj~iQ8bG!kFISKi$z+7fu zMjP=Gdv`7N?8wR1MtU1W4ry$v->J7J7iYX^e+eAz+RVcr$=!>lkNiCK3-!&wgP0pkw3&hHJ{EO82GoB}k1IWrpMGtyv{MCuQ`z!N(K3nWUt`s+a$BW$b z0{TzMj|cJm^+)_Sf$_$3W!&w@?`~>);Qdy8r;2$u&hFZFWH-IBrV9Mj1Y({m>Yk5k zCxVO7wxK&eU!XUtN1VFns6`A;Hs+6v#oX3@sD26A^T2WVCy?75O$^pT_hDcXxEnpz z?r?1nVNW-6yj-0PwITTCD4s!!o0Ox!`Rv>nZ!kC*Zy);Oo~7CTSU6*?(DqBPp|QTF zowX9b<;-K8z%I{!wOAwih`;Mue!s3K@k`tXG+%@EH}e10{#o)d2XS7E9Ckd}@ycQ^ zbkvR^Tf#A(h+DBzi}f2Z6}jLG{a=A5&P)C&D=p>%159MLzS~tIZ{q`u*R0<#V(RyMBxo zd&g(|aSz;wJYM9TSa)R`{pLU4<&<(^#A`eQEU(mmm-DVRM}byw1lXlMmV7qbt6H~r z7`#$DF`R#^^GkB0(PEDpr+j=R|GqWe5#(=ChLM|v*8_Zv|02AkvHor1t?Hxjdy#z# z{0uAxJ67_$-g36`llV1lc4qr-Y`2f)`{J?2EDqvbNqoncV>0s9(`4=av5xY&dJdZW zlFuAtl=stSB)m6W{Xo8hs??vQ(aVSzu^oG3oX^S|@lsPg7ye8?e?woQefP%tJEu|b zI53fqW6g}gkDSG>IG@CP)Q)Y+znzGf&ihJD`x)*~HfgFu=s&cQ?{wlFX8E`_+o64g zwvYOqK+ewJ%JxFPgYMCC<~R5+f%YJB??!m{D_;fTnIg^vuWI`Uh`G5MoC~VpLpD4I z`}QG!i~5H0R{A)Utxqe*o-n2!7v;{;Cz@yC{B2(BCGo7ZG1(G*IGBR=86Rv$N5~J< zz66bUeZa?j`#S6_AJ4@3>_}(1XH$OXSKCFKF6!;*?5F%9`##owr>e1NH&VG-+l$3l ze|-Ixk5}vW*Q=DLvvntJ`6|cdx|mDliDW;`Ym7}TFhMoUDv^<>0=DsN&nV!d^=sTJAd1?Yn6}4N1k(ERi1BM zHo)Q{uZ^W-kJsLs3SFgf=Wjaxqa69|ALQDB_vskUAG@M`ju&}iC@}W2FWO(=H`HTY zBr^?+L!Svd@8)-1kppMY?@W^K60TAo#Ga6GhA7&#ibw5d>i<5px8RxN zx2uRd`$R{wzd>KkZg%A}SHz*U6VHU{CD2nFbDr;jJ)6W`b?i0y9B@gc{@yy~z2w6L zh8q7`Z6dZ8(-Zw#gYj)|+;Qc-c!_$OHV?va20j`72JgStROIch`k1XQ2gH4Bes6si zEH~ueBE5+od)Rm2$N~F^*%QU?NNqcztwS5895ytA%f#Lp>akylxpGVO$Iu^9jy?Ui z%GOhSUs9Z`{xkfzrUCvjwfO!la$B6c;x{(M9%u(Ex8R2z_WZ~%#gFxCZ_MvS9w1+HcYHmb zoS7mI(G$-qvCr2IQT_dbQjt|WT`9coJd(O3J zv5$v6&M(F0=yXQTDUMZ+`7k&6JGtm@LwZ)B%>e&Ke-bR!_hEQ&-ecnY(gWQb)XpNm zf%+Q#UW_J(Cwkv!Pi1D0U|ywPmxp#IRevU{@SD7>GlPXeRKzpZ`vthfG~D&j){suWov!@7GFNg&r-%vbZyP|S%KrsQ42rEIQAkF^D4KbyVd)F zFTh+7^~II?8f;?gTb2CHVltnpk3P3jKTP>XeMQW@PIh~GV{BK0_S%@k{5)_0+?8DH zl`pWT6S>dH%b$4+{f*@syhFj)ApDrvwTzBq$XEyE7nQtj%0smMzH;b~F%N91-#hk% zBbN3A@^AjeB>Ii#kD^kK$079mQ~5@~hdE|XgntdqeA_+|x~bz2h$`J1Tl_0i^8%*XqK@&ok=Abjyi zMT*0VwIXKXJx!j2$CYape%9Z^%BO%CU?YCAj%xeTGYW)HdZM4#lHbYZZwQyEzt6Uz z@XUR|6F`~PM(WcC*g~T$6K}Oh9AC| zAHFb#r|FK^-U)4loFfnA_cV{GzePS`dNc_AY|G=1+%;Za;G^O)bZaSoOPO{$Lp^FpsfjY6p=6@&9YZ{{<9zuyzhuqE1Kry`|z{ch~pA{8Z~nR|~l1+wWN?(o41$ zb2c3Pdhjdto7890dyjS#z+>v+$Nv~3KaaRo*&G-A5P!2O=(WguXDAOO&sTXY)@i;M z9zgC~`ex%b;k`$mOvBCE{0Psu<9$V8zo;D#%xCQa_3hwm;fKJD#xYLYL%~!wJc4%^ z9e2?Y@F{!PU-Saokg4iBVo8j|JNzQ@hx{1teb{f8I-KX@nIu2g#Ta{m+4%QW@^@qTKKMtVtZqF2>N{Rpz=8&zUztpx`U0Y^?8f>vEU3ayjuTGZ=F0| zs>j*7Cz^dJtp$ZxYB2t5l-q&c)__ literal 0 HcmV?d00001 diff --git a/docs/source/features/speculative_decoding/audio_eagle.md b/docs/source/features/speculative_decoding/audio_eagle.md new file mode 100644 index 00000000..64d5666c --- /dev/null +++ b/docs/source/features/speculative_decoding/audio_eagle.md @@ -0,0 +1,140 @@ +# 语音模型EAGLE3训练 +## TTS + +### 1. 支持模型列表 +- `Fun-CosyVoice3` + +### 2. 数据组织形式 + +所有数据需保存在`jsonl`文件中,数据格式可参考: +- 原始训练数据示例:`AngelSlim/dataset/tts_fake_data/train.jsonl` +- 重采样训练数据示例:`AngelSlim/dataset/tts_fake_data/train_regenerate.jsonl` + +#### 2.1 原始训练数据 + +每行字段意义如下: +- `text`:输入文本 +- `audio_path`:真实音频绝对路径 +- `instruct`:发音人文本表示 +- `instruct_audio_path`:发音人音频绝对路径 + +#### 2.2 重采样训练数据(推荐) + +为得到高质量的目标模型SFT数据,建议使用目标模型重新采样训练数据,将LLM生成的语音token保存在`jsonl`文件中,每行字段意义如下: +- `text`:输入文本 +- `audio_tokens`:生成的语音token +- `instruct`:发音人文本表示 +- `instruct_audio_path`:发音人音频绝对路径 + + +### 3. 训练Eagle3模型 + +目前仅支持在线训练模式。 + +#### 3.1 在线训练 + +使用下面的脚本进行Eagle3模型的在线训练: + +```shell +bash scripts/speculative/train_eagle3_tts_online.sh +``` + +**脚本参数说明:** + +在使用前,需要在脚本中配置以下参数: + +- `TARGET_MODEL_NAME_OR_PATH`: 目标模型的HF名称或本地名称 +- `DRAFT_MODEL_CONFIG_PATH`: 草稿模型的config路径 +- `TRAIN_DATA_PATH`: 训练数据路径 +- `OUTPUT_DIR`: Eagle3模型输出路径 +- `MODEL_MAX_LENGTH`: 训练数据的最大长度 + + +### 4. HF基准测试 + +`Fun-CosyVoice3`仅支持HF测试平均接收长度。 + +#### 4.1.1 基本用法 + +使用 `tools/spec_benchmark.py` 脚本进行投机采样基准测试: + +```shell +python3 tools/spec_benchmark.py \ + --base-model-path ${BASE_MODEL_PATH} \ + --eagle-model-path ${EAGLE_MODEL_PATH} \ + --model-id ${MODEL_ID} \ + --mode both +``` + +#### 4.1.2 参数说明 + +**模型配置参数:** +- `--base-model-path`: 基础模型路径(必需) +- `--eagle-model-path`: Eagle辅助模型路径(必需) +- `--model-id`: 模型标识符(必需) + +**基准测试配置:** +- `--bench-name`: 基准数据集名称,可参考【`tts_fake_data`】 +- `--mode`: 执行模式,可选 `eagle`(仅投机采样)、`baseline`(仅基线)、`both`(两者都执行),默认为 `both` +- `--output-dir`: 结果输出目录 + +**生成参数:** +- `--temperature`: 采样温度,默认为 1.0 +- `--max-new-token`: 最大生成token数,默认为 1024 +- `--total-token`: 草稿树中的总节点数,默认为 60 +- `--depth`: 树深度,默认为 5 +- `--top-k`: Top-k采样,默认为 10 +- `--generate-audio`: 是否生成最终音频 + +**硬件配置:** +- `--num-gpus-per-model`: 每个模型使用的GPU数量,默认为 1 +- `--num-gpus-total`: 总GPU数量,默认为 1 +- `--max-gpu-memory`: 每个GPU的最大内存限制 + +**其他设置:** +- `--seed`: 随机种子,默认为 42 +- `--question-begin`: 问题起始索引(用于调试) +- `--question-end`: 问题结束索引(用于调试) +- `--no-metrics`: 跳过自动指标计算 + +**注意事项:** +- `--bench-name`: 也可以添加自定义测试集,在`AngelSlim/dataset`目录下创建新的子目录并将目录名作为`--bench-name`,在新目录下创建`question.jsonl`,框架会自动读取该文件 +- `--temperature`: `Fun-CosyVoice3`在`temperature`为0时容易生成大量重复token,建议测试时使用默认配置 + +#### 4.1.3 使用示例 + +**完整基准测试:** +```shell +python3 tools/spec_benchmark.py \ + --base-model-path /path/to/base/model \ + --eagle-model-path /path/to/eagle/model \ + --model-id cosyvoice3 \ + --mode both \ + --output-dir ./results \ + --deploy-backend pytorch_tts \ + --generate-audio +``` + +**注意事项:** +- `Fun-CosyVoice3`在设置`generate-audio`为`True`时需要额外导入`cosyvoice`包,安装步骤如下: + ```shell + git clone https://github.com/FunAudioLLM/CosyVoice + pip install hyperpyyaml omegaconf conformer diffusers hydra-core lightning gdown matplotlib wget x-transformers pyworld librosa + ``` + + 测试脚本参考: + ```shell + export PYTHONPATH=/xxx/CosyVoice:/xxx/CosyVoice/third_party/Matcha-TTS:$PYTHONPATH + python3 tools/spec_benchmark.py [ARGS] + ``` + +**不生成音频:** +```shell +python3 tools/spec_benchmark.py \ + --base-model-path /path/to/base/model \ + --eagle-model-path /path/to/eagle/model \ + --model-id cosyvoice3 \ + --mode both \ + --output-dir ./results \ + --deploy-backend pytorch_tts +``` diff --git a/requirements/requirements_multimodal.txt b/requirements/requirements_multimodal.txt index 621f664f..6bf3bc5b 100644 --- a/requirements/requirements_multimodal.txt +++ b/requirements/requirements_multimodal.txt @@ -1,3 +1,9 @@ qwen_vl_utils==0.0.11 qwen_omni_utils -mistral_common \ No newline at end of file +torchaudio +openai-whisper +onnxruntime-gpu +inflect +wetext +librosa +mistral_common diff --git a/scripts/speculative/train_eagle3_tts_online.sh b/scripts/speculative/train_eagle3_tts_online.sh new file mode 100644 index 00000000..302ab470 --- /dev/null +++ b/scripts/speculative/train_eagle3_tts_online.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +export CONFIG_DIR=angelslim/compressor/speculative/train/configs +export TARGET_MODEL_NAME_OR_PATH= +export DRAFT_MODEL_CONFIG_PATH=$CONFIG_DIR/cosyvoice3-llm-eagle3.json +export TRAIN_DATA_PATH= +export OUTPUT_DIR= +export RUN_NAME= +export MODEL_MAX_LENGTH= + +torchrun --nproc_per_node=8 tools/train_eagle3_online.py \ + --modal_type TTS \ + --target_model_name_or_path $TARGET_MODEL_NAME_OR_PATH \ + --draft_model_config_path $DRAFT_MODEL_CONFIG_PATH \ + --train_data_path $TRAIN_DATA_PATH \ + --output_dir $OUTPUT_DIR \ + --num_train_epochs 20 \ + --per_device_train_batch_size 1 \ + --per_device_eval_batch_size 1 \ + --gradient_accumulation_steps 1 \ + --save_strategy "steps" \ + --save_steps 1000 \ + --learning_rate 1e-4 \ + --weight_decay 0.0 \ + --warmup_ratio 0.1 \ + --lr_scheduler_type "constant" \ + --logging_steps 20 \ + --model_max_length $MODEL_MAX_LENGTH \ + --training_time_test_length 4 \ + --deepspeed $CONFIG_DIR/deepspeed_zero3.json \ + --report_to wandb \ + --run_name $RUN_NAME \ \ No newline at end of file diff --git a/tools/spec_benchmark.py b/tools/spec_benchmark.py index f56da48e..dcb00c9d 100644 --- a/tools/spec_benchmark.py +++ b/tools/spec_benchmark.py @@ -50,7 +50,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--deploy-backend", type=str, - choices=["pytorch", "vllm"], + choices=["pytorch", "pytorch_tts", "vllm"], default="pytorch", help="Backend for deployment (pytorch or vllm)", ) @@ -121,6 +121,9 @@ def parse_args() -> argparse.Namespace: default=1, help="Tensor parallel size for draft model (vllm only)", ) + parser.add_argument( + "--generate-audio", action="store_true", help="whether or not generate audio" + ) return parser.parse_args() @@ -155,10 +158,11 @@ def main(): "top_p": args.top_p, "top_k": args.top_k, "depth": args.depth, + "generate_audio": args.generate_audio, } # Add backend-specific parameters - if args.deploy_backend == "pytorch": + if "pytorch" in args.deploy_backend: config_dict.update( { "total_token": args.total_token, diff --git a/tools/train_eagle3_online.py b/tools/train_eagle3_online.py index 02b1b2e2..f16d1612 100644 --- a/tools/train_eagle3_online.py +++ b/tools/train_eagle3_online.py @@ -40,7 +40,7 @@ def parse_args(): "--modal_type", type=str, default="LLM", - choices=["LLM", "VLM", "Audio"], + choices=["LLM", "VLM", "Audio", "TTS"], help="Modal type: LLM for language models, VLM for vision-language models", ) model_group.add_argument( From 2dfaa6ff6edc0ff554447bd26e0d8fa24e68476d Mon Sep 17 00:00:00 2001 From: ali-88123 <1940747290@qq.com> Date: Tue, 13 Jan 2026 13:52:17 +0800 Subject: [PATCH 2/2] merge funcs in spec-inference --- .../benchmark/pytorch/benchmark_engine.py | 2 + .../pytorch/generate_baseline_answer.py | 72 +++- .../pytorch/generate_eagle_answer.py | 68 +++- .../inference/models/eagle3/__init__.py | 2 +- .../inference/models/eagle3/draft/__init__.py | 8 +- .../models/eagle3/draft/base_model.py | 28 +- .../models/eagle3/draft/llama3_eagle3.py | 284 +------------- .../inference/models/eagle3/eagle3_model.py | 351 +++++------------- .../eagle3/target/modeling_cosyvoice3_kv.py | 71 +--- .../compressor/speculative/utils/__init__.py | 6 - .../compressor/speculative/utils/util.py | 180 ++------- angelslim/engine.py | 6 +- .../speculative_decoding/audio_eagle.md | 140 ------- tools/spec_benchmark.py | 6 +- 14 files changed, 307 insertions(+), 917 deletions(-) delete mode 100644 docs/source/features/speculative_decoding/audio_eagle.md diff --git a/angelslim/compressor/speculative/benchmark/pytorch/benchmark_engine.py b/angelslim/compressor/speculative/benchmark/pytorch/benchmark_engine.py index a54dfaab..5b35d45a 100644 --- a/angelslim/compressor/speculative/benchmark/pytorch/benchmark_engine.py +++ b/angelslim/compressor/speculative/benchmark/pytorch/benchmark_engine.py @@ -82,6 +82,7 @@ class BenchmarkConfig: batch_size: int = 1 # TTS settings + is_tts: bool = False generate_audio: bool = False @@ -351,6 +352,7 @@ def _create_args_namespace(self, mode: str) -> argparse.Namespace: args.early_stop_method = self.config.early_stop_method # TTS settings + args.is_tts = self.config.is_tts args.generate_audio = self.config.generate_audio return args diff --git a/angelslim/compressor/speculative/benchmark/pytorch/generate_baseline_answer.py b/angelslim/compressor/speculative/benchmark/pytorch/generate_baseline_answer.py index bc28c28d..dba559e4 100644 --- a/angelslim/compressor/speculative/benchmark/pytorch/generate_baseline_answer.py +++ b/angelslim/compressor/speculative/benchmark/pytorch/generate_baseline_answer.py @@ -174,9 +174,12 @@ def process_tts_conversation_turn( qs: str, temperature: float, path: str, + is_cosyvoice3: bool = False, + max_token_text_ratio: float = 20.0, + min_token_text_ratio: float = 2.0, ) -> Dict[str, Any]: """Process a single question""" - if "cosyvoice3" in model_id: + if is_cosyvoice3: prompt_text = model.base_model.frontend.text_normalize( qs["prompt_text"], split=False, text_frontend=True ) @@ -199,12 +202,56 @@ def process_tts_conversation_turn( torch.cuda.synchronize() start_time = time.time() + dtype = model_input["text"].dtype + device = model_input["text"].device + + input_ids = torch.concat( + [ + model.base_model.model.llm.sos_id.unsqueeze(dim=0) + .to(dtype) + .to(device), + model_input["prompt_text"], + model_input["text"], + model.base_model.model.llm.task_token.unsqueeze(dim=0) + .to(dtype) + .to(device), + model_input["llm_prompt_speech_token"], + ], + dim=1, + ) + + # concat llm input embedding + text = torch.concat( + [model_input["prompt_text"], model_input["text"]], dim=1 + ) + text_emb = model.base_model.model.llm.llm.model.model.embed_tokens(text) + sos_emb = model.base_model.model.llm.speech_embedding.weight[ + model.base_model.model.llm.sos + ].reshape(1, 1, -1) + task_id_emb = model.base_model.model.llm.speech_embedding.weight[ + model.base_model.model.llm.task_id + ].reshape(1, 1, -1) + if model_input["llm_prompt_speech_token_len"][0].item() != 0: + prompt_speech_token_emb = model.base_model.model.llm.speech_embedding( + model_input["llm_prompt_speech_token"] + ) + else: + prompt_speech_token_emb = torch.zeros( + 1, 0, model.base_model.model.llm.llm_input_size, dtype=text.dtype + ).to(device) + inputs_embeds = torch.concat( + [sos_emb, text_emb, task_id_emb, prompt_speech_token_emb], dim=1 + ) + min_len = int(model_input["text"].shape[1] * min_token_text_ratio) + max_decode_steps = int(model_input["text"].shape[1] * max_token_text_ratio) + output_ids, new_token, idx = model.naive_generate( - text=model_input["text"], - prompt_text=model_input["prompt_text"], - llm_prompt_speech_token=model_input["llm_prompt_speech_token"], + input_ids=input_ids, + inputs_embeds=inputs_embeds, temperature=temperature, log=True, + min_len=min_len, + max_decode_steps=max_decode_steps, ) torch.cuda.synchronize() @@ -266,6 +313,7 @@ def generate_answer_for_question_tts( num_choices: int, temperature: float, path: str, + is_cosyvoice3: bool = False, ) -> List[Dict[str, Any]]: """Generate answers for a single question with multiple choices""" choices = [] @@ -273,7 +321,12 @@ def generate_answer_for_question_tts( torch.manual_seed(i) result = process_tts_conversation_turn( - model, model_id, question, temperature, path + model, + model_id, + question, + temperature, + path, + is_cosyvoice3, ) choices.append( @@ -310,11 +363,14 @@ def warmup_tts_lm( question: Dict[str, Any], temperature: float, path: str, + is_cosyvoice3: bool = False, ) -> None: """Warm up the model before actual evaluation""" for _ in range(3): torch.manual_seed(0) - process_tts_conversation_turn(model, model_id, question, temperature, path) + process_tts_conversation_turn( + model, model_id, question, temperature, path, is_cosyvoice3 + ) print("Warmup done") @@ -364,8 +420,10 @@ def get_tts_answers( ) -> None: """Generate answers for a batch of questions""" config = EvaluationConfig(args) + is_cosyvoice3 = False if os.path.exists(os.path.join(args.base_model_path, "cosyvoice3.yaml")): model = initialize_cosycoice3_model(config) + is_cosyvoice3 = True if questions: current_file = os.path.abspath(__file__) @@ -376,6 +434,7 @@ def get_tts_answers( questions[0], temperature, os.path.join(project_root, "dataset", args.bench_name), + is_cosyvoice3, ) os.makedirs(os.path.dirname(answer_file), exist_ok=True) @@ -389,6 +448,7 @@ def get_tts_answers( num_choices, temperature, os.path.join(project_root, "dataset", args.bench_name), + is_cosyvoice3, ) with open(os.path.expanduser(answer_file), "a") as fout: diff --git a/angelslim/compressor/speculative/benchmark/pytorch/generate_eagle_answer.py b/angelslim/compressor/speculative/benchmark/pytorch/generate_eagle_answer.py index f688ce90..2b789b7a 100644 --- a/angelslim/compressor/speculative/benchmark/pytorch/generate_eagle_answer.py +++ b/angelslim/compressor/speculative/benchmark/pytorch/generate_eagle_answer.py @@ -113,6 +113,7 @@ def initialize_cosycoice3_model(config: EvaluationConfig) -> CosyVoice3Eagle3Mod top_k=config.top_k, device_map="auto", torch_dtype="auto", + early_stop_method=config.early_stop_method, generate_audio=config.generate_audio, ) model.eval() @@ -175,9 +176,10 @@ def process_tts_conversation_turn( qs: str, temperature: float, path: str, + is_cosyvoice3: bool = False, ) -> Dict[str, Any]: """Process a single question""" - if "cosyvoice3" in model_id: + if is_cosyvoice3: prompt_text = model.base_model.frontend.text_normalize( qs["prompt_text"], split=False, text_frontend=True ) @@ -200,12 +202,53 @@ def process_tts_conversation_turn( torch.cuda.synchronize() start_time = time.time() + dtype = model_input["text"].dtype + device = model_input["text"].device + + input_ids = torch.concat( + [ + model.base_model.model.llm.sos_id.unsqueeze(dim=0) + .to(dtype) + .to(device), + model_input["prompt_text"], + model_input["text"], + model.base_model.model.llm.task_token.unsqueeze(dim=0) + .to(dtype) + .to(device), + model_input["llm_prompt_speech_token"], + ], + dim=1, + ) + + # concat llm input embedding + text = torch.concat( + [model_input["prompt_text"], model_input["text"]], dim=1 + ) + text_emb = model.base_model.model.llm.llm.model.model.embed_tokens(text) + sos_emb = model.base_model.model.llm.speech_embedding.weight[ + model.base_model.model.llm.sos + ].reshape(1, 1, -1) + task_id_emb = model.base_model.model.llm.speech_embedding.weight[ + model.base_model.model.llm.task_id + ].reshape(1, 1, -1) + if model_input["llm_prompt_speech_token_len"][0].item() != 0: + prompt_speech_token_emb = model.base_model.model.llm.speech_embedding( + model_input["llm_prompt_speech_token"] + ) + else: + prompt_speech_token_emb = torch.zeros( + 1, 0, model.base_model.model.llm.llm_input_size, dtype=text.dtype + ).to(device) + inputs_embeds = torch.concat( + [sos_emb, text_emb, task_id_emb, prompt_speech_token_emb], dim=1 + ) + output_ids, new_token, idx, accept_length_list = model.eagle_generate( - text=model_input["text"], - prompt_text=model_input["prompt_text"], - llm_prompt_speech_token=model_input["llm_prompt_speech_token"], + input_ids=input_ids, + inputs_embeds=inputs_embeds, temperature=temperature, log=True, + is_cosyvoice3=True, ) torch.cuda.synchronize() @@ -271,6 +314,7 @@ def generate_answer_for_question_tts( num_choices: int, temperature: float, path: str, + is_cosyvoice3: bool = False, ) -> List[Dict[str, Any]]: """Generate answers for a single question with multiple choices""" choices = [] @@ -278,7 +322,12 @@ def generate_answer_for_question_tts( torch.manual_seed(i) result = process_tts_conversation_turn( - model, model_id, question, temperature, path + model, + model_id, + question, + temperature, + path, + is_cosyvoice3, ) choices.append( @@ -316,11 +365,14 @@ def warmup_tts_lm( question: Dict[str, Any], temperature: float, path: str, + is_cosyvoice3: bool = False, ) -> None: """Warm up the model before actual evaluation""" for _ in range(3): torch.manual_seed(0) - process_tts_conversation_turn(model, model_id, question, temperature, path) + process_tts_conversation_turn( + model, model_id, question, temperature, path, is_cosyvoice3 + ) print("Warmup done") @@ -370,8 +422,10 @@ def get_tts_answers( ) -> None: """Generate answers for a batch of questions""" config = EvaluationConfig(args) + is_cosyvoice3 = False if os.path.exists(os.path.join(args.base_model_path, "cosyvoice3.yaml")): model = initialize_cosycoice3_model(config) + is_cosyvoice3 = True if questions: current_file = os.path.abspath(__file__) @@ -382,6 +436,7 @@ def get_tts_answers( questions[0], temperature, os.path.join(project_root, "dataset", args.bench_name), + is_cosyvoice3, ) os.makedirs(os.path.dirname(answer_file), exist_ok=True) @@ -395,6 +450,7 @@ def get_tts_answers( num_choices, temperature, os.path.join(project_root, "dataset", args.bench_name), + is_cosyvoice3, ) with open(os.path.expanduser(answer_file), "a") as fout: diff --git a/angelslim/compressor/speculative/inference/models/eagle3/__init__.py b/angelslim/compressor/speculative/inference/models/eagle3/__init__.py index 37604579..e16f74ab 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/__init__.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/__init__.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .eagle3_model import CosyVoice3Eagle3Model, Eagle3Model +from .eagle3_model import Eagle3Model, CosyVoice3Eagle3Model # isort: skip __all__ = ["Eagle3Model", "CosyVoice3Eagle3Model"] diff --git a/angelslim/compressor/speculative/inference/models/eagle3/draft/__init__.py b/angelslim/compressor/speculative/inference/models/eagle3/draft/__init__.py index 9055c65e..341b0b4e 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/draft/__init__.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/draft/__init__.py @@ -12,6 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .llama3_eagle3 import CosyVoice3Llama3Eagle3Drafter, Llama3Eagle3Drafter +# isort: off +from .llama3_eagle3 import ( + Llama3Eagle3Drafter, + CosyVoice3Llama3Eagle3Drafter, +) + +# isort: on __all__ = ["Llama3Eagle3Drafter", "CosyVoice3Llama3Eagle3Drafter"] diff --git a/angelslim/compressor/speculative/inference/models/eagle3/draft/base_model.py b/angelslim/compressor/speculative/inference/models/eagle3/draft/base_model.py index cd4108c8..775023c1 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/draft/base_model.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/draft/base_model.py @@ -177,6 +177,7 @@ def topK_genrate( self, hidden_states: Tensor, input_ids: Tensor, + inputs_embeds: Optional[Tensor] = None, logits_processor: Optional[Any] = None, ) -> Tuple[Tensor, Tensor, Tensor, Tensor]: """ @@ -204,18 +205,24 @@ def topK_genrate( sample_token = input_ids[:, -1] input_ids = input_ids[:, 1:] self.initial_position_id = input_ids.shape[1] + if inputs_embeds is not None: + inputs_embeds = inputs_embeds[:, 1:] + assert input_ids.shape[1] == inputs_embeds.shape[1] self.reset() # Generate initial hidden states and tokens last_hidden, past_key_values, early_stop_signal = self._get_initial_hidden( - hidden_states, input_ids + hidden_states, input_ids, inputs_embeds ) self.stable_kv = past_key_values # Generate first level of tokens topk_index, scores = self._get_topk_tokens(last_hidden) - scores_list.append(scores[None]) + if len(scores.shape) == 1: + scores_list.append(scores[None]) + else: + scores_list.append(scores) parents_list.append(torch.zeros(1, dtype=torch.long, device=scores.device)) # Handle vocabulary mapping if needed @@ -272,7 +279,7 @@ def topK_genrate( ) def _get_initial_hidden( - self, hidden_states: Tensor, input_ids: Tensor + self, hidden_states: Tensor, input_ids: Tensor, inputs_embeds: Tensor = None ) -> Tuple[Tensor, Any]: """Get initial hidden states and past key values.""" if hasattr(self, "stable_kv") and self.stable_kv is not None: @@ -280,11 +287,19 @@ def _get_initial_hidden( outputs = self( hidden_states, input_ids=input_ids[:, kv_len:], + inputs_embeds=( + inputs_embeds[:, kv_len:] if inputs_embeds is not None else None + ), past_key_values=self.stable_kv, use_cache=True, ) else: - outputs = self(hidden_states, input_ids=input_ids, use_cache=True) + outputs = self( + hidden_states, + input_ids=input_ids, + inputs_embeds=inputs_embeds, + use_cache=True, + ) out_hidden, past_key_values, early_stop_signal = outputs return out_hidden[:, -1], past_key_values, early_stop_signal @@ -332,7 +347,10 @@ def _process_tree_level( # Get top-k tokens for this level topk_index, topk_p = self._get_topk_tokens(out_hidden[0]) - cu_scores = topk_p + scores[:, None] + if len(scores.shape) == 1: + cu_scores = topk_p + scores[:, None] + else: + cu_scores = topk_p + scores # Select best candidates topk_cs = torch.topk(cu_scores.view(-1), self.top_k, dim=-1) diff --git a/angelslim/compressor/speculative/inference/models/eagle3/draft/llama3_eagle3.py b/angelslim/compressor/speculative/inference/models/eagle3/draft/llama3_eagle3.py index 8f3d3453..ba819c66 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/draft/llama3_eagle3.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/draft/llama3_eagle3.py @@ -14,7 +14,7 @@ import math import os -from typing import Any, List, Optional, Tuple +from typing import List, Optional, Tuple import torch import torch.nn.functional as F @@ -639,8 +639,9 @@ def forward( seq_length_with_past = seq_length past_key_values_length = 0 - with torch.no_grad(): - inputs_embeds = self.embed_tokens(input_ids) + if inputs_embeds is None: + with torch.no_grad(): + inputs_embeds = self.embed_tokens(input_ids) if past_key_values is not None: past_key_values_length = past_key_values[0][0].shape[2] @@ -674,7 +675,9 @@ def forward( past_key_values_length, ) - inputs_embeds = inputs_embeds.to(hidden_states.dtype) + dtype = self.fc.weight.dtype + inputs_embeds = inputs_embeds.to(dtype) + hidden_states = hidden_states.to(dtype) if hidden_states.shape[-1] != inputs_embeds.shape[-1]: hidden_states = self.fc(hidden_states) early_stop_signal: Optional[torch.Tensor] = None @@ -728,282 +731,9 @@ def load_embed(self, path: str) -> None: with torch.no_grad(): self.embed_tokens.weight.copy_(embed_tokens_weight) - def _get_initial_hidden( - self, hidden_states: Tensor, input_ids: Tensor, inputs_embeds: Tensor - ) -> Tuple[Tensor, Any]: - """Get initial hidden states and past key values.""" - if hasattr(self, "stable_kv") and self.stable_kv is not None: - kv_len = self.stable_kv[0][0].shape[2] - outputs = self( - hidden_states, - input_ids=input_ids[:, kv_len:], - inputs_embeds=inputs_embeds[:, kv_len:], - past_key_values=self.stable_kv, - use_cache=True, - ) - else: - outputs = self( - hidden_states, - input_ids=input_ids, - inputs_embeds=inputs_embeds, - use_cache=True, - ) - out_hidden, past_key_values = outputs - - return out_hidden[:, -1], past_key_values - - @torch.no_grad() - def topK_genrate( - self, - hidden_states: Tensor, - input_ids: Tensor, - inputs_embeddings: Tensor, - logits_processor: Optional[Any] = None, - ) -> Tuple[Tensor, Tensor, Tensor, Tensor]: - """ - Generate tokens using top-K speculative decoding. - - Args: - hidden_states: Hidden states from the target model - input_ids: Input token IDs - logits_processor: Optional logits processor - - Returns: - Tuple containing: - - draft_tokens: Generated draft tokens - - retrieve_indices: Indices for retrieving tokens from tree - - tree_mask: Mask for tree attention - - tree_position_ids: Position IDs in the tree - """ - # Initialize data structures - scores_list = [] - parents_list = [] - ss_token = [] - - # Prepare input and initialize tree - input_ids = input_ids.to(hidden_states.device) - sample_token = input_ids[:, -1] - input_ids = input_ids[:, 1:] - inputs_embeddings = inputs_embeddings[:, 1:] - self.initial_position_id = input_ids.shape[1] - - assert input_ids.shape[1] == inputs_embeddings.shape[1] - - self.reset() - - # Generate initial hidden states and tokens - last_hidden, past_key_values = self._get_initial_hidden( - hidden_states, input_ids, inputs_embeddings - ) - self.stable_kv = past_key_values - - # Generate first level of tokens - topk_index, scores = self._get_topk_tokens(last_hidden) - scores_list.append(scores) - parents_list.append(torch.zeros(1, dtype=torch.long, device=scores.device)) - - # Handle vocabulary mapping if needed - if self.config.vocab_size == self.config.draft_vocab_size: - ss_token.append(topk_index) - input_ids = topk_index - else: - mapped_tokens = topk_index + self.d2t[topk_index] - ss_token.append(mapped_tokens) - input_ids = mapped_tokens - - # Prepare for tree traversal - input_hidden = last_hidden[None].repeat(1, self.top_k, 1) - tree_mask = self.tree_mask_init - topk_cs_index = torch.arange(self.top_k, device=self.embed_tokens.weight.device) - - # Traverse the tree depth levels - for i in range(self.depth): - ( - tree_mask, - input_hidden, - input_ids, - scores, - topk_cs_index, - past_key_values, - ) = self._process_tree_level( - i, - tree_mask, - input_hidden, - input_ids, - scores, - topk_cs_index, - scores_list, - parents_list, - ss_token, - past_key_values, - ) - # Process the final results - draft_tokens, retrieve_indices, tree_mask, tree_position_ids = ( - self._finalize_results( - scores_list, ss_token, sample_token, parents_list, logits_processor - ) - ) - - # Delete some used lists and variables to free memory - del scores_list, parents_list, ss_token - - return ( - draft_tokens, - retrieve_indices, - tree_mask, - tree_position_ids, - ) - - def forward( - self, - hidden_states, - input_ids, - inputs_embeds: Optional[torch.Tensor], - attention_mask: Optional[torch.Tensor] = None, - position_ids: Optional[torch.LongTensor] = None, - past_key_values: Optional[List[torch.FloatTensor]] = None, - use_cache: Optional[bool] = None, - output_attentions: Optional[bool] = None, - output_hidden_states: Optional[bool] = None, - ): - batch_size, seq_length, _ = hidden_states.shape - seq_length_with_past = seq_length - past_key_values_length = 0 - - if inputs_embeds is None: - with torch.no_grad(): - inputs_embeds = self.embed_tokens.weight[ - input_ids.squeeze(0).tolist() - ].unsqueeze(0) - - if past_key_values is not None: - past_key_values_length = past_key_values[0][0].shape[2] - seq_length_with_past = seq_length_with_past + past_key_values_length - if position_ids is None: - device = ( - hidden_states.device - if hidden_states is not None - else inputs_embeds.device - ) - position_ids = torch.arange( - past_key_values_length, - seq_length + past_key_values_length, - dtype=torch.long, - device=device, - ) - position_ids = position_ids.unsqueeze(0).view(-1, seq_length) - else: - position_ids = position_ids.view(-1, seq_length).long() - - if attention_mask is None: - attention_mask = torch.ones( - (batch_size, seq_length_with_past), - dtype=torch.bool, - device=hidden_states.device, - ) - attention_mask = self._prepare_decoder_attention_mask( - attention_mask, - (batch_size, seq_length), - hidden_states, - past_key_values_length, - ) - - dtype = self.fc.weight.dtype - inputs_embeds = inputs_embeds.to(dtype) - hidden_states = hidden_states.to(dtype) - if hidden_states.shape[-1] != inputs_embeds.shape[-1]: - hidden_states = self.fc(hidden_states) - - next_decoder_cache = () if use_cache else None - - past_key_value = past_key_values[0] if past_key_values is not None else None - layer_outputs = self.midlayer( - input_emb=inputs_embeds, - hidden_states=hidden_states, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_value=past_key_value, - output_attentions=output_attentions, - use_cache=True, - ) - if use_cache: - next_decoder_cache += (layer_outputs[2 if output_attentions else 1],) - hidden_states = layer_outputs[0] - - return hidden_states, next_decoder_cache - def _get_topk_tokens(self, hidden: Tensor) -> Tuple[Tensor, Tensor]: """Get top-k tokens from hidden states.""" logits = self.lm_head(self.norm(hidden)) probs = self.logsoftmax(logits) topk = torch.topk(probs, self.top_k, dim=-1) return topk.indices, topk.values - - def _process_tree_level( - self, - level: int, - tree_mask: Tensor, - input_hidden: Tensor, - input_ids: Tensor, - scores: Tensor, - topk_cs_index: Tensor, - scores_list: list, - parents_list: list, - ss_token: list, - past_key_values, - ) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor]: - """Process one level of the speculative decoding tree.""" - self.tree_mask = tree_mask - position_ids = self.position_ids + self.initial_position_id - - # Get next level hidden states - out_hidden, past_key_values = self( - input_hidden, - input_ids=input_ids, - inputs_embeds=None, - past_key_values=past_key_values, - position_ids=position_ids, - use_cache=True, - ) - self.initial_position_id += 1 - - # Calculate parent indices - bias1 = self.top_k if level > 0 else 0 - bias2 = max(0, level - 1) - bias = 1 + self.top_k**2 * bias2 + bias1 - parents = topk_cs_index + bias - parents_list.append(parents) - - # Get top-k tokens for this level - topk_index, topk_p = self._get_topk_tokens(out_hidden[0]) - if len(scores.shape) == 2: - scores = scores.squeeze(0) - cu_scores = topk_p + scores[:, None] - - # Select best candidates - topk_cs = torch.topk(cu_scores.view(-1), self.top_k, dim=-1) - topk_cs_index, scores = topk_cs.indices, topk_cs.values - - # Update data structures - out_ids = topk_cs_index // self.top_k - input_hidden = out_hidden[:, out_ids] - input_ids = topk_index.view(-1)[topk_cs_index][None] - - # Handle vocabulary mapping if needed - if self.config.vocab_size == self.config.draft_vocab_size: - ss_token.append(topk_index) - else: - input_ids = input_ids + self.d2t[input_ids] - ss_token.append(topk_index + self.d2t[topk_index]) - - scores_list.append(cu_scores) - tree_mask = torch.cat((tree_mask[:, :, out_ids], self.tree_mask_init), dim=3) - - return ( - tree_mask, - input_hidden, - input_ids, - scores, - topk_cs_index, - past_key_values, - ) diff --git a/angelslim/compressor/speculative/inference/models/eagle3/eagle3_model.py b/angelslim/compressor/speculative/inference/models/eagle3/eagle3_model.py index dfdd97fb..c9283722 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/eagle3_model.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/eagle3_model.py @@ -30,13 +30,10 @@ evaluate_posterior, initialize_past_key_values, initialize_tree, - initialize_tree_cosyvoice3, prepare_logits_processor, reset_tree_mode, tree_decoding, - tree_decoding_cosyvoice3, update_inference_inputs, - update_inference_inputs_cosyvoice3, ) from .configuration_eagle3_model import Eagle3Config from .draft import CosyVoice3Llama3Eagle3Drafter, Llama3Eagle3Drafter @@ -208,7 +205,40 @@ def prepare_generation( input_len=input_ids.shape[1], ) - def prepare_generation_cosyvoice3( + def should_stop( + self, + input_ids: torch.Tensor, + input_len: int, + new_token: int, + config: GenerationConfig, + stop_token_id: Optional[int], + ) -> bool: + """Check if generation should stop""" + if stop_token_id is not None: + if torch.any(input_ids[0, input_len:] == stop_token_id): + return True + + if torch.any(input_ids[0, input_len:] == self.tokenizer.eos_token_id): + return True + + if new_token > config.max_new_tokens: + return True + + if input_ids.shape[1] > config.max_length: + return True + + return False + + def get_padding_token(self, device: torch.device) -> torch.Tensor: + """Get or create padding token""" + if self._padding_token is None or self._padding_token.device != device: + self._padding_token = (torch.zeros(1, 1, dtype=torch.long) - 1).to(device) + return self._padding_token + + +class CosyVoice3GenerationManager(GenerationManager): + + def prepare_generation( self, model: "Eagle3Model", input_ids: torch.Tensor, config: GenerationConfig ) -> GenerationState: """Prepare all necessary components for generation""" @@ -257,30 +287,6 @@ def should_stop( new_token: int, config: GenerationConfig, stop_token_id: Optional[int], - ) -> bool: - """Check if generation should stop""" - if stop_token_id is not None: - if torch.any(input_ids[0, input_len:] == stop_token_id): - return True - - if torch.any(input_ids[0, input_len:] == self.tokenizer.eos_token_id): - return True - - if new_token > config.max_new_tokens: - return True - - if input_ids.shape[1] > config.max_length: - return True - - return False - - def should_stop_cosyvoice3( - self, - input_ids: torch.Tensor, - input_len: int, - new_token: int, - config: GenerationConfig, - stop_token_id: Optional[int], ) -> bool: """Check if generation should stop""" if stop_token_id is not None: @@ -296,12 +302,6 @@ def should_stop_cosyvoice3( return False - def get_padding_token(self, device: torch.device) -> torch.Tensor: - """Get or create padding token""" - if self._padding_token is None or self._padding_token.device != device: - self._padding_token = (torch.zeros(1, 1, dtype=torch.long) - 1).to(device) - return self._padding_token - class Eagle3Model(nn.Module): """ @@ -431,6 +431,7 @@ def forward( def eagle_generate( self, input_ids: torch.Tensor, + inputs_embeds: Optional[torch.Tensor] = None, temperature: float = 0.0, top_p: float = 0.0, top_k: float = 0.0, @@ -438,6 +439,7 @@ def eagle_generate( max_length: int = 2048, log: bool = False, is_llama3: bool = False, + is_cosyvoice3: bool = False, early_stop_smooth_type: str = "ewma", ) -> Union[torch.Tensor, Tuple[torch.Tensor, int, int, List[int]]]: """Generate text using EAGLE speculative decoding""" @@ -457,7 +459,11 @@ def eagle_generate( # Prefill phase draft_tokens, retrieve_indices, tree_mask, tree_position_ids, logits, _, _ = ( initialize_tree( - input_ids, self, state.past_key_values, state.logits_processor + input_ids, + inputs_embeds, + self, + state.past_key_values, + state.logits_processor, ) ) @@ -485,7 +491,10 @@ def eagle_generate( draft_tokens = draft_tokens.to(input_ids.device) tree_position_ids = tree_position_ids.to(input_ids.device) - self.base_model.model.tree_mask = tree_mask + if is_cosyvoice3: + self.base_model.model.llm.llm.model.model.tree_mask = tree_mask + else: + self.base_model.model.tree_mask = tree_mask # Target model forward pass logits, hidden_state_new, _ = tree_decoding( @@ -545,6 +554,7 @@ def eagle_generate( # Update inference inputs ( state.input_ids, + inputs_embeds, draft_tokens, retrieve_indices, tree_mask, @@ -553,6 +563,7 @@ def eagle_generate( early_stop_signal, ) = update_inference_inputs( input_ids=state.input_ids, + inputs_embeds=inputs_embeds, candidates=candidates, best_candidate=best_candidate, accept_length=accept_length, @@ -642,7 +653,7 @@ def naive_generate( return (state.input_ids, state.new_token, step) if log else state.input_ids -class CosyVoice3Eagle3Model(nn.Module): +class CosyVoice3Eagle3Model(Eagle3Model): """ CosyVoice3 EAGLE3 Model for speculative decoding """ @@ -650,12 +661,12 @@ class CosyVoice3Eagle3Model(nn.Module): def __init__( self, base_model: nn.Module, + tokenizer: AutoTokenizer, eagle_layer: nn.Module, + early_stop_method: Optional[str] = None, ): - super().__init__() - self.base_model = base_model - self.eagle_layer = eagle_layer - self.generation_manager = GenerationManager(None) + super().__init__(base_model, tokenizer, eagle_layer, early_stop_method) + self.generation_manager = CosyVoice3GenerationManager(tokenizer) @classmethod def from_pretrained( @@ -667,11 +678,33 @@ def from_pretrained( top_k: int = 10, threshold: float = 1.0, enable_benchmark: bool = False, + early_stop_method: Optional[str] = None, + stop_think_token: str = "", + step_split_tokens: Optional[List[str]] = None, **kwargs, ) -> "CosyVoice3Eagle3Model": """Create CosyVoice3Eagle3Model from pretrained components""" # Load base model and tokenizer + if not step_split_tokens: + step_split_tokens = [ + "\n\n", + "\n\n\n", + ".\n\n", + ".\n\n\n", + " \n\n", + " \n\n\n", + ] base_model = ModelLoader.load_base_model(base_model_path, **kwargs) + tokenizer = base_model.frontend.tokenizer + tokenizer.stop_think_id = tokenizer.encode( + stop_think_token, add_special_tokens=False + )[0] + tokenizer.step_split_ids = [] + for s in step_split_tokens: + t = tokenizer.encode(s, add_special_tokens=False) + if len(t) > 1: + continue + tokenizer.step_split_ids.append(t[0]) # Load configuration config_path = ModelLoader.ensure_config_path(eagle_model_path) config = Eagle3Config.from_pretrained(config_path) @@ -689,6 +722,7 @@ def from_pretrained( threshold=threshold, path=base_model_path, load_emb=True, + early_stop_method=early_stop_method, ) # Clean up unused components @@ -707,201 +741,32 @@ def from_pretrained( ) eagle_layer.total_tokens = total_token - 1 - return cls(base_model, eagle_layer) + return cls(base_model, tokenizer, eagle_layer, early_stop_method) def forward( - self, - text: torch.Tensor, - prompt_text: torch.Tensor, - llm_prompt_speech_token: torch.Tensor, - past_key_values: Optional[Any] = None, - ) -> Union[Tuple[Any, torch.Tensor], Tuple[Any, torch.Tensor, torch.Tensor]]: - """Forward pass through the model""" - device = self.base_model.model.device - first_token, hidden_states, inputs_embeddings = self.base_model.model.llm( - text=text.to(device), - text_len=torch.tensor([text.shape[1]], dtype=torch.int32).to(device), - prompt_text=prompt_text.to(device), - prompt_text_len=torch.tensor([prompt_text.shape[1]], dtype=torch.int32).to( - device - ), - prompt_speech_token=llm_prompt_speech_token.to(device), - prompt_speech_token_len=torch.tensor( - [llm_prompt_speech_token.shape[1]], dtype=torch.int32 - ).to(device), - past_key_values=past_key_values, - ) - first_token = torch.tensor(first_token).unsqueeze(dim=0) - - return first_token, hidden_states, inputs_embeddings - - def tree_decoding_forward( self, input_ids: Optional[torch.Tensor] = None, + inputs_embeds: Optional[torch.Tensor] = None, attention_mask: Optional[torch.Tensor] = None, past_key_values: Optional[Any] = None, + output_orig: bool = False, position_ids: Optional[torch.Tensor] = None, ) -> Union[Tuple[Any, torch.Tensor], Tuple[Any, torch.Tensor, torch.Tensor]]: """Forward pass through the model""" - logits, hidden_states = self.base_model.model.llm.inference_one_step( + outputs, orig = self.base_model.model.llm( input_ids=input_ids, + inputs_embeds=inputs_embeds, attention_mask=attention_mask, - past_key_values=past_key_values, position_ids=position_ids, + past_key_values=past_key_values, ) - - return logits, hidden_states - - @torch.no_grad() - def eagle_generate( - self, - text: Optional[torch.Tensor] = None, - prompt_text: Optional[torch.Tensor] = None, - llm_prompt_speech_token: Optional[torch.Tensor] = None, - temperature: float = 0.0, - top_p: float = 0.0, - top_k: float = 0.0, - max_new_tokens: int = 512, - max_length: int = 2048, - log: bool = False, - is_llama3: bool = False, - **kwargs, - ) -> Union[torch.Tensor, Tuple[torch.Tensor, int, int, List[int]]]: - """Generate text using EAGLE speculative decoding""" - config = GenerationConfig( - temperature=temperature, - top_p=top_p, - top_k=top_k, - max_new_tokens=max_new_tokens, - max_length=max_length, - log=log, - is_llama3=is_llama3, - ) - - input_ids = torch.concat( - [ - self.base_model.model.llm.sos_id.unsqueeze(dim=0) - .to(text.dtype) - .to(text.device), - prompt_text, - text, - self.base_model.model.llm.task_token.unsqueeze(dim=0) - .to(text.dtype) - .to(text.device), - llm_prompt_speech_token, - ], - dim=1, - ) - state = self.generation_manager.prepare_generation_cosyvoice3( - self, input_ids, config - ) - padding = self.generation_manager.get_padding_token(text.device) - - # Prefill phase - ( - draft_tokens, - retrieve_indices, - tree_mask, - tree_position_ids, - logits, - inputs_embeddings, - first_token, - ) = initialize_tree_cosyvoice3( - text, - prompt_text, - llm_prompt_speech_token, - input_ids, - self, - state.past_key_values, - state.logits_processor, - ) - - accept_length_list = [] - max_decode_steps = config.max_length - self.eagle_layer.total_tokens - 10 - - out_tokens = [] - out_tokens.append(first_token.item()) - - for step in range(max_decode_steps): # noqa: B007 - # Ensure tensors are on correct device - draft_tokens = draft_tokens.to(input_ids.device) - tree_position_ids = tree_position_ids.to(input_ids.device) - - self.base_model.model.llm.llm.model.model.tree_mask = tree_mask - - # Target model forward pass - logits, hidden_state_new = tree_decoding_cosyvoice3( - self, - draft_tokens, - state.past_key_values, - tree_position_ids, - state.input_ids, - retrieve_indices, - ) - - draft_tokens = torch.cat((draft_tokens, padding), dim=1) - candidates = draft_tokens[0, retrieve_indices] - - # Verification phase - best_candidate, accept_length, sample_token = evaluate_posterior( - logits, candidates, state.logits_processor - ) - - accept_length_list.append( - accept_length.item() - if torch.is_tensor(accept_length) - else accept_length - ) - - out_tokens.extend(candidates[best_candidate, : accept_length + 1].tolist()) - out_tokens.append(sample_token.item()) - - # Update inference inputs - ( - state.input_ids, - inputs_embeddings, - draft_tokens, - retrieve_indices, - tree_mask, - tree_position_ids, - state.new_token, - ) = update_inference_inputs_cosyvoice3( - input_ids=state.input_ids, - inputs_embeddings=inputs_embeddings, - candidates=candidates, - best_candidate=best_candidate, - accept_length=accept_length, - retrieve_indices=retrieve_indices, - logits_processor=state.logits_processor, - new_token=state.new_token, - past_key_values_data_list=self.past_key_values_data, - current_length_data=self.current_length_data, - model=self, - hidden_state_new=hidden_state_new, - sample_token=sample_token, - ) - - if self.generation_manager.should_stop_cosyvoice3( - state.input_ids, - state.input_len, - state.new_token, - config, - state.stop_token_id, - ): - break - - return ( - (state.input_ids, state.new_token, step, accept_length_list) - if log - else state.input_ids - ) + return outputs, orig, None @torch.no_grad() def naive_generate( self, - text: Optional[torch.Tensor] = None, - prompt_text: Optional[torch.Tensor] = None, - llm_prompt_speech_token: Optional[torch.Tensor] = None, + input_ids: torch.Tensor, + inputs_embeds: Optional[torch.Tensor] = None, temperature: float = 0.0, top_p: float = 0.0, top_k: float = 0.0, @@ -909,9 +774,8 @@ def naive_generate( max_length: int = 2048, log: bool = False, is_llama3: bool = False, - max_token_text_ratio: float = 20.0, - min_token_text_ratio: float = 2.0, - **kwargs, + min_len: Optional[int] = None, + max_decode_steps: Optional[int] = None, ) -> Union[torch.Tensor, Tuple[torch.Tensor, int, int]]: """Generate text using naive (non-speculative) decoding""" config = GenerationConfig( @@ -924,47 +788,19 @@ def naive_generate( is_llama3=is_llama3, ) - input_ids = torch.concat( - [ - self.base_model.model.llm.sos_id.unsqueeze(dim=0) - .to(text.dtype) - .to(text.device), - prompt_text, - text, - self.base_model.model.llm.task_token.unsqueeze(dim=0) - .to(text.dtype) - .to(text.device), - llm_prompt_speech_token, - ], - dim=1, - ) - state = self.generation_manager.prepare_generation_cosyvoice3( - self, input_ids, config - ) + state = self.generation_manager.prepare_generation(self, input_ids, config) - device = self.base_model.model.device - logits = self.base_model.model.llm( - text=text.to(device), - text_len=torch.tensor([text.shape[1]], dtype=torch.int32).to(device), - prompt_text=prompt_text.to(device), - prompt_text_len=torch.tensor([prompt_text.shape[1]], dtype=torch.int32).to( - device - ), - prompt_speech_token=llm_prompt_speech_token.to(device), - prompt_speech_token_len=torch.tensor( - [llm_prompt_speech_token.shape[1]], dtype=torch.int32 - ).to(device), + _, logits = self.base_model.model.llm( + input_ids=input_ids, + inputs_embeds=inputs_embeds, past_key_values=state.past_key_values, - return_first_token=False, ) out_tokens = [] - min_len = int(text.shape[1] * min_token_text_ratio) - max_decode_steps = int(text.shape[1] * max_token_text_ratio) for step in range(max_decode_steps): # noqa: B007 input_id = self.base_model.model.llm.sampling_ids( - logits.squeeze(dim=0), + logits[:, -1].squeeze(dim=0), out_tokens, ignore_eos=True if step < min_len else False, ) @@ -975,14 +811,13 @@ def naive_generate( .unsqueeze(0) ) - logits, _ = self.base_model.model.llm.inference_one_step( + _, logits = self.base_model.model.llm( input_id, past_key_values=state.past_key_values ) - logits = logits.squeeze(0) state.input_ids = torch.cat([state.input_ids, input_id], dim=-1) state.new_token += 1 - if self.generation_manager.should_stop_cosyvoice3( + if self.generation_manager.should_stop( state.input_ids, state.input_len, state.new_token, diff --git a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_cosyvoice3_kv.py b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_cosyvoice3_kv.py index bb00e12e..d0398f0c 100644 --- a/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_cosyvoice3_kv.py +++ b/angelslim/compressor/speculative/inference/models/eagle3/target/modeling_cosyvoice3_kv.py @@ -709,79 +709,32 @@ def sampling_ids( return top_ids @torch.inference_mode() - def inference_one_step( + def forward( self, - input_ids: torch.Tensor, + input_ids: Optional[torch.LongTensor] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, attention_mask: Optional[torch.Tensor] = None, - past_key_values: Optional[Any] = None, position_ids: Optional[torch.Tensor] = None, - pre_tokens: Optional[List] = None, + past_key_values=None, ) -> List[int]: - inputs_embeds = self.speech_embedding.weight[ - input_ids.squeeze(0).tolist() - ].unsqueeze(0) - - out_tokens = [] - if pre_tokens is not None: - out_tokens.extend(pre_tokens) + if inputs_embeds is None: + inputs_embeds = self.speech_embedding.weight[ + input_ids.squeeze(0).tolist() + ].unsqueeze(0) + # prefill y_pred, hidden_states = self.llm.forward_one_step( inputs_embeds, + masks=attention_mask, past_key_values=past_key_values, position_ids=position_ids, output_hidden_states=True, return_hidden_states=True, ) logp = self.llm_decoder(y_pred).log_softmax(dim=-1) - return logp, hidden_states - - @torch.inference_mode() - def forward( - self, - text: torch.Tensor, - text_len: torch.Tensor, - prompt_text: torch.Tensor, - prompt_text_len: torch.Tensor, - prompt_speech_token: torch.Tensor, - prompt_speech_token_len: torch.Tensor, - sampling: int = 25, - past_key_values=None, - return_first_token=True, - ) -> List[int]: - device = text.device - text = torch.concat([prompt_text, text], dim=1) - text_len += prompt_text_len - text = self.llm.model.model.embed_tokens(text) - - # concat llm_input - sos_emb = self.speech_embedding.weight[self.sos].reshape(1, 1, -1) - task_id_emb = self.speech_embedding.weight[self.task_id].reshape(1, 1, -1) - if prompt_speech_token_len != 0: - prompt_speech_token_emb = self.speech_embedding(prompt_speech_token) - else: - prompt_speech_token_emb = torch.zeros( - 1, 0, self.llm_input_size, dtype=text.dtype - ).to(device) - lm_input = torch.concat( - [sos_emb, text, task_id_emb, prompt_speech_token_emb], dim=1 - ) - - # prefill - out_tokens = [] - y_pred, hidden_states = self.llm.forward_one_step( - lm_input, - past_key_values=past_key_values, - output_hidden_states=True, - return_hidden_states=True, - ) - logp = self.llm_decoder(y_pred[:, -1]).log_softmax(dim=-1) - if return_first_token: - probs = torch.nn.functional.softmax(logp.squeeze(dim=0), dim=-1) - input_id = torch.multinomial(probs, 1) - out_tokens.append(input_id.item()) - return out_tokens, hidden_states, lm_input + outputs = {"hidden_states": hidden_states} - return logp + return outputs, logp class CosyVoice3Model: diff --git a/angelslim/compressor/speculative/utils/__init__.py b/angelslim/compressor/speculative/utils/__init__.py index 48bb02cd..57952ee6 100644 --- a/angelslim/compressor/speculative/utils/__init__.py +++ b/angelslim/compressor/speculative/utils/__init__.py @@ -5,26 +5,20 @@ MomentumScorePredictor, evaluate_posterior, initialize_tree, - initialize_tree_cosyvoice3, padding, prepare_logits_processor, reset_tree_mode, tree_decoding, - tree_decoding_cosyvoice3, update_inference_inputs, - update_inference_inputs_cosyvoice3, ) __all__ = [ "prepare_logits_processor", "reset_tree_mode", "initialize_tree", - "initialize_tree_cosyvoice3", "tree_decoding", - "tree_decoding_cosyvoice3", "evaluate_posterior", "update_inference_inputs", - "update_inference_inputs_cosyvoice3", "initialize_past_key_values", "MomentumScorePredictor", "EWMAScorePredictor", diff --git a/angelslim/compressor/speculative/utils/util.py b/angelslim/compressor/speculative/utils/util.py index 406b8a88..aac4e1e2 100644 --- a/angelslim/compressor/speculative/utils/util.py +++ b/angelslim/compressor/speculative/utils/util.py @@ -90,9 +90,9 @@ def prepare_logits_processor( return processor_list -def initialize_tree(input_ids, model, past_key_values, logits_processor): +def initialize_tree(input_ids, inputs_embeds, model, past_key_values, logits_processor): outputs, orig, hidden_states = model( - input_ids, past_key_values=past_key_values, output_orig=True + input_ids, inputs_embeds, past_key_values=past_key_values, output_orig=True ) if logits_processor is not None: @@ -104,6 +104,11 @@ def initialize_tree(input_ids, model, past_key_values, logits_processor): token = torch.argmax(orig[:, -1]) token = token[None, None] input_ids = torch.cat((input_ids, token.to(input_ids.device)), dim=1) + # add embedding + if inputs_embeds is not None: + add_inputs_embeds = torch.cat( + [inputs_embeds, model.eagle_layer.embed_tokens(token)], dim=1 + ) # Clone the output hidden states eagle_device = next(model.eagle_layer.parameters()).device @@ -113,47 +118,8 @@ def initialize_tree(input_ids, model, past_key_values, logits_processor): ] hidden_states = torch.cat(outputs["hidden_states"], dim=-1) draft_tokens, retrieve_indices, tree_mask, tree_position_ids, _ = ( - model.eagle_layer.topK_genrate(hidden_states, input_ids, logits_processor) - ) - return ( - draft_tokens, - retrieve_indices, - tree_mask, - tree_position_ids, - orig, - hidden_states, - token, - ) - - -def initialize_tree_cosyvoice3( - text, - prompt_text, - llm_prompt_speech_token, - input_ids, - model, - past_key_values, - logits_processor, -): - first_token, hidden_states, inputs_embeddings = model( - text, prompt_text, llm_prompt_speech_token, past_key_values - ) - - input_ids = torch.cat((input_ids, first_token.to(input_ids.device)), dim=1) - # add embedding - add_inputs_embeds = model.eagle_layer.embed_tokens.weight[ - first_token.squeeze(0).tolist() - ].unsqueeze(0) - new_inputs_embeddings = torch.cat([inputs_embeddings, add_inputs_embeds], dim=1) - - # Clone the output hidden states - eagle_device = next(model.eagle_layer.parameters()).device - if hidden_states[0].device != eagle_device: - hidden_states = [x.to(eagle_device) for x in hidden_states] - hidden_states = torch.cat(hidden_states, dim=-1) - draft_tokens, retrieve_indices, tree_mask, tree_position_ids = ( model.eagle_layer.topK_genrate( - hidden_states, input_ids, new_inputs_embeddings, logits_processor + hidden_states, input_ids, add_inputs_embeds, logits_processor ) ) return ( @@ -161,9 +127,9 @@ def initialize_tree_cosyvoice3( retrieve_indices, tree_mask, tree_position_ids, + orig, hidden_states, - inputs_embeddings, - first_token, + token, ) @@ -185,9 +151,8 @@ def tree_decoding( position_ids = tree_position_ids + input_ids.shape[1] if position_ids is not None and position_ids.dim() == 1: position_ids = position_ids.unsqueeze(0) - # import pdb; pdb.set_trace() outputs, tree_logits, hidden_state = model( - tree_candidates, + input_ids=tree_candidates, output_orig=True, past_key_values=past_key_values, position_ids=position_ids, @@ -204,29 +169,6 @@ def tree_decoding( return logits, hidden_state, outputs -def tree_decoding_cosyvoice3( - model, - tree_candidates, - past_key_values, - tree_position_ids, - input_ids, - retrieve_indices, -): - position_ids = tree_position_ids + input_ids.shape[1] - if position_ids is not None and position_ids.dim() == 1: - position_ids = position_ids.unsqueeze(0) - tree_logits, hidden_state = model.tree_decoding_forward( - input_ids=tree_candidates, - past_key_values=past_key_values, - position_ids=position_ids, - ) - - hidden_state = torch.cat(hidden_state, dim=-1) - - logits = tree_logits[0, retrieve_indices] - return logits, hidden_state - - def evaluate_posterior( logits: torch.Tensor, candidates: torch.Tensor, @@ -322,6 +264,7 @@ def evaluate_posterior( @torch.no_grad() def update_inference_inputs( input_ids, + inputs_embeds, candidates, best_candidate, accept_length, @@ -334,79 +277,8 @@ def update_inference_inputs( hidden_state_new, sample_token, ): - prev_input_len = input_ids.shape[1] - # Map the best candidate indices to the original indices in the sequence - select_indices = ( - retrieve_indices[best_candidate, : accept_length + 1] + prev_input_len - ) - # Append the tokens from the best candidate to the input sequence - input_ids = torch.cat( - [ - input_ids, - candidates[None, best_candidate, : accept_length + 1].to(input_ids.device), - ], - dim=-1, - ) - # Update the past key values based on the selected tokens - # Source tensor that contains relevant past information based - # on the selected candidate - for past_key_values_data in past_key_values_data_list: - tgt = past_key_values_data[ - ..., select_indices.to(past_key_values_data.device), : - ] - # Destination tensor where the relevant past information will be stored - dst = past_key_values_data[ - ..., prev_input_len : prev_input_len + tgt.shape[-2], : - ] - # Copy relevant past information from the source to the destination - dst.copy_(tgt, non_blocking=True) - - # Update the current length tensor (currently only support batch size is 1) - current_length_data.fill_(prev_input_len + tgt.shape[-2]) - - retrieve_hidden_state_new = hidden_state_new[:, retrieve_indices] - accept_hidden_state_new = retrieve_hidden_state_new[ - :, best_candidate, : accept_length + 1 - ] - - draft_tokens, retrieve_indices, tree_mask, tree_position_ids, early_stop_signal = ( - model.eagle_layer.topK_genrate( - accept_hidden_state_new, - input_ids=torch.cat((input_ids, sample_token.to(input_ids.device)), dim=1), - logits_processor=logits_processor, - ) - ) - - new_token += accept_length + 1 - - return ( - input_ids, - draft_tokens, - retrieve_indices, - tree_mask, - tree_position_ids, - new_token, - early_stop_signal, - ) - - -@torch.no_grad() -def update_inference_inputs_cosyvoice3( - input_ids, - inputs_embeddings, - candidates, - best_candidate, - accept_length, - retrieve_indices, - logits_processor, - new_token, - past_key_values_data_list, - current_length_data, - model, - hidden_state_new, - sample_token, -): - assert input_ids.shape[1] == inputs_embeddings.shape[1] + if inputs_embeds is not None: + assert input_ids.shape[1] == inputs_embeds.shape[1] prev_input_len = input_ids.shape[1] # Map the best candidate indices to the original indices in the sequence select_indices = ( @@ -422,11 +294,11 @@ def update_inference_inputs_cosyvoice3( ) # add embedding - add_inputs_embeds = model.eagle_layer.embed_tokens.weight[ - candidates[None, best_candidate, : accept_length + 1].squeeze(0).tolist() - ].unsqueeze(0) - inputs_embeddings = torch.cat([inputs_embeddings, add_inputs_embeds], dim=1) - + if inputs_embeds is not None: + add_inputs_embeds = model.eagle_layer.embed_tokens.weight[ + candidates[None, best_candidate, : accept_length + 1].squeeze(0).tolist() + ].unsqueeze(0) + inputs_embeds = torch.cat([inputs_embeds, add_inputs_embeds], dim=1) # Update the past key values based on the selected tokens # Source tensor that contains relevant past information based # on the selected candidate @@ -450,15 +322,16 @@ def update_inference_inputs_cosyvoice3( ] # add embedding - add_inputs_embeds = model.eagle_layer.embed_tokens.weight[ - sample_token.squeeze(0).tolist() - ].unsqueeze(0) + if inputs_embeds is not None: + add_inputs_embeds = model.eagle_layer.embed_tokens.weight[ + sample_token.squeeze(0).tolist() + ].unsqueeze(0) - draft_tokens, retrieve_indices, tree_mask, tree_position_ids = ( + draft_tokens, retrieve_indices, tree_mask, tree_position_ids, early_stop_signal = ( model.eagle_layer.topK_genrate( accept_hidden_state_new, input_ids=torch.cat((input_ids, sample_token.to(input_ids.device)), dim=1), - inputs_embeddings=torch.cat([inputs_embeddings, add_inputs_embeds], dim=1), + inputs_embeds=torch.cat([inputs_embeds, add_inputs_embeds], dim=1), logits_processor=logits_processor, ) ) @@ -467,12 +340,13 @@ def update_inference_inputs_cosyvoice3( return ( input_ids, - inputs_embeddings, + inputs_embeds, draft_tokens, retrieve_indices, tree_mask, tree_position_ids, new_token, + early_stop_signal, ) diff --git a/angelslim/engine.py b/angelslim/engine.py index d0582037..7ddcddf9 100644 --- a/angelslim/engine.py +++ b/angelslim/engine.py @@ -409,10 +409,6 @@ def __init__(self, config=None, deploy_backend: str = "pytorch"): self.BenchmarkConfig = pytorch_benchmark.BenchmarkConfig self.BenchmarkEngine = pytorch_benchmark.BenchmarkEngine self.BenchmarkMode = pytorch_benchmark.BenchmarkMode - elif self.deploy_backend == "pytorch_tts": - self.BenchmarkConfig = pytorch_benchmark.BenchmarkConfig - self.BenchmarkEngine = pytorch_benchmark.TTSBenchmarkEngine - self.BenchmarkMode = pytorch_benchmark.BenchmarkMode elif self.deploy_backend == "vllm": self.BenchmarkConfig = vllm_benchmark.BenchmarkConfig self.BenchmarkEngine = vllm_benchmark.BenchmarkEngine @@ -453,6 +449,8 @@ def setup_benchmark( config_dict.update(kwargs) self.config = self.BenchmarkConfig(**config_dict) + if self.config.is_tts: + self.BenchmarkEngine = pytorch_benchmark.TTSBenchmarkEngine self.benchmark_engine = self.BenchmarkEngine(self.config) return self.config diff --git a/docs/source/features/speculative_decoding/audio_eagle.md b/docs/source/features/speculative_decoding/audio_eagle.md deleted file mode 100644 index 64d5666c..00000000 --- a/docs/source/features/speculative_decoding/audio_eagle.md +++ /dev/null @@ -1,140 +0,0 @@ -# 语音模型EAGLE3训练 -## TTS - -### 1. 支持模型列表 -- `Fun-CosyVoice3` - -### 2. 数据组织形式 - -所有数据需保存在`jsonl`文件中,数据格式可参考: -- 原始训练数据示例:`AngelSlim/dataset/tts_fake_data/train.jsonl` -- 重采样训练数据示例:`AngelSlim/dataset/tts_fake_data/train_regenerate.jsonl` - -#### 2.1 原始训练数据 - -每行字段意义如下: -- `text`:输入文本 -- `audio_path`:真实音频绝对路径 -- `instruct`:发音人文本表示 -- `instruct_audio_path`:发音人音频绝对路径 - -#### 2.2 重采样训练数据(推荐) - -为得到高质量的目标模型SFT数据,建议使用目标模型重新采样训练数据,将LLM生成的语音token保存在`jsonl`文件中,每行字段意义如下: -- `text`:输入文本 -- `audio_tokens`:生成的语音token -- `instruct`:发音人文本表示 -- `instruct_audio_path`:发音人音频绝对路径 - - -### 3. 训练Eagle3模型 - -目前仅支持在线训练模式。 - -#### 3.1 在线训练 - -使用下面的脚本进行Eagle3模型的在线训练: - -```shell -bash scripts/speculative/train_eagle3_tts_online.sh -``` - -**脚本参数说明:** - -在使用前,需要在脚本中配置以下参数: - -- `TARGET_MODEL_NAME_OR_PATH`: 目标模型的HF名称或本地名称 -- `DRAFT_MODEL_CONFIG_PATH`: 草稿模型的config路径 -- `TRAIN_DATA_PATH`: 训练数据路径 -- `OUTPUT_DIR`: Eagle3模型输出路径 -- `MODEL_MAX_LENGTH`: 训练数据的最大长度 - - -### 4. HF基准测试 - -`Fun-CosyVoice3`仅支持HF测试平均接收长度。 - -#### 4.1.1 基本用法 - -使用 `tools/spec_benchmark.py` 脚本进行投机采样基准测试: - -```shell -python3 tools/spec_benchmark.py \ - --base-model-path ${BASE_MODEL_PATH} \ - --eagle-model-path ${EAGLE_MODEL_PATH} \ - --model-id ${MODEL_ID} \ - --mode both -``` - -#### 4.1.2 参数说明 - -**模型配置参数:** -- `--base-model-path`: 基础模型路径(必需) -- `--eagle-model-path`: Eagle辅助模型路径(必需) -- `--model-id`: 模型标识符(必需) - -**基准测试配置:** -- `--bench-name`: 基准数据集名称,可参考【`tts_fake_data`】 -- `--mode`: 执行模式,可选 `eagle`(仅投机采样)、`baseline`(仅基线)、`both`(两者都执行),默认为 `both` -- `--output-dir`: 结果输出目录 - -**生成参数:** -- `--temperature`: 采样温度,默认为 1.0 -- `--max-new-token`: 最大生成token数,默认为 1024 -- `--total-token`: 草稿树中的总节点数,默认为 60 -- `--depth`: 树深度,默认为 5 -- `--top-k`: Top-k采样,默认为 10 -- `--generate-audio`: 是否生成最终音频 - -**硬件配置:** -- `--num-gpus-per-model`: 每个模型使用的GPU数量,默认为 1 -- `--num-gpus-total`: 总GPU数量,默认为 1 -- `--max-gpu-memory`: 每个GPU的最大内存限制 - -**其他设置:** -- `--seed`: 随机种子,默认为 42 -- `--question-begin`: 问题起始索引(用于调试) -- `--question-end`: 问题结束索引(用于调试) -- `--no-metrics`: 跳过自动指标计算 - -**注意事项:** -- `--bench-name`: 也可以添加自定义测试集,在`AngelSlim/dataset`目录下创建新的子目录并将目录名作为`--bench-name`,在新目录下创建`question.jsonl`,框架会自动读取该文件 -- `--temperature`: `Fun-CosyVoice3`在`temperature`为0时容易生成大量重复token,建议测试时使用默认配置 - -#### 4.1.3 使用示例 - -**完整基准测试:** -```shell -python3 tools/spec_benchmark.py \ - --base-model-path /path/to/base/model \ - --eagle-model-path /path/to/eagle/model \ - --model-id cosyvoice3 \ - --mode both \ - --output-dir ./results \ - --deploy-backend pytorch_tts \ - --generate-audio -``` - -**注意事项:** -- `Fun-CosyVoice3`在设置`generate-audio`为`True`时需要额外导入`cosyvoice`包,安装步骤如下: - ```shell - git clone https://github.com/FunAudioLLM/CosyVoice - pip install hyperpyyaml omegaconf conformer diffusers hydra-core lightning gdown matplotlib wget x-transformers pyworld librosa - ``` - - 测试脚本参考: - ```shell - export PYTHONPATH=/xxx/CosyVoice:/xxx/CosyVoice/third_party/Matcha-TTS:$PYTHONPATH - python3 tools/spec_benchmark.py [ARGS] - ``` - -**不生成音频:** -```shell -python3 tools/spec_benchmark.py \ - --base-model-path /path/to/base/model \ - --eagle-model-path /path/to/eagle/model \ - --model-id cosyvoice3 \ - --mode both \ - --output-dir ./results \ - --deploy-backend pytorch_tts -``` diff --git a/tools/spec_benchmark.py b/tools/spec_benchmark.py index dcb00c9d..af9f1b83 100644 --- a/tools/spec_benchmark.py +++ b/tools/spec_benchmark.py @@ -50,7 +50,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--deploy-backend", type=str, - choices=["pytorch", "pytorch_tts", "vllm"], + choices=["pytorch", "vllm"], default="pytorch", help="Backend for deployment (pytorch or vllm)", ) @@ -121,6 +121,9 @@ def parse_args() -> argparse.Namespace: default=1, help="Tensor parallel size for draft model (vllm only)", ) + parser.add_argument( + "--is-tts", action="store_true", help="whether or not TTS model" + ) parser.add_argument( "--generate-audio", action="store_true", help="whether or not generate audio" ) @@ -158,6 +161,7 @@ def main(): "top_p": args.top_p, "top_k": args.top_k, "depth": args.depth, + "is_tts": args.is_tts, "generate_audio": args.generate_audio, }