From b40e645aa3d874ec11f71e837952a2be74828ac3 Mon Sep 17 00:00:00 2001 From: Ringssss <28751159+Ringssss@users.noreply.github.com> Date: Fri, 30 Oct 2026 02:06:35 +0800 Subject: [PATCH] feat(clover): add native Hygon block diffusion and optional frontier decoding --- README.md | 5 + csrc/engine/infer_engine.cpp | 2 + csrc/engine/rank_worker.hpp | 2 + csrc/global_state/forward_context.hpp | 6 + .../layers/attention/backends/static_attn.cpp | 3 +- .../clover_moe/clover_decoder_layer.cpp | 83 ++++++++ .../clover_moe/clover_decoder_layer.hpp | 43 ++++ .../clover_moe/clover_for_causal_lm.cpp | 105 +++++++++ csrc/models/clover_moe/clover_gdn.cpp | 104 +++++++++ csrc/models/clover_moe/clover_gdn.hpp | 22 ++ csrc/models/clover_moe/clover_moe.cpp | 104 +++++++++ csrc/models/clover_moe/clover_moe.hpp | 26 +++ csrc/models/infinilm_model.hpp | 2 + .../qwen3_next/qwen3_next_gated_deltanet.hpp | 2 +- csrc/pybind11/bindings.cc | 4 + csrc/pybind11/engine/engine.hpp | 10 +- docs/clover-hygon-ops.cmake | 4 + docs/clover-hygon.md | 169 +++++++++++++++ docs/images/clover-hygon-validation.png | Bin 0 -> 80243 bytes examples/bench_clover.py | 103 +++++++++ pyproject.toml | 1 + python/infinilm/diffusion/__init__.py | 1 + python/infinilm/diffusion/clover.py | 181 ++++++++++++++++ python/infinilm/diffusion/frontier.py | 43 ++++ python/infinilm/infer_engine.py | 12 ++ python/infinilm/licenses/dLLM-serve/LICENSE | 201 ++++++++++++++++++ python/infinilm/licenses/dLLM-serve/NOTICE | 4 + python/infinilm/modeling_utils.py | 8 + python/infinilm/server/clover_server.py | 146 +++++++++++++ test/diffusion/test_clover_loading.py | 38 ++++ test/diffusion/test_frontier.py | 35 +++ test/diffusion/workload.json | 194 +++++++++++++++++ third_party/InfiniCore | 2 +- 33 files changed, 1661 insertions(+), 4 deletions(-) create mode 100644 csrc/models/clover_moe/clover_decoder_layer.cpp create mode 100644 csrc/models/clover_moe/clover_decoder_layer.hpp create mode 100644 csrc/models/clover_moe/clover_for_causal_lm.cpp create mode 100644 csrc/models/clover_moe/clover_gdn.cpp create mode 100644 csrc/models/clover_moe/clover_gdn.hpp create mode 100644 csrc/models/clover_moe/clover_moe.cpp create mode 100644 csrc/models/clover_moe/clover_moe.hpp create mode 100644 docs/clover-hygon-ops.cmake create mode 100644 docs/clover-hygon.md create mode 100644 docs/images/clover-hygon-validation.png create mode 100644 examples/bench_clover.py create mode 100644 python/infinilm/diffusion/__init__.py create mode 100644 python/infinilm/diffusion/clover.py create mode 100644 python/infinilm/diffusion/frontier.py create mode 100644 python/infinilm/licenses/dLLM-serve/LICENSE create mode 100644 python/infinilm/licenses/dLLM-serve/NOTICE create mode 100644 python/infinilm/server/clover_server.py create mode 100644 test/diffusion/test_clover_loading.py create mode 100644 test/diffusion/test_frontier.py create mode 100644 test/diffusion/workload.json diff --git a/README.md b/README.md index 37dcf9dbc..d803b3d5d 100644 --- a/README.md +++ b/README.md @@ -195,3 +195,8 @@ LM 不隐式构建依赖栈,也不安装 Core 的独立 Python runtime。 ```bash python examples/bench.py --device nvidia --model= --enable-paged-attn [--attn=default | --attn=flash-attn] ``` + +### Clover on Hygon + +Native Clover block diffusion and the optional frontier/expert-reuse decoder are +documented in [the Hygon guide](docs/clover-hygon.md). diff --git a/csrc/engine/infer_engine.cpp b/csrc/engine/infer_engine.cpp index b2243ef25..b0a0e970e 100644 --- a/csrc/engine/infer_engine.cpp +++ b/csrc/engine/infer_engine.cpp @@ -448,6 +448,8 @@ InferEngine::Input::to_model_input( : to_device(target_hidden_states), sample_all_positions}; input.logical_kv_upper_bound = logical_kv_upper_bound; + input.diffusion_preview = diffusion_preview; + input.diffusion_expert_budget = diffusion_expert_budget; auto &attn_metadata = forward_context.attn_metadata; attn_metadata = { diff --git a/csrc/engine/rank_worker.hpp b/csrc/engine/rank_worker.hpp index 4fcbb3e0b..d8193bbb9 100644 --- a/csrc/engine/rank_worker.hpp +++ b/csrc/engine/rank_worker.hpp @@ -98,6 +98,8 @@ class RankWorker { std::optional>> sampling_rows; std::optional nsa_owner_snapshot; + bool diffusion_preview{false}; + bool diffusion_expert_budget{false}; infinilm::InfinilmModel::Input to_model_input( infinilm::runtime::Device device, diff --git a/csrc/global_state/forward_context.hpp b/csrc/global_state/forward_context.hpp index 76e7a3939..f47a0e9e4 100644 --- a/csrc/global_state/forward_context.hpp +++ b/csrc/global_state/forward_context.hpp @@ -31,6 +31,7 @@ struct StaticPrefillHostDescription { }; struct AttentionMetadata { + bool diffusion_preview{false}; /// Past Lengths of cached sequence for each request, of shape `[num_requests]`. std::optional past_sequence_lengths; /// ToTal Lengths for each request sequence, of shape `[num_requests]`. @@ -119,6 +120,11 @@ struct ForwardContext { AttentionMetadata attn_metadata; MambaMetadata mamba_metadata; MultiModalMetadata mm_metadata; + bool diffusion_preview{false}; + size_t diffusion_block_size{32}; + bool diffusion_expert_budget{false}; + infinilm::runtime::Tensor diffusion_valid; + infinilm::runtime::Tensor diffusion_live; std::vector kv_cache_vec; std::vector conv_state_vec; std::vector ssm_state_vec; diff --git a/csrc/layers/attention/backends/static_attn.cpp b/csrc/layers/attention/backends/static_attn.cpp index 31e5bb758..6f18b3e37 100644 --- a/csrc/layers/attention/backends/static_attn.cpp +++ b/csrc/layers/attention/backends/static_attn.cpp @@ -95,6 +95,7 @@ infinilm::runtime::Tensor StaticAttentionImpl::forward(const AttentionLayer &lay const bool finite_window = window_size[0] >= 0; bool varlen_candidate = finite_window; varlen_candidate = varlen_candidate || (prefill && seq_len > 1 && kv_quant_scheme_ == infinilm::quantization::KVQuantAlgo::NONE && (q_rope->dtype() == infinilm::runtime::DataType::kFloat16 || q_rope->dtype() == infinilm::runtime::DataType::kBFloat16)); + varlen_candidate = varlen_candidate && !attn_metadata.diffusion_preview; if (finite_window && seq_len > 1 && !prefill) { throw std::invalid_argument("Static finite window requires validated prefill row/past/total metadata"); } @@ -269,7 +270,7 @@ infinilm::runtime::Tensor StaticAttentionImpl::forward(const AttentionLayer &lay throw std::invalid_argument("causal_softmax has unsupported shape or dtype"); } std::optional causal_mask; - if (seq_len != 1) { + if (seq_len != 1 && !attn_metadata.diffusion_preview) { causal_mask = infinilm::runtime::Tensor::empty( runtime, {seq_len, total_seq_len}, attn_weight_softmax->dtype()); } diff --git a/csrc/models/clover_moe/clover_decoder_layer.cpp b/csrc/models/clover_moe/clover_decoder_layer.cpp new file mode 100644 index 000000000..0e8f3c2af --- /dev/null +++ b/csrc/models/clover_moe/clover_decoder_layer.cpp @@ -0,0 +1,83 @@ +#include "clover_decoder_layer.hpp" + +#include "runtime/runtime.hpp" +#include +#include +#include +#include + +namespace infinilm::models::clover_moe { + +CloverDecoderLayer::CloverDecoderLayer(infinilm::runtime::Runtime &runtime, + std::shared_ptr model_config, + size_t layer_idx, + const engine::distributed::RankInfo &rank_info, + backends::AttentionBackend attention_backend, + const layers::moe::EPConfig &ep_config) + : layer_idx_(layer_idx) { + runtime.assertOwnerThread(); + if (runtime.isGraphRecording()) { + throw std::logic_error("Clover decoder construction requires eager Runtime"); + } + if (!model_config || rank_info.device != runtime.device() || rank_info.tp_size <= 0 + || rank_info.tp_rank < 0 || rank_info.tp_rank >= rank_info.tp_size + || (rank_info.tp_size > 1 && !rank_info.comm)) { + throw std::invalid_argument("Invalid explicit Qwen35 decoder construction resources"); + } + const auto layer_types = model_config->get>("layer_types"); + layer_type_ = layer_types.at(layer_idx); + if (layer_type_ != "linear_attention" && layer_type_ != "full_attention") { + throw std::invalid_argument("Clover decoder has unsupported layer_type"); + } + try { + const auto &dtype = model_config->get_dtype(); + const size_t hidden_size = model_config->get("hidden_size"); + const double rms_norm_eps = model_config->get("rms_norm_eps"); + + input_layernorm_ = this->bind_child("input_layernorm", std::make_shared>(runtime, hidden_size, rms_norm_eps, dtype)); + post_attention_layernorm_ = this->bind_child("post_attention_layernorm", std::make_shared>(runtime, hidden_size, rms_norm_eps, dtype)); + mlp_ = this->bind_child("mlp", std::make_shared>(runtime, model_config, layer_idx, rank_info, attention_backend, ep_config)); + + if ("linear_attention" == layer_type_) { + linear_attn_ = this->bind_child("linear_attn", std::make_shared>(runtime, model_config, layer_idx, rank_info, attention_backend, ep_config)); + } else if ("full_attention" == layer_type_) { + self_attn_ = this->bind_child("self_attn", std::make_shared>(runtime, model_config, layer_idx, rank_info, attention_backend)); + } else { + throw std::runtime_error( + "CloverDecoderLayer: unsupported layer_type '" + layer_type_ + + "' for layer " + std::to_string(layer_idx)); + } + } catch (...) { + try { + runtime.syncStream(); + } catch (...) { std::terminate(); } + throw; + } +} + +std::tuple CloverDecoderLayer::forward( + const infinilm::runtime::Tensor &positions, + infinilm::runtime::Tensor &hidden_states, + infinilm::runtime::Tensor &residual, + infinilm::global_state::ForwardContext &forward_context) { + auto &runtime = forward_context.require_runtime(); + runtime.assertOwnerThread(); + if (!hidden_states || hidden_states->device() != runtime.device() + || (residual && residual->device() != runtime.device()) + || input_layernorm_->weight()->device() != runtime.device() + || post_attention_layernorm_->weight()->device() != runtime.device()) { + throw std::invalid_argument("CloverDecoderLayer: forward Runtime/input/norm device mismatch"); + } + input_layernorm_->forward_inplace(runtime, hidden_states, residual); + if ("linear_attention" == layer_type_) { + hidden_states = linear_attn_->forward(hidden_states, forward_context); + } else { + hidden_states = self_attn_->forward(positions, hidden_states, forward_context); + } + + post_attention_layernorm_->forward_inplace(runtime, hidden_states, residual); + hidden_states = mlp_->forward(runtime, hidden_states, forward_context); + return std::make_tuple(hidden_states, residual); +} + +} // namespace infinilm::models::clover_moe diff --git a/csrc/models/clover_moe/clover_decoder_layer.hpp b/csrc/models/clover_moe/clover_decoder_layer.hpp new file mode 100644 index 000000000..cbe7503fa --- /dev/null +++ b/csrc/models/clover_moe/clover_decoder_layer.hpp @@ -0,0 +1,43 @@ +#pragma once +#include "../../layers/normalization/rmsnorm.hpp" +#include "../../weights/weights.hpp" + +#include "../qwen3_5/qwen3_5_attention.hpp" +#include "clover_gdn.hpp" +#include "clover_moe.hpp" + +#include +#include + +namespace infinilm::models::clover_moe { + +class CloverDecoderLayer : public infinilm::weights::ModelWeights { +public: + CloverDecoderLayer(infinilm::runtime::Runtime &runtime, + std::shared_ptr model_config, + size_t layer_idx, + const engine::distributed::RankInfo &rank_info, + backends::AttentionBackend attention_backend, + const layers::moe::EPConfig &ep_config); + + std::tuple forward( + const infinilm::runtime::Tensor &positions, + infinilm::runtime::Tensor &hidden_states, + infinilm::runtime::Tensor &residual, + infinilm::global_state::ForwardContext &forward_context); + + size_t layer_idx() const { return layer_idx_; } + +protected: + std::shared_ptr input_layernorm_; + std::shared_ptr post_attention_layernorm_; + std::shared_ptr self_attn_; + std::shared_ptr linear_attn_; + std::shared_ptr mlp_; + +private: + size_t layer_idx_; + std::string layer_type_; +}; + +} // namespace infinilm::models::clover_moe diff --git a/csrc/models/clover_moe/clover_for_causal_lm.cpp b/csrc/models/clover_moe/clover_for_causal_lm.cpp new file mode 100644 index 000000000..f38131fa6 --- /dev/null +++ b/csrc/models/clover_moe/clover_for_causal_lm.cpp @@ -0,0 +1,105 @@ +#include "../../layers/causal_lm_templates/text_causal_lm.hpp" +#include "../models_registry.hpp" +#include "../qwen3_5/qwen3_5_for_causal_lm.hpp" +#include "../qwen3_next/qwen3_next_allocate_kv_cache_tensors.hpp" +#include "clover_decoder_layer.hpp" +#include + +namespace infinilm::models::clover_moe { + +using CloverModel = layers::causal_lm_templates::TextModel; + +class CloverForCausalLM : public layers::causal_lm_templates::TextCausalLM { +public: + using Base = layers::causal_lm_templates::TextCausalLM; + using Base::Base; + + Output forward(const Input &input, global_state::ForwardContext &context) const override { + auto &runtime = context.require_runtime(); + if (runtime.isGraphRecording()) { + throw std::invalid_argument("Clover requires eager execution."); + } + context.diffusion_preview = input.diffusion_preview; + context.diffusion_expert_budget = input.diffusion_expert_budget; + context.attn_metadata.diffusion_preview = input.diffusion_preview; + context.diffusion_block_size = model_config_->get("block_size"); + if (!input.diffusion_preview) { + denoise_step_ = 0; + } + if (input.diffusion_preview) { + const auto tokens = input.input_ids.value()->numel(); + if (!input.sample_all_positions || tokens % context.diffusion_block_size) { + throw std::invalid_argument("Clover preview requires complete blocks and all-position logits."); + } + context.diffusion_valid = runtime::Tensor::empty(runtime, {tokens}, runtime::DataType::kInt8); + execution::submit(runtime, infini::ops::Fill::DefaultConfig(runtime.device().type()), + context.diffusion_valid, 1.0, context.diffusion_valid); + if (input.diffusion_expert_budget) { + bool refresh = denoise_step_ % 5 == 0 || !previous_budget_; + const auto &ids = input.input_ids.value(); + if (!previous_ids_ || previous_ids_->shape() != ids->shape()) { + previous_ids_ = runtime::Tensor::empty(runtime, ids->shape(), ids->dtype()); + refresh = true; + } + context.diffusion_live = runtime::Tensor::empty(runtime, {tokens}, runtime::DataType::kInt8); + execution::submit(runtime, execution::RecordingPolicy::HostOnly, + infini::ops::CloverLiveMask::DefaultConfig(runtime.device().type()), ids, previous_ids_, + static_cast(model_config_->get("mask_token_id")), refresh, context.diffusion_live); + } + previous_budget_ = input.diffusion_expert_budget; + ++denoise_step_; + } + return Base::forward(input, context); + } + + void reset_cache(const cache::CacheConfig *cache_config, global_state::ForwardContext &context, + backends::AttentionBackend backend, const engine::distributed::RankInfo &rank) override { + if (backend != backends::AttentionBackend::STATIC_ATTN) { + throw std::invalid_argument("Clover currently requires the static attention backend."); + } + auto &runtime = context.require_runtime(); + denoise_step_ = 0; + previous_budget_ = false; + previous_ids_.reset(); + auto allocated = qwen3_next::qwen3_next_allocate_cache_tensors(runtime, rank, cache_config, model_config_, backend); + for (auto &state : allocated.ssm_state_tensors) { + if (!state) { + continue; + } + state = runtime::Tensor::empty(runtime, state->shape(), runtime::DataType::kFloat32); + execution::submit(runtime, infini::ops::Fill::DefaultConfig(runtime.device().type()), state, 0.0, state); + } + context.kv_cache_vec = std::move(allocated.kv_cache_tensors); + context.conv_state_vec = std::move(allocated.conv_state_tensors); + context.ssm_state_vec = std::move(allocated.ssm_state_tensors); + cache_config_ = cache_config ? cache_config->unique_copy() : nullptr; + } + +private: + mutable size_t denoise_step_{0}; + mutable bool previous_budget_{false}; + mutable runtime::Tensor previous_ids_; +}; + +std::shared_ptr create_clover_config(std::shared_ptr config) { + if (config->get_dtype() != runtime::DataType::kBFloat16) { + throw std::invalid_argument("Clover requires BF16 model weights."); + } + auto &json = config->get_config_json(); + if (json.at("model_type") != "clover_moe" || json.at("block_size") != 32 || json.at("linear_key_head_dim") != 128 || json.at("linear_value_head_dim") != 128 || json.at("linear_conv_kernel_dim") != 4 || json.at("num_experts") != 256 || json.at("num_experts_per_tok") != 8) { + throw std::invalid_argument("Unsupported Clover architecture."); + } + json["norm_topk_prob"] = true; + json["position_id_axes"] = 1; + json["rope_parameters"]["mrope_section"] = {16, 8, 8}; + json["rope_parameters"]["mrope_interleaved"] = false; + return qwen3_5::prepare_qwen3_5_model_config(config); +} + +} // namespace infinilm::models::clover_moe + +namespace { +INFINILM_REGISTER_EXPLICIT_CAUSAL_LM_MODEL(clover_moe, + infinilm::models::clover_moe::CloverForCausalLM, + infinilm::models::clover_moe::create_clover_config); +} diff --git a/csrc/models/clover_moe/clover_gdn.cpp b/csrc/models/clover_moe/clover_gdn.cpp new file mode 100644 index 000000000..85dde42b7 --- /dev/null +++ b/csrc/models/clover_moe/clover_gdn.cpp @@ -0,0 +1,104 @@ +#include "clover_gdn.hpp" +#include "../../global_state/forward_context.hpp" +#include "execution/submit.hpp" +#include "runtime/runtime.hpp" +#include +#include +#include + +namespace infinilm::models::clover_moe { + +CloverGatedDeltaNet::CloverGatedDeltaNet(runtime::Runtime &runtime, std::shared_ptr config, + size_t layer, const engine::distributed::RankInfo &rank, + backends::AttentionBackend backend, const layers::moe::EPConfig &ep) + : Qwen3NextGatedDeltaNet(runtime, config, layer, rank, backend, ep), + tp_rank_(rank.tp_rank), tp_size_(rank.tp_size), + full_key_dim_(local_key_dim_ * tp_size_), full_value_dim_(local_value_dim_ * tp_size_) { + try { + const auto dtype = config->get_dtype(); + right_weight_ = weights::Weight(runtime, {2 * full_key_dim_ + full_value_dim_, 1, 3}, dtype); + bind_weight("conv_right_weight", right_weight_); + const auto heads = config->get("linear_num_value_heads"); + logit_scale_ = weights::Weight(runtime, {heads}, dtype, 0, tp_rank_, tp_size_); + history_bias_ = weights::Weight(runtime, {heads}, dtype, 0, tp_rank_, tp_size_); + history_c_ = weights::Weight(runtime, {heads}, dtype, 0, tp_rank_, tp_size_); + bind_weight("bd_softmax_logit_scale", logit_scale_); + bind_weight("bd_softmax_history_bias", history_bias_); + bind_weight("bd_softmax_history_c", history_c_); + } catch (...) { + runtime.syncStream(); + throw; + } +} + +void CloverGatedDeltaNet::process_weights_after_loading(runtime::Runtime &runtime, bool) { + conv_weight_ = conv1d_->state_dict().at("weight").tensor(); + auto local = runtime::Tensor::empty(runtime, {2 * local_key_dim_ + local_value_dim_, 1, 3}, right_weight_->dtype()); + for (size_t part = 0; part < 3; ++part) { + const size_t width = part == 2 ? local_value_dim_ : local_key_dim_; + const size_t src = part * full_key_dim_ + tp_rank_ * width; + const size_t dst = part * local_key_dim_; + local->narrow({{0, dst, width}})->copy_from(runtime, right_weight_->narrow({{0, src, width}})); + } + runtime.syncStream(); + right_weight_ = weights::Weight(local); + local_weights_["conv_right_weight"] = right_weight_; +} + +runtime::Tensor CloverGatedDeltaNet::forward(const runtime::Tensor &hidden, + const global_state::ForwardContext &context) const { + auto &runtime = context.require_runtime(); + auto input = hidden; + const size_t tokens = hidden->size(0) * hidden->size(1); + auto qkv = in_proj_qkv_->forward(runtime, input)->view({1, tokens, 2 * local_key_dim_ + local_value_dim_}); + auto z = in_proj_z_->forward(runtime, input); + auto a = in_proj_a_->forward(runtime, input)->view({1, tokens, local_num_value_heads_}); + auto b = in_proj_b_->forward(runtime, input)->view({1, tokens, local_num_value_heads_}); + const auto device = runtime.device().type(); + runtime::Tensor mixed; + if (context.diffusion_preview) { + auto flat = qkv->view({tokens, qkv->size(2)}); + auto out = runtime::Tensor::empty(runtime, flat->shape(), flat->dtype()); + execution::submit( + runtime, execution::RecordingPolicy::HostOnly, + infini::ops::CloverBlockConv1d::DefaultConfig(device), + flat, context.conv_state_vec[layer_idx_], context.mamba_metadata.init_state_indices.value(), + conv_weight_, right_weight_.tensor(), context.diffusion_valid, context.diffusion_block_size, out); + mixed = out->view(qkv->shape()); + } else { + mixed = conv1d_->forward(qkv, context); + } + auto q = mixed->narrow({{2, 0, local_key_dim_}})->as_strided({1, tokens, local_num_key_heads_, key_head_dim_}, {mixed->stride(0), mixed->stride(1), static_cast(key_head_dim_), 1}); + auto k = mixed->narrow({{2, local_key_dim_, local_key_dim_}})->as_strided(q->shape(), q->strides()); + auto v = mixed->narrow({{2, 2 * local_key_dim_, local_value_dim_}})->as_strided({1, tokens, local_num_value_heads_, value_head_dim_}, {mixed->stride(0), mixed->stride(1), static_cast(value_head_dim_), 1}); + auto g = runtime::Tensor::empty(runtime, a->shape(), runtime::DataType::kFloat32); + auto beta = runtime::Tensor::empty(runtime, a->shape(), runtime::DataType::kFloat32); + execution::submit( + runtime, execution::RecordingPolicy::HostOnly, + infini::ops::FusedGatedDeltaNetGating::DefaultConfig(device), + A_log_.tensor(), a, b, dt_bias_.tensor(), 1.0f, 20.0f, g, beta); + auto out = runtime::Tensor::empty(runtime, v->shape(), v->dtype()); + if (context.diffusion_preview) { + execution::submit( + runtime, execution::RecordingPolicy::HostOnly, + infini::ops::CloverLocalMemory::DefaultConfig(device), + q, k, v, g, context.ssm_state_vec[layer_idx_], context.mamba_metadata.init_state_indices.value(), + context.diffusion_valid, logit_scale_.tensor(), history_bias_.tensor(), history_c_.tensor(), + context.diffusion_block_size, out); + } else { + execution::submit( + runtime, execution::RecordingPolicy::HostOnly, + infini::ops::CloverRecurrentGdn::DefaultConfig(device), + q, k, v, g, beta, context.mamba_metadata.input_offsets.value(), + context.mamba_metadata.init_state_indices.value(), context.mamba_metadata.final_state_indices.value(), + context.ssm_state_vec[layer_idx_], out); + } + auto normalized = norm_->forward(runtime, out->view({tokens * local_num_value_heads_, value_head_dim_})); + normalized = normalized->view(z->shape()); + auto gate = runtime::Tensor::empty(runtime, z->shape(), z->dtype()); + execution::submit(runtime, infini::ops::Silu::DefaultConfig(device), z, gate); + execution::submit(runtime, infini::ops::Mul::DefaultConfig(device), normalized, gate, normalized); + return out_proj_->forward(runtime, normalized); +} + +} // namespace infinilm::models::clover_moe diff --git a/csrc/models/clover_moe/clover_gdn.hpp b/csrc/models/clover_moe/clover_gdn.hpp new file mode 100644 index 000000000..36589dc1d --- /dev/null +++ b/csrc/models/clover_moe/clover_gdn.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "../qwen3_next/qwen3_next_gated_deltanet.hpp" + +namespace infinilm::models::clover_moe { + +class CloverGatedDeltaNet : public qwen3_next::Qwen3NextGatedDeltaNet { +public: + CloverGatedDeltaNet(runtime::Runtime &runtime, std::shared_ptr config, + size_t layer, const engine::distributed::RankInfo &rank, + backends::AttentionBackend backend, const layers::moe::EPConfig &ep); + runtime::Tensor forward(const runtime::Tensor &hidden, + const global_state::ForwardContext &context) const; + void process_weights_after_loading(runtime::Runtime &runtime, bool pre_transpose) override; + +private: + weights::Weight right_weight_, logit_scale_, history_bias_, history_c_; + runtime::Tensor conv_weight_; + size_t tp_rank_, tp_size_, full_key_dim_, full_value_dim_; +}; + +} // namespace infinilm::models::clover_moe diff --git a/csrc/models/clover_moe/clover_moe.cpp b/csrc/models/clover_moe/clover_moe.cpp new file mode 100644 index 000000000..9b699680c --- /dev/null +++ b/csrc/models/clover_moe/clover_moe.cpp @@ -0,0 +1,104 @@ +#include "clover_moe.hpp" +#include "../../global_state/forward_context.hpp" +#include "../../layers/linear/dense.hpp" +#include "execution/submit.hpp" +#include +#include +#include + +namespace infinilm::models::clover_moe { + +CloverSparseMoe::CloverSparseMoe(runtime::Runtime &runtime, std::shared_ptr config, + size_t layer, const engine::distributed::RankInfo &rank, + backends::AttentionBackend backend, const layers::moe::EPConfig &ep) + : Qwen3NextSparseMoeBlock(runtime, config, layer, rank, backend, ep), + coreset_(config->get_or("moe_block_coreset_enabled", true)), + top_p_(config->get_or("moe_block_coreset_top_p", 0.9)), + top_k_(config->get("num_experts_per_tok")) { + router_weight_ = gate_->state_dict().at("weight").tensor(); +} + +std::tuple CloverSparseMoe::route( + runtime::Runtime &runtime, const runtime::Tensor &hidden, + const global_state::ForwardContext &context) const { + if (!context.diffusion_preview || (!coreset_ && !context.diffusion_expert_budget)) { + return gate_->forward(runtime, hidden); + } + auto logits = layers::linear::dense_projection(runtime, hidden, router_weight_, std::nullopt, 1.0f); + auto masked = logits; + const auto device = runtime.device().type(); + if (coreset_) { + masked = runtime::Tensor::empty(runtime, logits->shape(), logits->dtype()); + execution::submit(runtime, execution::RecordingPolicy::HostOnly, + infini::ops::CloverCoreset::DefaultConfig(device), logits, context.diffusion_block_size, top_p_, masked); + } + const auto active_top_k = context.diffusion_expert_budget ? size_t{4} : top_k_; + auto weights = runtime::Tensor::empty(runtime, {hidden->size(0), active_top_k}, runtime::DataType::kFloat32); + auto indices = runtime::Tensor::empty(runtime, weights->shape(), runtime::DataType::kInt32); + auto scratch = runtime::Tensor::empty(runtime, weights->shape(), runtime::DataType::kInt32); + execution::submit(runtime, execution::RecordingPolicy::HostOnly, + infini::ops::TopkSoftmax::DefaultConfig(device), masked, + std::optional{}, std::optional{}, true, + weights, indices, scratch); + return {weights, indices}; +} + +infinilm::runtime::Tensor CloverSparseMoe::forward(infinilm::runtime::Runtime &runtime, + const infinilm::runtime::Tensor &hidden_states, + const global_state::ForwardContext &context) const { + ASSERT(hidden_states->ndim() == 3); + + auto shape = hidden_states->shape(); + auto hidden_states_reshaped = hidden_states->view({shape[0] * shape[1], shape[2]}); + + auto [routing_weights, selected_experts] = route(runtime, hidden_states_reshaped, context); + if (context.diffusion_preview && context.diffusion_expert_budget) { + execution::submit(runtime, execution::RecordingPolicy::HostOnly, + infini::ops::CloverMaskRoutes::DefaultConfig(runtime.device().type()), + selected_experts, context.diffusion_live, int64_t{256}, selected_experts); + } + infinilm::layers::moe::TopKOutput topk_output{ + routing_weights, + selected_experts, + infinilm::runtime::Tensor(), + }; + auto routed_states = fused_moe_->forward( + runtime, hidden_states_reshaped, + topk_output, + experts_->moe_weights()); + if (context.diffusion_preview && context.diffusion_expert_budget) { + if (!cached_routed_ || cached_routed_->shape() != routed_states->shape()) { + cached_routed_ = runtime::Tensor::empty(runtime, routed_states->shape(), routed_states->dtype()); + } + execution::submit(runtime, execution::RecordingPolicy::HostOnly, + infini::ops::CloverMergeMoe::DefaultConfig(runtime.device().type()), + routed_states, context.diffusion_live, cached_routed_, routed_states); + } + + auto shared_states = shared_expert_->forward(runtime, hidden_states); + auto hidden_states_for_gate = hidden_states; + const auto ops_input = shared_expert_gate_->forward(runtime, hidden_states_for_gate); + auto shared_gate = infinilm::runtime::Tensor::empty(runtime, ops_input->shape(), ops_input->dtype()); + infinilm::execution::submit( + runtime, infini::ops::Sigmoid::DefaultConfig(runtime.device().type()), ops_input, shared_gate); + INFINILM_TRACE_OP(Sigmoid, shared_gate); + shared_gate = shared_gate->as_strided(shared_states->shape(), {shared_gate->stride(0), shared_gate->stride(1), 0}); + { + auto ops_output = infinilm::runtime::Tensor::empty(runtime, shared_states->shape(), shared_states->dtype()); + infinilm::execution::submit( + runtime, infini::ops::Mul::DefaultConfig(runtime.device().type()), shared_states, shared_gate, ops_output); + INFINILM_TRACE_OP(Mul, ops_output); + shared_states = std::move(ops_output); + } + + auto routed_states_3d = routed_states->as_strided( + {shape[0], shape[1], shape[2]}, + {static_cast(shape[1] * shape[2]), static_cast(shape[2]), 1}); + auto ops_output = infinilm::runtime::Tensor::empty(runtime, routed_states_3d->shape(), routed_states_3d->dtype()); + infinilm::execution::submit( + runtime, infini::ops::Add::DefaultConfig(runtime.device().type()), routed_states_3d, shared_states, ops_output); + INFINILM_TRACE_OP(Add, ops_output); + return ops_output; +} + +} // namespace infinilm::models::clover_moe diff --git a/csrc/models/clover_moe/clover_moe.hpp b/csrc/models/clover_moe/clover_moe.hpp new file mode 100644 index 000000000..8c1bd558e --- /dev/null +++ b/csrc/models/clover_moe/clover_moe.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "../../global_state/forward_context.hpp" +#include "../qwen3_next/qwen3_next_sparse_moe_block.hpp" + +namespace infinilm::models::clover_moe { + +class CloverSparseMoe : public qwen3_next::Qwen3NextSparseMoeBlock { +public: + CloverSparseMoe(runtime::Runtime &runtime, std::shared_ptr config, + size_t layer, const engine::distributed::RankInfo &rank, + backends::AttentionBackend backend, const layers::moe::EPConfig &ep); + runtime::Tensor forward(runtime::Runtime &runtime, const runtime::Tensor &hidden, + const global_state::ForwardContext &context) const; + +private: + std::tuple route(runtime::Runtime &runtime, + const runtime::Tensor &hidden, const global_state::ForwardContext &context) const; + runtime::Tensor router_weight_; + mutable runtime::Tensor cached_routed_; + bool coreset_; + double top_p_; + size_t top_k_; +}; + +} // namespace infinilm::models::clover_moe diff --git a/csrc/models/infinilm_model.hpp b/csrc/models/infinilm_model.hpp index 073bd4681..f35e38bfb 100644 --- a/csrc/models/infinilm_model.hpp +++ b/csrc/models/infinilm_model.hpp @@ -67,6 +67,8 @@ class InfinilmModel : public infinilm::weights::ModelWeights { /// Preserve logits for every packed position for speculative/MTP callers. bool sample_all_positions{false}; size_t logical_kv_upper_bound{0}; + bool diffusion_preview{false}; + bool diffusion_expert_budget{false}; }; struct Output { diff --git a/csrc/models/qwen3_next/qwen3_next_gated_deltanet.hpp b/csrc/models/qwen3_next/qwen3_next_gated_deltanet.hpp index a74578fa9..aba19843d 100644 --- a/csrc/models/qwen3_next/qwen3_next_gated_deltanet.hpp +++ b/csrc/models/qwen3_next/qwen3_next_gated_deltanet.hpp @@ -64,7 +64,7 @@ class Qwen3NextGatedDeltaNet : public infinilm::weights::ModelWeights { infinilm::runtime::Tensor forward(const infinilm::runtime::Tensor &hidden_states, const infinilm::global_state::ForwardContext &forward_context) const; -private: +protected: std::shared_ptr in_proj_qkv_; std::shared_ptr in_proj_z_; std::shared_ptr in_proj_a_; diff --git a/csrc/pybind11/bindings.cc b/csrc/pybind11/bindings.cc index 4a4dda6aa..2ef7d37c3 100644 --- a/csrc/pybind11/bindings.cc +++ b/csrc/pybind11/bindings.cc @@ -37,6 +37,10 @@ PYBIND11_MODULE(_infinilm, m) { || !checkpoint_float(input->dtype()) || !checkpoint_float(output->dtype())) { throw std::invalid_argument("checkpoint Cast requires matching CPU shapes and FP32/FP16/BF16 tensors"); } + if (input->numel() != 0 && input->data() == output->data() + && input->dtype() != output->dtype()) { + throw std::invalid_argument("checkpoint dtype conversion requires distinct storage"); + } const infini::ops::Tensor input_view{input->view()}; const infini::ops::Tensor output_view{output->view()}; infini::ops::Cast::Call(input_view, output_view); }, py::arg("input"), py::arg("output"), py::call_guard()); diff --git a/csrc/pybind11/engine/engine.hpp b/csrc/pybind11/engine/engine.hpp index 2fbcdeec1..5927ecfe4 100644 --- a/csrc/pybind11/engine/engine.hpp +++ b/csrc/pybind11/engine/engine.hpp @@ -233,6 +233,8 @@ inline void bind_infer_engine(py::module &m) { "random_values", "sampling_rows", "nsa_owner_snapshot", + "diffusion_preview", + "diffusion_expert_budget", }; for (auto &item : kwargs) { @@ -243,7 +245,11 @@ inline void bind_infer_engine(py::module &m) { "InferEngine.Input got an unexpected keyword argument '" + key + "'"); } - if (key == "temperature") { + if (key == "diffusion_expert_budget") { + input.diffusion_expert_budget = py::cast(item.second); + } else if (key == "diffusion_preview") { + input.diffusion_preview = py::cast(item.second); + } else if (key == "temperature") { input.temperature = py::cast(item.second); } else if (key == "top_p") { input.top_p = py::cast(item.second); @@ -303,6 +309,8 @@ inline void bind_infer_engine(py::module &m) { .def_readwrite("visual_token_ranges", &InferEngine::Input::visual_token_ranges) .def_readwrite("target_hidden_states", &InferEngine::Input::target_hidden_states) .def_readwrite("sample_all_positions", &InferEngine::Input::sample_all_positions) + .def_readwrite("diffusion_preview", &InferEngine::Input::diffusion_preview) + .def_readwrite("diffusion_expert_budget", &InferEngine::Input::diffusion_expert_budget) .def_readwrite("temperature", &InferEngine::Input::temperature) .def_readwrite("top_k", &InferEngine::Input::top_k) .def_readwrite("top_p", &InferEngine::Input::top_p) diff --git a/docs/clover-hygon-ops.cmake b/docs/clover-hygon-ops.cmake new file mode 100644 index 000000000..9dd4023bf --- /dev/null +++ b/docs/clover-hygon-ops.cmake @@ -0,0 +1,4 @@ +# Public APIs needed to compile the complete InfiniLM source tree. +set(INFINI_OPS_DECLARATIONS + "add;argmax;cast;cat;causal_conv1d;chunk_gated_delta_rule;clover_block_conv1d;clover_coreset;clover_local_memory;clover_recurrent_gdn;convolution;copy;deepseek_moe;embedding;fill;flash_attn_varlen_func;flash_attn_with_kvcache;fused_add_rms_norm;fused_gated_delta_net_gating;gelu;gemm;i8_gemm;internal_softmax;layer_norm;mamba_selective_scan;moe_align_block_size;moe_fused_dense;moe_fused_gate;mrope;mul;pad;per_channel_quant_i8;per_tensor_dequant_i8;per_tensor_quant_i8;quick_gelu;recurrent_gated_delta_rule;relu;reshape_and_cache_flash;rms_norm;rotary_embedding;row_l2_normalize;separate_table_rope;sigmoid;silu;silu_and_mul;softmax;sum;take;top_k_top_p_sampling_from_logits;topk_sigmoid;topk_softmax;tril;triu;upsample_bicubic2d;upsample_bilinear2d;w4_marlin_gemm;w4_marlin_parameters;w4_marlin_repack" + CACHE STRING "Additional public operator declarations" FORCE) diff --git a/docs/clover-hygon.md b/docs/clover-hygon.md new file mode 100644 index 000000000..05ef81f80 --- /dev/null +++ b/docs/clover-hygon.md @@ -0,0 +1,169 @@ +# Clover block diffusion on Hygon + +`clover_moe` loads the complete Clover-35B-A3B-Preview checkpoint, including its +right-convolution and bidirectional GDN scalar shards. Model forward runs in +InfiniLM C++; InfiniOps supplies Hygon kernels. Triton compiles kernels on first +use and the native driver launches their binaries thereafter. Torch is used by +the Python decoder for confidence selection, not as a model-forward fallback. + +The model currently requires BF16 weights. The validated configuration is TP=2 on two Hygon BW 64GB devices, DTK +26.04, eager execution, static cache, and 32-token diffusion blocks. The chat +endpoint serializes requests and supports greedy, non-streaming text generation. +The measurements below cover concurrency one and short prompts; they are not a +measurement of maximum concurrent-serving capacity or 128K-context performance. + +## Build + +Build the matching InfiniRT, InfiniOps and InfiniCCL revisions first. The Hygon +InfiniRT driver API and InfiniOps Clover providers are required; the original +InfiniLM dependency pin does not supply them. The dependency PR links are +recorded in this change's PR description. + +```sh +export INFINI_ROOT=/path/to/installed-stack +export CUDA_HOME=/opt/dtk/cuda/cuda +export LD_LIBRARY_PATH="$INFINI_ROOT/lib:$CUDA_HOME/lib64:$LD_LIBRARY_PATH" +cmake -S third_party/InfiniCore/submodules/InfiniRT -B build/rt \ + -DCMAKE_INSTALL_PREFIX="$INFINI_ROOT" -DWITH_CPU=ON -DWITH_HYGON=ON +cmake --build build/rt -j 12 +cmake --install build/rt +cmake -S third_party/InfiniCore/submodules/InfiniOps -B build/ops \ + -C docs/clover-hygon-ops.cmake -DCMAKE_INSTALL_PREFIX="$INFINI_ROOT" \ + -DINFINI_RT_ROOT="$INFINI_ROOT" -DWITH_CPU=ON -DWITH_HYGON=ON \ + -DWITH_TRITON=ON -DHYGON_ARCH=gfx936 -DGENERATE_PYTHON_BINDINGS=ON \ + -DAUTO_DETECT_DEVICES=OFF -DAUTO_DETECT_BACKENDS=OFF +cmake --build build/ops -j 12 +cmake --install build/ops +cmake -S third_party/InfiniCore/submodules/InfiniCCL -B build/ccl \ + -DCMAKE_INSTALL_PREFIX="$INFINI_ROOT" -DWITH_HYGON=ON -DWITH_NCCL=ON \ + -DAUTO_DETECT_DEVICES=OFF -DBUILD_EXAMPLES=OFF +cmake --build build/ccl -j 12 +cmake --install build/ccl +export INFINILM_CXX11_ABI=1 +xmake f -y -m release --cxx11-abi=1 +pip install -e '.[clover]' --no-build-isolation +``` + +Match the C++ ABI to the installed DTK Torch build. `gfx936` is the tested +architecture, not a default for all Hygon products. Install the InfiniOps Python +package as `infini` as well: its JIT compiler and kernel sources must remain +importable. A direct CMake install puts that package at the install prefix; +placing a link named `infini` to this prefix in a Python search directory is +sufficient for a development build. A wheel installs this namespace normally. + +The declaration preset lets the complete InfiniLM source compile when other +model families reference operators without Hygon providers. Such operators +remain unsupported; generating their APIs does not install substitute kernels. + +## Generate and serve + +```python +from infinilm.diffusion.clover import CloverGenerator + +generator = CloverGenerator('/path/to/Clover-35B-A3B-Preview', tp=2, max_context=2048) +result = generator.generate('What is the capital of France?', max_new_tokens=64) +print(result.text) +``` + +```sh +HIP_VISIBLE_DEVICES=0,1 python -m infinilm.server.clover_server \ + --model /path/to/Clover-35B-A3B-Preview --tp 2 --max-context 2048 \ + --policy frontier --expert-budget --frontier-floor 0.7 --port 8000 +``` + +Send ordinary `/v1/chat/completions` requests with `temperature: 0` and +`stream: false`. Optional request fields are `diffusion_policy` (`baseline` or +`frontier`), `expert_budget`, and `frontier_confidence_floor`. Neutral repetition, +presence and frequency penalties and thinking disabled define the tested lane. +Nonzero penalties, stochastic sampling and streaming receive explicit errors. + +## Methods and state contract + +Preview reads the committed convolution/GDN state. Full attention can read the +causal prefix and all positions within the current block. Preview KV writes are +confined to the uncommitted suffix. Every block is fully resolved before causal +replay updates persistent state. Mask tokens are never emitted or committed. + +`baseline` uses the checkpoint's 0.95 low-confidence rule with top-one progress +when no masked position clears the threshold. `frontier` adapts the target +stride from Ringssss/dLLM-serve V3. It retains all positions above 0.95, adds +safe positions toward the stride, and guarantees progress. Its confidence +floor is explicit because lowering it changes decoding and can harm output. + +`expert_budget` adapts the supplied dllm-h100 study's top-4 expert budget and +TV6 decoded-row reuse with refresh interval five. Clover's own block coreset +continues to define the allowed experts. A token is eligible for reuse only +when it was unmasked in both consecutive previews. Inactive routes are omitted +from expert alignment/GEMMs and their previously combined outputs are restored. +Shared experts and causal replay still compute normally. State is reset at each +new block/request. These are optional approximate preview optimizations. + +This port does not implement the H100 code's DP/EP sequence-parallel communication +pipeline, or dLLM-serve's complete admission/compaction scheduler. The serial +endpoint exposes the mechanisms validated here. It also does not implement +Clover's separate self-speculative AR-verification serving mode. + +Source provenance: + +- dLLM-serve: `1da50a96c9ce6b888d245d378f2bac0425feba8c`, + `cwsrpt_v2/algorithm/cw_srpt_v3.py`, Apache-2.0; retained license/notice under + `python/infinilm/licenses/dLLM-serve`. +- Supplied dllm-h100 archive: top-4 routing and refresh/live-row logic in + `codex_coding/src/{test_m_skip_sweep.py,tv6_patch.py}`; reimplemented for the + Clover softmax router and InfiniLM state ownership. +- Preview kernels are provided and attributed by the companion InfiniOps PR. + +## Validation and measurement + +The native baseline matched the local SGLang Clover port's output tokens on +four initial prompts. Operator comparisons cover preview convolution/GDN, +clean-state immutability, packed recurrent commits, expert selection, fused +MoE, liveness and cached-row merging. Broader generative outputs can differ +across kernel implementations even with identical sampling settings. + +The recorded HTTP campaign uses the same model, tokenizer, BF16 precision, +TP2 budget, prompts, per-request output caps and sampling controls. Each of five +lanes ran 32 prompts three times after a separate warmup, alternating lane +order. Throughput is visible completion tokens, retokenized identically, divided +by summed client request time. SGLang's diffusion usage counters can include +block padding, so they are not used as the numerator. Results and the exact +workload are recorded in the PR evidence; no profiling run contributes to timing. + +A portable client and the controlled 32-prompt workload are included: + +```sh +python examples/bench_clover.py --url http://127.0.0.1:8000 --model clover \ + --tokenizer /path/to/Clover-35B-A3B-Preview --output native.jsonl +python examples/bench_clover.py --url http://127.0.0.1:8001 --model /served/model/name \ + --tokenizer /path/to/Clover-35B-A3B-Preview --backend sglang --variants baseline --output sglang.jsonl +``` + +Repeat `-1` is warmup and must be excluded. Sum `visible_tokens` over successful +requests and divide by summed `elapsed_s` over all measured requests, retaining +failures. Keep model weights, TP, sampling, output caps and context capacity fixed. + +### Recorded short-prompt campaign + +| Variant | Visible output tok/s | Per DCU tok/s | +| --- | ---: | ---: | +| InfiniLM baseline | 28.57 | 14.29 | +| Frontier stride | 38.47 | 19.24 | +| Expert budget and live-row reuse | 28.70 | 14.35 | +| Joint, confidence floor 0.7 | 39.92 | 19.96 | +| Local SGLang Clover iterative port | 5.88 | 2.94 | + +The joint path is 1.397x the native baseline and 6.791x this local SGLang port. +Three joint repetitions measured 39.85–40.02 visible tok/s. The SGLang reference +is the local DTK 0.5.12 port of iterative Clover, with eager Triton attention/MoE; +it is not the official self-speculative implementation. The two lanes use +separate pairs of identical BW devices on the same host. Context capacity is +2048 for both. These numbers include HTTP/request latency and real EOS behavior. + +All requests completed. The controlled structural/answer checks passed 96/96 +for joint, 93/96 for expert-only and 90/96 for baseline/frontier/SGLang. This +checks the included fixture, not general quality equivalence. Generated text +can still contain imperfect wording. A lower floor of 0.5 produced invalid +Python and repetitions and was rejected. Keep outputs and failed candidates in +any further evaluation. + +![Rendered validation record](images/clover-hygon-validation.png) diff --git a/docs/images/clover-hygon-validation.png b/docs/images/clover-hygon-validation.png new file mode 100644 index 0000000000000000000000000000000000000000..61c415b6ae22843a90b179aef4c2387f28053ad5 GIT binary patch literal 80243 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 z z5Fy(l&NA+~0@ijvqRY7=(RJ6~3yT^q+RwwHg`VaU?BWX9op0=c)_F`ef*j~v6K2fw zubYoySaI^d%Fx(l^b>Kpq%EH#7R+r!$QxS52HTYnim^&=CzrE5j~wqQCup7;qiY?| z|3D>!l}Lt_K({eLlEmuT{gZ{vwXJpNwHy4Y5`ss2d`>9#uwSjoGq5q6{6D=ZX@dOp z5As(zqERyv`<0M0sI&LK`aj%nAulM%E7s(Ou3VDt@-zZ|cfhf(KFWL;AY=k6lo<97 z=6JF|I|awqUFg%l;>_9KM{M~A{Yosmo9y1#1TIx8VX>*aGZ-jGZ_SLIw!Qagfi^&Zit^#oZSbk4HDr@Qb$_Y7&h>46W z$j_P_%wFoO@b5be_K%RvKaeIIny5CY1KV^dK@6nwfChxMg-qsmTy&0&{7^t8Roc)O zkW^S06C*aQHppG9>9zQK$U&=B?edEF-e9~CdM3bIu-;{S6j7{{CFT6_`nI>nE5Xu` zRD)98l2P$9r%LOh^kTIvDFTp`RBV4xt1FPLFI#C=TTxPG=DOv~NH~^jn+r-|O=#Zr zY1N81b=^0U0uq)SF`SwL6CGVeMG@tXP4?{`qckD)E*rHkvZCw1dI5kuFwpwzu4k+> z3B$`%vk$$%=;3ww>{&uKZkeAC-fGEHx&of1_8A`V7^=yeS_nL@cYM}XlZ}%0HDP3v zEzam|NsuM{J*J?ps>)Vi!|wfO-6~dY-TVn_k&17lz)(`BRfMt>sS0sofF}f<8SLXm(8aKrR3GcB^ApIuIn3Wipe+(73>EY-G!x$ zZ^+KO_#}bwSgUU>6EVryKmnXh3JW;s++saKx_0Wfe8u2^D**)G-@kM|=WJ53U(R`L z|4;)6U_e&_GpYx+Z9Ow4FRAEK`0LQaTXgEtTY=zURp~JYy*t8BcXE;!OhUf3;>}^J z_q`0rseF)=$p>E5kf7gND8Q=2N$l0js2kxv8Vel6`XU^Wl#~RRL&Oh`O(aJFICBDC zum|9@Rx5eedwte7JvngggBnFe#p3TxTD;mP&^yUEw-ky?HE?gf31VLagpB{1B>yiN zBGCzTKU8yezcJPmEbfDyI@1=2vAarXr(P3w{M9+rwh0v8KNe&ta=xwQsiyhd2h8{tFhQ_B) zrQ{?*a?RpTF8Vv$M=ee(%0X_GTKUu#GADtnMO6h;wHQDNL3Y$kjWjpZa+up=?{*|?ce-@R282u&qqG0F(+%IvF@E4K~S*j9pQR8(#XW(958(l z2(sAI7Kq0qMK=R@9(!C2OW|{MUyqBg5g_mAq+MOHhr^8o_XLUAd_u>F6<#(z_2M@k z2yVdfid0rQI(!GX2cR)x@(HoY$;u{fC5 zh;~LSVdEC*3SoAR8Yo1B&^WVC*60^rKRC28MHb80i z84o<&lc1PGw6fh9pk7>7Qwn@oxle0#cKe+94-emg7CqnMelUCFNgKBPXQcQNZParx zIz%wbM+v{eAqN>wxmWz0!oryFz0tOpdKa^%&AOV^HjS9B^MF+YsY3JWNtdLDW zV?HUb0;i);jGWv4Nrl@z3ocTDfdsRJH=}8B9}bG+vk~CSsIw-TSsF62vX*4%R@KnsvfX7`xualdVisJ}Kse=jJ zuM9zy2iPrn6Q{ROb#f_90}G$C;Omb3&!zJt@v?!4zMi! zd%;Fa;4U5;xXr3ZF1&)`J~vWxk>e61-5QFi$O%Cr^7sb{^#fM1{zPfXJ>vmcHy+_*(Qg@!@*imLm@O$(h2_^3IB> zYD*d=;d_m4vXF7oX~5MF@EazbB4T6ZB~TZFoN&1vEs*Jqo*gL9$qP7$cJ$}AogdbT zx1Ma=fY&L54Uf;jAwan~K~LNAB^HQ1AR3^h!I{GAT=>`7^fUyzOwzjW*}8BlaV+0L zsUW*#zM@ee)YrFBVwx@=$j3vx`rf3+LcG+fEeWOD%eEcKOz-~zg;L>L2(T+DkFw}DrRNlIltSfyIh~3|H-1G~XQed6*)Sbu z70aojw{=b*Qjl0n8A0{x9Zptx&5$59AV|7%VB}YAIp_2{*J9S1k8G|+bj&ZB&v!`Y z^JM+jCqT?kt+m_bg9Ady-+Hn0qL})X?=A-iRm>PEP->!GiFyQzdOE z$y^(}%x#)Z$pS3x6)EDc=D!J9CFE5Y`fSBEsmiEQ{B?0M{)5cW0xKFX1 z_=%HqT*v=aIqtxawX%~W62G+KnQo|^HAZ8j_uV%Ovs`C}Re9Jy&EupS;6jW|O)2AY zb8|ldV`%zp|92_$uHu8R?nEIw(Z7kGR#!5*1G6ZZx{R8)7_KCx|=gR1Na8ftZTe@noh+yR_WUl-g2&$7OqUJynEw#2Y!m`-x2+URJ}ewXyUD{t9|cWTsG zveHk^5?zJ8(F|@ghfUazX+ZEak&U%;B8+j|*0sew_JV(ZumRwHP@rhX+fxO(m%{gw zju3@~+wL5+gctmwz&Eb>`lhe0;19#ed%@4=V_n?b>6?3EfF~#2EVX4hMC{hCa8u#wo>GSU< z?5M8(fZ6LyIkzw?3BSq7#0#&E$yTU$QDsIqc!5Q=i0_la-+VTxJe;ULTxo+~;^#LT zJ-zmw<+AIopGlgjlDNf>;Xpw;!B%>dp4F&J08W zlg9Nm+!LqY-lh0IbsZqF4DqLpD=swa@v&jC{lH$(t~O_gsu(*ZdAT{9!aa9=7?5VB zXO}wPRM4a4Piy3-EN9AE!0uk zZqwa9wdU1f3Lr{x61r<5AlNg3Z&8Y#ZzgAU2HW^X4b^g zFguR4r&U(7N$x=_n!Zg+-BkK6E<<%ewm>tJ!HLbv#pC{YT^G~>MIclF;X7x&PsC{G z_^iCUyi%##Sp%{R_^FAp@#;|AUr*#o{P0P%^r}w7F11+XL|9tUR%}9IyzT7DGZdNw zcL$j6&cCl-Dt9@RSq=|V{aP6ALhc#R4(UWhN9zhe6M*##nrJLblKu6#;6Zh`$<2ON zdL8mUN)u15tdfAe(HjMhG63Pw)mjU%{Ew=tS-M-Rp00(|l*3nHhet>8*7ba<(=}Fk z8M@J$tT{DBAqn~UHZ?=XjEg3zAtoU=$;s+Meap$}tiZr>_t~idnYJ_X7T6fhfckCs z`}fnGR?8)a_7?8p+Dz9r>LwBbz6+@0!&7S&HMPfH_ycy0S6C&$7O{vFA5P{iGsU91 zf8SkDH#xw_fAXZ}IzH;v`D(I62=tQaEfq&3#-u>?hz11kamX(YN@^r#c+J!)iG@9S z>It5JxQ42l5<6$^IHJCNx2WKUL(CNV+iBwJaLQPzkFE(dHMOXyBgd#SBM~_E)>=a_ z=N9m(r9|>WcYaz}Nz+GPua3A_X-JVa7ZtoU4SIJ3SdsDC8o+|}gMp<8)Z0I`27+iA zXT>ukAc&q$Bd1Pw(zCMCv-78losX~zh}i7P1f*f$tY44oNGBB+m(6yM2(>_DfQcft z_3JA~^(@uRuO8D!6g6qB)rp9iczmqm6Ckq6z2I;H2SW|VI(_c_r+aHEiWuw)Tj>_h z@;Uy>$}riwsGxZiL$k5~?e0(PUOL&BG`Y+i0j@rnoE(`KylHkg|Htg+)eR60&dkh2 zRCr$yOF_8RNR5q+&jUavg=BrJvy1w>IE0Nuor{Yrt_GZT43Lmk{stREAOh%)kWp`9 zCN;(-ARwx3qouWfG{&7L2?2*bDNnw^;dgMsiM90OMgeSatN=}z1~}xSITlq_=~1}- zw=YB`ADg{+ICI5Xvbp$DocLV+1jLhfEghUIcft)9&r?lC&%rs>fSy(L*9^3@$U38G zkY#9eziLh*JHA}qc}PjjHBda3lvORv#e6;B9_ugUgBv~qTuzjn#4ue6?e9{%Mo8&A z@=dytsuCbm>l>SI)gkTzDf6STK+y#PJU}IVia(0Xl?WY)FgJ#N}-d;Rhp45KK^F(VVt2dN8`3b)BqRT5Q z0kHm+2)a1CcFA7)4;wO4i20mtODn5KCZqo&2(OxGY4PC_xOOVNonfvz5Fp!Ipn_5+ z0GTq~VOP*gN$xA%QOIadsa77Zv@O&_Z1v~JXgd}BF|-s_{G{xEkD@Q|Kx=nS-U4BX zMTm`_*96o;p|HC#OmV+v-b`jATGC-Ju13Z>YHOu5Q>Z5xodwtdm|4QdqYWj^&dO5k zg6DQ%8AEsTE2RV#IM~Z!5^HC>42`|kFiu}^KtnbNIC=I50m*!o^o@*J0ymXr9ZCAi ztDdio(XDJoEGFyb_|8|(JBp?jKVA2PbF$2$O7+4M0w3d2qh$!C2Qj>qb zV8-YfLYZe{Cq{_%(g&=ntum~uL6@{c7%s&FI>COj|y@nbR!d&+Keq{{In$?58j z|M!km(ProQs{;V*`ZavQ!HSQJP=*^VTD??fmFE4w3AV5EvI%LFmVg5Y%Vi2%3;)|3 zBSiBI%L^aH%-QH8SP|a-K*ym8I9V7zI>42yO^%>;1^7n7G4acy4I>vCxJ|A9Y}w6VmmRXzUa&!^(Dw>VvN5sh91lzn(s zJ~XKkA!fzg9bEzLR+N%X`bUzP_7sN;@U*z1DOU&Xo=b%twHNRhUnwr7%vmy z6ir}AU=w(xmDQBAybulG^bJjRIIZ8~Fn?bH2-*6EC98hJH9(hbS(29Lu^!KW2`38y z*{?@59z?{%|9eAc`6se!zWoLZO>G7$z%Kx{vHkH0@Yme;MqmdR<^ppggr2A<@3Pf; zIna=)ZvNMWyLyd!wboR)lP(p8G`5m4QC5}Kqj2ERoDXLd}TlxW&uJh;A$E zc9Iwbk3Ssu=i{=8PD8ngIbM}N%=5e@lr1hIPI;)<*f^L3_8Sc0RG$xuW{_`KcR}OH zr6JAV^~Qvw=Zfst%jMqsE@p?VK-pO5dby*eBQQ{6)9H4;ZG=%U_R9ptY1ePZE=Fg) z8+-#LxEEaQs*b0BZnF`5W6`dGlAaMMFG^=&I-68P4XJ7e!a`#RBntHU~CS^~PIgI7Ei!UMH&Cr#s&#Q|$u|Nk5@;mV$Swbkz8UlMr;o_riL?2DKY-Y9XXvXx$bX zq3&aJv`CQ^*{QF67tb!=jKgv;^^CWSX89t_07-;2Q}bqo*==7W?huW`eX>_f(k7!( zYBYZpO(inFG59I0rok6FCeE6Zcp)C=#)oo=8kF)gPz-qCEYH}ExP9FfIpVZbtqb>g zTcdmR1vh~{4({9i#jQT*ERdyZx-eOq6_;T0 z!?~DH^_=XTE6wdL-EigK31*1<=z;~UkB*Y`3~@=(DC1;3(W|`fJ%N{w%~T#lQqljt zm`z$d+4wT7f737=?(7dYW#YM8w`M)#U4qxg&>KZg=W!NkimR`+nZHy8MegQvS{UqZGUg?`=}_cTw`|gUiyW;t4DOtH^D1-o3Fe^1=o~ANeO;s zNv|;{2+;46=Kh`7%#EgHU70Jj91=Iue@0no*7x|)Bf(>|PAz_Sw)7w%dgTv5K4E;lX(IXWM%OIm8K#~H z=ZcA|7Mk+4IaPuCKUNjV&cTY@=GN{pe{<@P_jUr8oJ&GVX#m|xbHXNfl9zug7_7vB z3G7{&3RN@M4&ep#^dm5rigmah&8pv?T%jv14tD6*;*Z4@Y<`=P+1(vsgi;H!e`Pk#Y27{o^T0v)`XIv{8g#;c@Yj!L$CJ!IfJte!?q5)%qel^T5Ko zb!t9oQ@)z>`_gE>d18fUL~K*MpT#p8%KA679Q))+zbp0MIV8YU#Cm;6@l!S9u*+s6 zbi@*!GfRlt(hMu7va`Q_D2BJKb|Tjs^X>b{Y%Vi`6%oD(PiFBbw@!Qa`#Xs(Gop%K z$zYDa!=EL^*MAU|It#7+C|LY*P1<+=ffPw zhIv#SMS<(peRqEe8Q6x6v#2zo^ect(kmem@@~;nYM`;crqIgAv88y{YWHGdHBq`3x zl~rT5l?}G2iw2u}E`s`lp-E@ZW*SBNi!GNe$P$FYLRVsAJK|ZB-s&?VN}*>8PP9gl z9I-O;rF}>UvNLQO>HEI2tVnpTb?mEn-G}uV7nOs%;6(zbusXZ(qV@Czp{>?|*o8Pv zFiP&(7B>ao`jP3>_bBDFUD1-GXu;H|+6i&V(@0^UDm)^UA*gBx5#S^ZuGEPg({y=&;PX;HUpI|>`>d7fS!(id zT+};$s0-#jlrk9@(t_7H^VDM#I}L;iZy8Zn;0b4UVVRk{a95ekM!tA)ERh< z+YjTb9CN_c@O7zlJo`4%?7E~*-~CAKcUV{qMXfh|a>va!e%gl}@a;D&WE4eCSP7|& zH-CK92?P16$?CQv(*5~yDBEiIPc(0~z_@?AWE2P=(NS<2NJr%gxXvyt+bY7WFBN&4 z+jaTl%!qrNukUE(%2a;6+cyjTzY&ephXtT}lm zih9(vf)*k%wYDOpHzAH_gQ{Be$ftJgU~|UQQf?IF=&rJQ@^Q-vO@2!Tp^&B-Aa!U< zOXq5DSP%}}r~J8P^u3yp<;>&*7O);;=xQ*R0`n+gM%>@Yv!&ucz0Z z+$eJVMoV?(xG%`%thd)QbiwQ!+L1F6f!pZ?v*y5)V}A50;U_gWnJRKQPQ=th$aTK% z3WRE59=9_=rIMBcS-F`1Mt=@Aqj7nl^Q4+UI^^v$532M4+v1Y5+I~N>Xk77r$4Z}N z81ElTo$ZGYZ5TXK=d|q9w`VJ`!^=}qp)zhs)OqXrQO`n45xAlRP!%AHqG9~Jrp9GB z0amKN)m8oa6Ck`t)zsD2+}s||T93+IVM|hAuW2);X`rk+uWl=vh#qtSu~(6399l{(jfSTF!SGx9E|~) zQ&5Xp6lrOG_uA~Ue3e_D?1|dUt7omj?Tte^+=T9&L$PeQOjcA|E>s%a2y8TxpG?pM z#ldo-w(0bND1;x*i8M8;I+&zsD*1~+_KB!30My5DS>jl)f_r{G&~7(1HpW{OyRW$~ zz+ZSZG6yDXD`ms}#c!5w#q308w{^6{^XYwkt4{P*dkp0=RSLe=#u+fr0LD+Izn9wX znU5{M#LH`9ug(_SgL94>kkoE6w{Mjx$rWEobfIru;u<2K=$NC(#A*^qIWnsL)u zV)w(N5~a)j4^Kz^=cQ?q!9n#5Zd9&6ZqyOXjcPYs6jG-Nqs9&G3Cp2MCvX=>GJT*a z52V~Px%kHoTPIG00gf@PM5&dUXCx<^X_avnb@!lywJ$APIoKYDNDBn1ii_F7**aQc zQLD2@InQKCe%juC584ybSmo%OL$XRr#zU0{FlX~^-Z@D*I&wv>RjeJnSC@S;+F`QZ z?hDRNKxonh+~WNxgDPZ4jDDY6`%x?_C&$UfCXjEKQdlTI6+0C@?1;<>Ew|~+6=p~5 zzQa`ZB@Q(pr2F3Zk)TwP^n2Xod}QFblm=QJpZ`K)Du1qB`4^3(G*=2aT}HnRQJ zfC*Aafi5RKn)}346o5X{BEo0TsO&FZtf1o#)@n*9DMeJ;NrEFfJ@+&5HpgyTnJki~ zd~#Dmx-5xvDm(w}Hx3 zVr*dNX2Q-8`Jtz9~SK=zRlk=-v$I+5h{R@(iyiJa4u~6 z&?oU>No*dpaZxVojz7BJQ(5%MRF24agk4KjOl)%^6#BIrfg~^mCEcE@N_xZ*31^4P zQs`O_RB1G`AusYC3~#K2?yZb>pjwKQNbCcN4Te&!-1^iw;GPp#%#$3IQHZnFIwk^#b1{`IxdZ~H6dq(c(AJwXQlXP9Rt~fdBP2u_!d2cx)Bn1$3b7eT@bmul zG(}dwY@%w@l9G}TvYD7m3f2~WX$we$y2{;vD+n?vsX+JBZh(w2N#Rh+tEkW=w`(gr zr5SbP6c(5czwblM=h2vNvmsZP#=?>)bZ?g=EWJkUD-i`$QhB}Sl7A?1=RNKXsxvQZps)P!8DI7W>k(G5Wh@Q3o9e z4K(McvTVVXbsp`eSqKIn$X!9ZTWU30yBc>J!tDSHfN{x}qevEptRQo^P@KnIiY>nV ze(u}>unXYH1a5Zx8UMnUB%d{vBsN!w@Rm+oW?vSuP=h(wXW!xZ#pzevlpJF4GLnd} zftOM0`r$THe{SO1ICgDp=~|gl@59Bi^M8E^W3{CI!%7=`SxYWF;-GBAy0B2V(abY0 z?jD}iPQ_f$pN^ZbUFSh&-zU=r=~bX7QS3(k+mTxZOdugD^52C}kGg=?)>doox(+}P zNFL_Ua}ZLB9|;t^YFVYBNxSgr2&0z5bY)eY#tKGFARhOyZj}UN5k-0H=ZV*CVOkM4 zaJNFZ>8EHhlvGcG4Cy-dn+8VY0tQT@MSP)b-$L${tr^T&%`oHPEYodQYxr{AyUXL* zG;{HLvSe3QM&^x)vy}MN9T9)hS&Pzf=(Cfr`pRR=dES2aN!MKqlN30gr^T5Eg9~?M ziv*~KVs}{U72dr>QHkw7)E_y3Sf3)kP@sh0E^3A*Y6W~Hdn++an%pT?Z=H#WO`vgu zkH40UouBO?T5WCOnjuX5Nb%>LP{hmu+#+U^Hp4ny-zkS~+$hoc_1->W7#B#!m;0ya zS?6E(A0F3v_kzfO^S6F_A-A_YNRf=5kU2|G<97VbLLDY_McV!X*W|>-)nJ@QnQ4lg zRJbK>9&yL%fI&E%rd7Y-E4U-6DZ#!-GpjR_Mahvs6FEu`2->#t!*rc922!MLO!SVM zXf3_q3ex_W!5R?zfRF}nHuJIc1V<6Ed>tpJxU{a{4_F{|qFr~9Hu-SA#MAnLin!j4 zU%5``NKaV}brG?N(`|e+q+Q0_XNOciQgW+Z62h4273K0|8X63F5M;~#kuR~S*|IJw z=zBYz(yli3yIBi0f|$-X!SPcCdELM3ugtrw!H@8joY=MCohp7?c&>lv6JV3Zg&4W= zR0Z=^HVD2QFl6drVG;h^xFT$x{Ua-#9^99j@E8SKI2gdb_zzu;vDuVsFE4obg=dccy z_YIfhia6rIj}?Ixo&!ImTpF2w!}`d7RIgwo@iF&vrNPKi#EJvFnyu@$CW((B)BvO2 zyU{lPrSdjqc%LNVo8>@rrlVzrVx zm3IBQaP7rX{;L-`n{57c4|#-NC&XR-asSn9e@+!Hhk5TuV1Q6B9n(V@_<3ifpT2jI zKBiG-hB~R?=^@$*FfI!$l?T|>%f?)hiRETJl87c{#hF`ktRCs6Qd$6RkFP^F#+t@l&AC-A0%nrp2rZ%x}s>eSQbGiV0ops2y`L-7V8+oJiXrw{xP zjYiP{0qq5EM_u;vl@*kf6qHJq``3StynQ^GK3weA1>nq-hSJE6*b5VnX}x&*niBa6 z@&H2svtmqQ!y;V>VAmgOGq?XD>`U(i*!-nJ8pN$rpf-J5^Ky>laPPR1!> z>L2$}n_hdQWMP_B=$H=G-0^e3FDTK8!ZXRak{7NJundpa*zHB*y=AE03b2NkMgN^p z#~Fz|Hs1mBmR`%7SPw-)T3Q&J43CdzW)(ij(xk{)F8a?E2FrPx96&wAd@<1d z|8I4{|M#~wWY!5?6Zn@Ym~i|Wm}}h&bVdb1mjrtNA%FwVR9?oKI+d-!P6iwtfJl}h zvOitsG2w>!=We?@!A*=C23QY0hifK#FizvV7=4+lX@#q#e6ir<>5+PS>Uwx&*O|;= zo^!}$F`na6C+C7BZ@8LC54md+_Od0um%1WA#Jp3#&Y;l`Z;EIFmE(C%2>+9?oBV)G z{?DHT{zv=ZI)<0|YQNj7nZa>0XPiV-SLGM3jr|SY+dSqLIwhc`CdsH47jcy3`}qYi zxuAc;I^mup^C>kSkW^Bd6MeZG+-#Ioc9c*{O}Qc+3FfQ0q1PAnyu8jHNRkO9A7x%^ zJgNihFH8I~*2z8aZjcMQjkquNPxbUEDD(mSA0O#`^)%{@rFTG#73z&8*eHQ)!B5$` z72dZfwgVBAMrMUWO^SMAMtdPLzO_as`e;@sNzZ;}ue#4w;`EsiTH@ef+CMe~MN!}| zu=aGbK-NoPff<*SZ6|W!ZTWs9#B&sFvlnVa0f%tcB?}AdL>GPlc)vRs4f*-N{)$UA zd$UWIDGQg(R42?KT*v`D3ebzYEXfW8Ua8lagaGjeDg;q!jdak|tt!SMR_tLw4jhp> z_nn_r3n*!_kxwa_d&f=hXVT&sEA37leiGp4X`&wF8uzyieg06r$~-26Pet`knz;r%Sk4o3)V z$}t~qf{}m4NM+4@bguP)O5cnS>&s-0-}UAEbu;iSk!(|4t{T4TcH9oN3vPP@!2*JEh@5VFuhI7i<{CJM5Tq2Y1K+EPQwVhvj<-t6XMubosEsX(%kt7z5OMAvn0ED5}$LfdZQ z!eIQ=WOo2Kih+lDm}1~P2&BUio=Jca}8dNc`gGn%-!ib5DC|h1h zj>pZm42n`uHn2@-);d3tQ85I`;jyJsX@i&WsuMt8?~RL4BA>U2T!j2e%mYN@v3Kx> zi%@4*SDyT*R&N1e?9FO#jPh7+&LxL)SI7Ev%A*L zW7zXy3ODh9opV^@j(TCW)DdZ9%=-u81l=L)-Au_>j8#~_2 zv@C_OG-ycgX%)0IKRhj-N%(m(18xcqFfUBB`@)FRV<}=YAI}$f#gXfjJucbrk}pA) zy*4gR12&mNS?WNua;^qwje#T|Ow+R>4pp(Ru2F+M(+3_m?mNi0;B7m4xZBbfrMP%r z4qRwiL2JO@cxfW9@boF|Ptjcn-rZA!ZSrO3OHtD*(+Bo|06*1g>z#%7mLPL?#d7$zU?D?FFY=E8MQkH3>M(kZ@xL&1(F3gR_5ksJ3-k7vkHQvroXrt?0u`nLWKr> z3DTrL7#J9?0q~cWElw1-EcSs;>W2@@U>YYWT_Z@L0sC9B1T&W-{{(cn22vy{QZjUc z(}v(Vhbnx_Usvczf=T=kB1*o`)Fl+?HLEfQxh@$_=m)#0{4I|nJpmaf!k7pb zq>=HS9A2(bx`<1nJHDg4`}|gwzwl~7s6RutcmxC7ax*^(2=K#*`%^%nW!NnOM?pTt zrz+W#8mZkOXA$sKgQ*=1(l>7{UpC+~sxyY*7Zj}ERh5@l1PM~J%$fqP=8CI&dHZ{o zN$3GeRz{ylNIa2JDgiGpi82d*iOxOK**|CxC1#`YA|fNz7HiNWT*JuF!Z}x`PW#}w z6O(qQxMaD`=-??K;oM%<4bU9}6YGz?RK&%96t|=-bjCr|3`mRWgxt=?0$`kuo=A$% z3!SSUT`)EMK0vvKg*{VYw75qqWop`?Uj|b&^l160W<`lXVTN@VW}4xa2Hfn?_y9hCAGcV z$gQsS_6J;S;|0c^zE(KUJlN;Nf`KQ}hkru|FflAgc;ISMwj3My{OOx#7~21Gmd|3c zzo?1H58y;7Du#PbWWMvpaRDOp$Uo0RXlN)U9_$pt!z(H;PEMw5>jFbhVtB641J;F= zk0?l|L9FmwPE;)PYxC7@Pr#qtyqKMBc2fOeHz44vJ{U!}H!cv8v=RSVBR(*@Ws1Eo z1aZ>Q8q0Q2Fk{?Tg|Y|;PWUCa zFc&qVrV5;Fr6vuzat*5=*mEt_vb?HYmv&Ho)b+{Ko)w#I)34t3Lb6IYHI@GMM{&Qx z(9kJ!Bn$(Xh`BC+9c;GE0d_ddDH@Ob)5?1`k#{itO~sp9Enp5f3Ws+~&SR9`wk+fy zO7Ev~U7p*W88#}=*L)8omfs&2cO1^AlMfb4t$G$t##oY0$tpAn;fiZ`Ha2z^7R=~i zq9lHDU2H5_W7y?HLmAr{3Em^^>Pz5oWh(Nph?=y#_9t<(>&5|FQHHP8t=Puer(jtv z{GD-$_4E^zNe?)o!fk~7Qxr?WH!u$(1zdN6&JZzIf|-4NtU5IDjcU1%J5kIvM09va zjF>R+8_rG+4D>J`g~($f1%J&k&>xLVR%U<$CW3J)L-YXSa?AAmyejTjS{fo8h*23f{Z5WUQKH8;0QHEor8k|U!kxAl5a;A_+aMUPJ1~g{^7NkNa$}< zr(x`8dXR98j(^v(U+$iVw{82SryjSMD>QI}eKNJX%$C013MA?8>y{0Rh>#MOHZ~Oa z*!+82?fuv39KSzQZEk6W>y2!r?92(GCz2;?J&kHWWVn8$^NfV8{zOe7VuaXrEfQ%LJ0`Z8Jwc0iP$`H}v|l zRtc}Vxt~Zd_xKH9l>)pUpvl<3d-5BL=XQR!-1~q4N#M7oc;Q5I*xdu`?Hd$9(v?+k2a1SE z#SQ{P9wxakKtTM24IePa1Sz3J>h!Yk$S#9+hGqyLp;qTePTC-w@Np^_FjA{^*mloK zpCX|R$&I;-=Y-zBD5!&J1Hg|3I*l4f*5x_N=XK0@dmDXjpddlaUDYb9E4Z>GvA^3gV36{H$f7PgXf^E)QrUJPQ2yH=pr-#)F{xgJ1$%NeG z(vQ6MR<<3h_ta6}=%@iiP&K*9yWpxskK0D3n zN=5@PeB|{;K8pk{dx7)IKhpdN?^b+NlhgL@xA`@+uhITgtp{qaU`a`k)9~wGPz#vz zJ>nZpUuL{ZhScESNZ2%El@$ixTz4u3HOqgvE0MP(WJrg)&}c%gpzf?@I?^58Pb$y4a0*b zKCZ^dZhD0QE}(jr^&M?3`qM9VGhQWYEhN#2x^7>OV{I-}xkY0u z@+(eGTLPSsbhfS+FX>`)5Ricj zkS_!mRt&Z;UJvw2Z>ZAd7i?JK1%^|5>Ks1v1M2ce+#Jle?*~7=D?i6?wlBWs#3?{q zzfNh7IfwHt?2mmxBLphY>d3g-{jK4<3j)|d7tN{gJkK6%o-%LiSi=h!CYWYmMt#@u z8`i$M*xrfbFMZ?o)ifza|F1%GgV{Q^11&wJu@MQmL7~aIun5n=Qd7X7@#D6sj8ngn zu0}(Tjym9chFQ+yZn^^ylLQO^@AHz#xHcZ#CB3IU{)&ggxm**TNYHq|fDSUNv7E;J zB@#{Ngt%Rw33&^U#Ik4RrKf*P%MySlL1gFiCBliKsy&D{nJ988%D&H2Z|Vnh5{7MJ zai)zpcy*H39hv$$kOXYSS(%y91Dr8}#=9jFB8^y|uDT;d#1yJHnKYPhFB$a7K|Kj@ z{_@W~01}9k;kz(i_8m^jCwtHTT1|#jA>2-Rv&nyLeElT7k!@~LZPlR*xL3<9wG-Ew z6uLMcqr=%A8rOVlGEUb|NVu11?H%x{$n;dNKLO^}6U`imDS$cJS+nlWbx(ZEr(up4 z=&$k)grkZnE}tAHjOVb#A2Y4S-8Wclp{j~Tz-CZ3u-o$3ezA*E@Ocbx;7G+hSpWJ= z1LPz1tP>9v;iLz8xjx|Yg`_3GUKR5pBMd2Z!-0%(Faj`CGPz@2v?p>V!}|E(i{CWp zsD!UoCk(#NvSOsUw`Z#^3jkW+*0YT*3^MwQ!_QZESzjMpQ4eb_I)SQ-rSh+>v#)~x zwM{nn|HJ~?L-d&*0H1+R9b_BD9vlkhC{r+7=4>?kSzN#f!k}ws%3~Jf-hnNh6##Y+ zu$aKP988_8R$_8x)sWAk{{;^ZPf{H8uDL@NRgmG{Q-5xur3KYK6hA9Ku!WbTl_yxe z{sR^8Nr5UH;H+*xPk$b+xoik|35zzhD1~IHH@)nBt<`XTFCv2Q ze)wQvls@@6;#>P&y}&DbB02q^7xKYhlGF&u^@#+P=sK=H?qW47a`!n0bxkRye(_Uh z%$fkLTBqIp4A8>JLwbaF=QGm${H(1Gple6n;unMs3J^$wX~2yV-bm4_=xJQYBG1a; z2m^)Bre&`(rGYcHb4{X?2MnK}L5im-Vckja0GPmz$|3*NsWgmYI=YaTr&VVIEy(?e znOEFJH*#;Z-`c0;-jvG!g+=}r;l5f-Q1W$NG6SIyDDzudZ=lCnK{Wbd#{%)~8Z+}$I9Zy62MjZTgdh?dSO9QPsJIf(KQE-iD&?(FghYQTHM#d(t2RAA$ zijJ#Li9iCdUtY9e%{;lepI_dl3j?R;Wx8)d zCa{yu`Cgc{#8|or?BY6(l&K6syYHhy8cXGUyortVdiyP`U}i|jo3x=MX4CDyRy83h z4(5Xg57^z-F9H4kXv6crCItV$za->@eBj*>!>bb>_ocDgItQ<{voh=xAYx7=^z}c% zXNLfzrZpU`*;_)_j<-2r`2~ip2~M^9cj2k3UShlMvsEPIen9JK4;4w9iX{lAsEu){ zO9RKgzzPsF9(#qkU1%17`aD*+GnGDVKajcPN9t3^WBvT9I}pKDZyBbLg4@;B zn~SPtD-H-+n%LL@tx`v2AkmyH0;k+xs+uuZE=O8GwC;VP`{5YjQJrY;P1wy>@(&$T zn^~RR1|S%ZbBG@}e+Nz8AvZotp(^slAqe%iWq5MH(GC$0NpVNv{sIMQppeZFATl#E zi}Q{6ZI*{YAn_X_fyENBVD4R<>0+Wcnkeq^;0cT6w74TLvKf1VJwx7u<oRa$`hpAqN8lc>(iX(qZC?R`3RhmjlF2v) zbO5_fM6?aSDt+wX3csR9I3WHJ9R;?>$EdX{APb5ubPX|NTXr$SBZtd!lHe#|>qto! zT+w!E_J3d&1*mK$LhUMeDr^iT9bc{JZu+AUxMRvkY_X2jK$7(-`1m!rc`-V=99+M7 z#~WkTe0}wWN|5F_zTpyb_{OSb4)_fuy!pYX_%^UwqH1lN0RGx>k9dm>9GO^isS4;p zVcY5CShz!=-vVYQD6qMsW>5`1#Key!OaDaj?Pg_*Enb?aw5iLmiG+(?@nr!hJPXW~ z!0FvFw2pKLltn-#tzGNf@KD=we+uETgl%sb2c;2k07)yanv7M(j&I(Dtv=lo9lA34 zm<2u378V8ugIPlxkvSvCuGWrk-;yD@IuU#XNYdw}<`i0*U+C%SpI14_^nKrhJb>bcuaJK zM&u6QB7RL3|&SRU&+C&n*&A$XYfZKv#*PIGTB+1)+_hwg6S?dnJPgWO! z{@3qZ_RFYAnW^5i`(-_Fu=L53S}(-G4YjhMyJ2r2y>q!-m>rxQAf^i|g15XfdJkJFCGn2uy0dhwyN7+8snRu;Y5H5z>tU_b`%{YlC(g3zXS|&~3x}|y) z4x>0+@+Z)ZwGldZOSBNP-`6fQGAN?O$ucp91f+fK&vn;tGvITNG*{oYq`htnyENf7 zoUduAai4{1p>3U95MgD<%dnMjZ~IR-KtxoO*5SJz5yaAc&H)Pj2yuOsPoN#P*6_@H=OU9|=X zsN*mmHCD)j-}3T_eeR#!md7mD^ha<3Nx$Q^Z3dJpK(cZeYfKg^<2W5m(vpi~uG~NN z1l5xQa-q@+jSzkr4iyX}vOojl&>b_Ql$Te2S;*RF+QnV5yR0K%c<4y=Z=CtIk9EZuqeU6$NZ5N( zM`xPwf$Of3>;w-k<|aiYgz&uF#f-|n zCh9W_hhzVxz4E81t+DaaF8aq!5ANGR=^XG~@DIPKsEEe;JSZxRiCRVLKsIgJ!~_IE z^RZGEJ~~(ppIaf40_cy;Jexm`r6?#(FoI)CJ3-lc!yFfbv+V6VaEM{X7*L^f7_QJK zB=~KRlVhUDT*^E53`EiE5I8x8THdEtT)17pX##45u`ywdUqGYjyY2TeF&s{Vp&!eD zX83%x<8w-P6|+SdKE6A0W!$`TcS}+n{N7hLCVv!+-XSVe-!YJ8sQw=N>=3zZf08^} z*1h@FavX)gPkfIldf@3=sQ4RzS;8a2)Y%;}U-~m0H0!Ya@nBX;ZYl$WH5nP6QeDDG zuO}NZS9)4f5-lTRYHV=#$>uaRusDPm#}?6t{YeRj58 zyB9(X!Sr{^vl191r%C3i;6=?;#G>v%t-q2BSuoKS)en}bIf|yDFoUC+<<(~X@0vdY zvEP3NlOfcanIy|QLsU8X8gG4;2XdGkN#*6_lE7GGux5my{AcXkX}$taJ{_<13GDQn zjgjz}bpKJIuLMb1Pd2FIp9=3SpCD~sSAH;o^>+ZJgsU%{^_qU?fQU4ae&pS+)~))e z?j{yKzO+Ckp`=|%ys=bhZGbNkl7%8nMg3H1`Cl8z0Xd^Y^Fa|Qa4*hhUXpwg%ukV# z=WPyl0kB2M2C+(tf;S=HZ-}SEiNr~%=kPf_Kb@#6EOd9hQ#Hn%9e8jPzlQ`Xud%d?z9TV64keCB1 z1}~rMgx6b#&jSI-Cq2lRDQP!YBF%BcBhU{}$ zNOImifPR^nsDalG14|k)h1k^GkmGA>>-}M#1x7O1NdTP};6i0$YG480HK?eedD`BA zoWkF+P_;DK6cEzCv@!*{pm4RoJF1|NLluQu+8F{=J!+$`rrd08w1hHwR123hC=T>@ zSdexM%Gau=DL6YIBSFlO&KO?z$c6;=Je0&DuQOvpxpuK@iFK|75DyQFMGLU&j;=pR z>(Wk2PUy7mnd1#^Dq1mRv^5m;p`icbUxy{QW4U!?EX)o#E--CSvUo@4ldF>Pn0X07 z^fODGox|Zz-aXW$07=`8jxing)K~L=GBU1AIynF}XjZ76X4cebvYz1V&PKk_SC*!Z zgXk#^8A{^*x`onl@2ljXR5J1bT%{fZNxbM9d6K|5k3JyH5evKlgNErVCL2C6`^2Y(|IZRi}>M#(HP3``$7vBiOq&gr#9MvWaCbr)3f=L%Jk)RF-gUUCJ_t*QuBZ?EHkm5Z$ z@E|uAM6HaZiE^D920HI^+Gw4O1FDVg(&U5& zJb*z`L!*{*)3TW8=v0&>iejaj{0(vKN=_`jX8(~(Hp&>dKq&I`>HNpz&j9Y>C^JgU z%%r5FOH0cd&+^`z+wlf}<&BK=rLd-KFtWo>iOJ|>D)#jEFTNNt0f6f$=RG1do%Pa% zhW_GR(BPtHLifr2|7do$slvikv@Z98L^TO9N!Wo<1Rl-(deVQe0_oLgDk(3)9@!u| zmTNt~0>l_CNA{iu9+)gJUm2Og{)ERBApD4TP)&q&|N7k%!RUWRdgVPcDt`Uu9i?*k zW`BIidqUIw$JhVPa;#qz&DV;sr|j1s{?mf7Jrs7bA#*v9E#vuJGLlIiYcK-|GgEys zWKKl{CIqmCrszgCLmk7fKiQftF2V&)NFPAhK$v;2+==TRf2fbiKqhN`;uHkPk#}AqW94p>OZ{)2;SwtBsPrsKygVPUavfuB~@-Q>2nG(YXDMe=*B$jH4}!8F`5y2<_r0;h{Sa!vlWZ|~8=9CHfk5TF zy-K4xqRS!;3&FIi*7Y^|(xY+$q2n&hqSXEbyBOV^?TvfO5ja#00qyN8W$SA{78cR7 zF>NL4VBIn-q@h0H2X=rDtfb*}I=a9l6$lxGA@fpq*f|Dji!|wYN9}ir(q*d`Jfz=h zjPd6~k&*J|;|l4+@ESrWfg}T?1;!%4&YKXFiOL(XUsz8O6mWB~UDFusI3a=c+V&=s z-+>(At^;3H)!l9w!j=fSuWEc+Wt-@_9*AND?ejDzBWJ_x@ke z&8B8s{x8U8%HKIR|0J6QiuwNSx-UGR8*m1u!@lFtQ8GuVTX({VVB*6KBDPS;`H(yO zCLLy25{9_j+6;`0aDy<>aw1TOCXndg0SFi?{o(%uQk!1s{{g8j`pr^q6DoIG;m=M$ zy!tpIO@2rzK%%$1uP1@q!tt{Ich9VHJ&ljG$q}1|njL@b23v+3JrxsR(&FeC(-9uEANm+gEwS7ZFjCyu| zmgnkI`Q6BxXSP0Yn6?Wi1^njbGR$`y!|jC;{wA~VwTPOAi_B4c?#Nka^XKGhE4|d5 zt^+{gXUOu;OMvTy%m^nfryxr(ENa5+sY*%KJIB_pT5K?x6!vICFf}z5iVH6h;e?8* z2af&4(2Q*x;qH9v2jgG8Fj=s}Os5;Pq=-7i@3@X++%NWDf_V=xT)-HFsXYjCqSDbe zr_*|@40C^E$)O~IEfjzP6#!5YYzhRS#80jtIG1qq;vQ@Zz~}gbb@^r|YV6%MB;if= zzH36$62lKYTD9$-^K>$GRUlS!X67d`VW`H>5(ptq9t(p~1iV@IPp&k{%*v$0dHA-S z2PUxVcXq3R@H(Q3EBokwOUz@?v+Gph_j&e8i`nK&f1V+2bVv8c51&6j1^J{1AJ+$D zPD9W#ByO~K%SJsfhr^EVS`L+FALv0#yLHnXGn`**r~H$n$`6k>O|C=ngXuvtbzm9^d zb^bMivYsCZ$gq_X*2Tuaf|3o*Aw$6-73LV5waO@2Ie>1tEu8Vm{C`ah^x!)5qhJ>7 zL5FBVaJsFOY2UYNn1K~e>Vac~(q|F8-cA((cOgbkOi;E@9n8gh342|H1(xK%)p^}7EV%eA?` zaP^(txm`TT4%1yIxdF3y_bvl?hH}*_-upCGy?D+525)eNr|AAX+lGPwcO7#ie#rj< zM)T&Lg=$^4TU8|Vgmx>@WWzObno=BqE{n2Eq9nBbOSWV^KX9~QX@^;rD#%Yg54H0$ zBj=%P!QNeBR#bE{qk8=e1dK7Dk;BCaPHoKYKDeG?5hV%nBAfQ@9mFwlXnfcc;pyoR z@CICTMp3|CBo2;_1*h6C#v_c?K@bQG@FIy*dJlKhQWI zyRdB;_JSr85O5D6uHpLAJD5V_&8=!(Si}+!c;?~UvZ-&c=Dm3g=9K1xnts>%4Uctb z)W*6|du8u5Zbu)5OJ0|)S@1FLGZ|)27c49ZKabOnT2ni1-Vv95Jp>F}FbdsChB1Kl zUbzApj8tELO?BY%hoN+Ad%yN@_#NBMuJJ!+%=UVEJL;r^3fkTRY)DGnaGQt>srF3;aV(fOEb0iMEF zN!c*RAF}_j3=z0~`-dE6-pA8#gA54}wB~9%!HyjNp)npKOj&K};~J)6>5B&J$V*I! zYNDW|p*gpei57}_A6<-^^3{2J_yC0^0f$}%JT|arc_ZxgI6Al^uFiV0bFaqvIok%= zL{&^;A6Q+%%-Yh@5?{AC@4RL|eXANeC9;1Cm6s}}SXu+XfJ~8+2Ag*XPs|Vq1W!q8 zrk*W5$DB409O_is6lAcn`0@Y=kW>D4u7pE{zZJAkhmwE62R~CF~qN z&CT5}79y1-&psdRckC+VfEhyofY1*)iW8hjPnyR6#LT3CA4UP+N!CBN_ihNqM z>)~;8o%}dGpD#iuV_v4;14G`DmX7Au+(;n-Hvc2HpRBJ; zCv~vB@;5)s4Z5<#)V~0~jwCC2T4t~F`p=Rk8SNJ=!luMr&ihIZ8!TJCuDFm}GZ_fT z-B_4*Vr0GhDJ*F4aKIm!;HjU~@81l_UX43>NHHR&AaH} z(^HwL-d?7Q6jzZ|2X>k#aDO7t@!Y)?qx$as4cMT2{ruZ{-Q!ZEbXRkxvb6t=(Af=w z$V@^dCYkRWc^%E!Cxm_*8+fl-zEUPcL`Ig#$r)%D71yY#KlYIpmFhfxudADEKAH}@ z@nURv6%~~es#d?he2{^Ddg0?!qbp7a=XT5ozg43hjW!}#sRB(6e9)l}FZ3+!k9PgW zr2eeLGzFi8=#+!Px6G5?E++Xyl=xu3EY&;L{ryofn#n!{rWNw>DK^b&eWJN}pSrrd zJT-56-(-o5%fe}_oI_{ThYDTFneUe%uzP{xqVMGMdpFSD^CszC`Wg{eupYkn(DO)N zn*jeJMcWNkoCk}rg;i=z(CzwR>R`L%=L;06K5aEVCCSp*do^yLn{(a!soHdv*je+$ z-S(Q@*Wvh&Q(h{ zy4Zz_Zb#v4tox|>K?|9#xKm!_= zb{Y{LiI6yuMhHE;vuW{4-a2#1kwBm3Tz&rq*Ql^CL;V`w`SGT=(BZJ?Y2lRcIG>_h z^FQX_=b7W95S9#&($fcjJXNAdORG387uoZ*j1xC!{Ks0Nbt&huz73r+Q>0p2x{XzR z8u4=`d~yK~4=*<*IK8NrrR0r;jK61OQnjsicwe|z z4z@GoL|mX4syn)Tyen17tH%3-^zm0p&Afx~sHj+}`_rz2w$;A2Z8sb|$Z|)k zx#q{yJidM%t3HZGCEjK!kCs~4;!bAbyDHB`ceKr6HBd= z4FtJ0AwC<<&d;Iu&uLoiWGXjDhkUJMo0z-U5BbYH`%!G}{#e%<{wLSRz1p$J**T(OW#!fMz0;y>QkF{An^#sU z*v=|bZK^yqK(-$+3L^aYsx4RE5l21OBk8W=qL`KLZU{4E{&f6|DEr~z^I?U z#7q58Rg3o7k=kY61*Nq~YZg_!nf9n^kmB@tJ5%R#+2Hi1)SoAeh+rIA^Fe=*Ra3k1 z3e^*1qjHggBd`4myNKxdg1ma^;~dS3_tze{ZND2{e6Qx-AF_Mft(!Y1-DkgItQr@)xR5rvr-OCLgvIjSn^XTq@&|e{abJ>`+`ipV-=6p`VRFwjY?;?(+~wp zN=l`Oh$NJStb}gqCLK#;?ueQV_Q!O3w)ul&=LsHfaOsUFJQ*!ff}qA!wRB#2^l-#Z z6L;PhJjab5zq(SJTKm1ibE2f3Hk?Ehjq8m(O_!PDM%w9ukVu@H7W|951vQGTa^5@r zQPicXZ0&iKpn~v5@5tk5)HGF+kiX{QLy_C;mAAyeC$V1bMw*BQxF22T(p%p)A-SY; zzF~SXK?n8@uE-6CdA4x|vEFiC?Y7)W-!eH{TebSLVk7@aj*Jz85~6ENnl&XqtjOP8 zgFkgLh5SHwb{Tm;H$6K}(CKTgxv42rb!xHg=B8MM)4_>WREa4%iQ+DgU2FTdf%X0e z*2C|^?5kcCji}YQ?%98j`>~N?f4m%HqLP@*tz}!nx2`D4HP91WTD8V+(QjR~T57JS z!IJP1@l3#atY;}%y@NkYQgmaq;J4!oSa)uA7zGfTNL;1X+R9F`=djN5bcfr*4v$m5+RA@6}$o#oVs8nw(_UJvHo=OMVM)+RNKpt+DAkE9vyAtG|te z<8B2FLbuaZ>}4;ygn+Q{k8kdKZCNJH^A2y`zP*KyuP7f%#N9PN*xPNV{{Azj!+EmV z*%=2IEn$ZziY{Z4vbeI!5-8z-PmzXk|biNrE{HPgvHW%Uo zUO76eyoE?NPF5xnuHWGBPx7WKgNHslvlGp69gfc)^~%f3Z}+XZXjM@v$}8G-nDP$T zE(bm+DI@>&dZs(H@MaVgwFa#Z6PS~BO?W`?4nOFmn5};T)h~?&i!FZt=?m0(+JDG2 zKYaNIlKt{K@=rkg;?-+^24iC1|G#}$ zVuvD%xF|=?G#rr1TI@+GSm_)S8H+?;_|WNo967<567$+m9yNW{?Rfib^A5aKY3TvP z0J*s=4bet^N>3aP9RyyvK{Ah;jxOf+j*wOxcN9|UJbc_Ors*?gG=NM;grCwt<;3l_ zaR~cO=$9YU7HM9g{Zi*42%K%JBO!1nJvG&lc;mP)&Sj0K_VedcYbaG}0QIE%6Rw(VCZ+q2DK4abK zY2TW&zUoxYX}8jqmYVu=wFI}la16uwHqODTll)6VyAm1Cllan4va~8Au^-o& z(!w_`e;p8TgS5k8$p`Ogm6A#86ZqDM@3-!7eOP;`L1f{g1^?<*kMTaO6?COt}roO(e;V<7%J2cLtb5mj&MVAs|Hdf$&(lNyPENSK3Zp{r7(^)l%9QtNcbpwbqih0gr4`-bLLy6$t?n22P!xvCsWZh*HO-|OJEE^=9x zVrTb|I#N-r_jf`yIyWAM^)R(vZ|*bYv|mJS5ACtDJKg*A2}5LzzT8nVPkEn_JL%En z$EwJ~{jnMG%=GllYb6Y{v|Oia3B}NINW`*?!7}avZ)tNg=wx2;k?Xgp*rT+Sb@&xp zDTgjJNNI?lzE_o(C*C<+$-dAqAa$^Rsa2(f651?tO`d={Q4t9!)7TrIYlGUN5V>gP z#54J^bkQ+novc$-(Fc2znddd*Momh)SLo#A+k}NEkCa**qz@rxGOBLdKSoJLX2UhZ z2oCeM?fMLy{cOPA7-^1t+T*(Cve6lDFQ4dpVZ;9R(dPm<8)4;(xBsA}(Wyl3J3;pi zCxN$(342|E=A$*$;{`@=}`2Osi1J&*~=RKz02HX3-?UH3k;EZ*WR|t!sFW4-#=Q%X$h%sLGoE}De9Gq+dXJak0e{Nl8s@?arhxR6~0| z175$riL~C@Ofy|ri1G>zRwaDXp7^8UXipv)49w)G+pALH+??>Sr->(QVHt}@lF3r03(@;_{{+@?e~BWXG3m#3Ur$=N4oa(yK@IwOKkx#D?uIaUJu7TeFYS=ZF8Pq}Ve zi&sLvYw=ea1%~SCCi@a%qL-0v-d&VP&^(k+%+bO=a4~p*sBK`l4{c|?bDwogsl0#U ztK(r6Ii0C0=YxyL~w8 z#==-iL#Jnves|V|HQvE$lM6?fS8P?|wHytzIY!Bk9Ri72bf2I!ao#Ynhn%bqRGL8L z->z9fe{@6@IQjM_Quz5K>(Gcjc&fcZySnn=c>l6)(12-{Q@U=sx82&>s#f$PeX#N_ zVUD1oJZ#Sbb@zI2LML5SO9c{vTsimS7Dp7K@24eN@!UkY#+DOmV(UhKK9j=>t#Yf3 z#60V_3%8daTA)Y2d&3G1dw`ND2u;|mbP49{Ukp_Na8QLiOe#b9ZeVY%jugQ!S4on# zA!cB%dAcD-KqVPBrt{S>ZdDLUQa(XRO0Q{xTacI18{7sH+pm?8!2k*~uIc_yn3@6& z*hoMLpr=~xsVR}e(xHCHkbs61hVC}#)@=bbu_-A7G56pADzDop_u6L!8Y{3De)^;b z``t1>2VFY@7n2gn8qO)_?d$dIzMgPXJG$fv@btiwE)~bLdc5EXyqFiZ)oU$+h|icg zmCK9l&dD;NYrSy5UpiDLVK)`l)0>I+q@r=>Jr`K_T%*ET1CFkuI$gkDf)E^!=ch8p zzqn|+6z7PENaU-OvX8Hl@8)0f_LkEhgQiP1 zHDnfZELys!AFwUDes@-omsj7bYJK*wh3Z*^s%J-RAlH3b+C0s&QxjxYE*~dbj@(x> zE%37FV8c}{ReT(~_xAJZ`39#OmB+8XH@**GQ<66A3HGah#+I5;p`P)#c-J?vwM&6q7(8v~76pY4BOz%4E7^UlJqnEhB z3=>^0V=^8^#p4hVr7y9-b3ePoSm@$7BJ1$Ji_FB=;hr{Tl_f+<-R@zZ|!+yTR}l)cJszlJ6lTfVt9UL z8B_>PW-CXY4o%d1vL#Fwot4Fpn39Hy9UVtS#YGpAlM3B;o`a;yXaWEFWpc=VukNg^ z&j>5K->FB?KHh=;Zo;R^-2nqJJB31R6&2WuA^kj0R^Qb4FlpZQkZr)br7A3X6Z!aY ze#RL?ii3n!n27lc@5IMa%W(AlcVGMrVELKJhYj?To8|Pq$4=~)dAAiR_UKV3bnl}c z$FbpYHIkXBnOqJ7L5;eydqLPwg8o3`cA3XP-n>~2#n~cjAv6+R*m1$tC&V3j-ty%v z)VWq0TDWiL8TiS3ya)&)wwZ~w=tidM>7aO`3?#AA9aC(xU3@H15^GJK#(t;tZG_gt zF@24e@(|x$^E-@PC4>3HzshMJeOt2T9E{gqp?U$Cm+x%9feqe%lKZNgtcgCQoEJ7^ z77edErzP3AS&TKeHQoqt?927^(TqTmW$O-V{y*3eAmz&7p`Iv#4-MnhM5gdMX> zt^r@SN`r{B$<#4}WoY)52QhxYjOFfQgzm+L5W*gqXTjq!!$nz?T(GUy~%(M6u%q2cJl^Os*wrro>D5<7Fdt%cYxa&tv{R0;{oDGq_ z>BS})Z==F(_MWDCPR*z?W)vvi^-|y3TpP^_5H-1XIYKFUJUJujQ_={6g0^cXTFS|( zbl@Uxn}I|_Oe~kpf@BBZXFtF28C|=bB0RkC#xdEN1TL%!0_*N6;`SYu(A)_$5s}wF zOZGwQ#GqmLDfN%N*%2Ywd%dt>EiLl}lyx`Q@5DOOX=rPAY6_p9-xP_n69Y=QcO5snXEMR<}tL{}pzO z7_mx;t*DTx3F_$`deGFYr#I*2&0EN61tkNQF51!X{<#yEa*e%8-0d%(K7+Tmtl;8l zO})b$On5*~5BGjGaVS?cw~RZGhMFctD*lVf(4estfQt%ZRu%Kc4#R1EEvxaxP-}=c zM6DO)dK=G+3rgB;>j2yt>zz^UD!~PCCp1;YZtTCR$e`pqbo?oiL6wWEBsd4wVGF4C zn*(3!LFgA19p#HfV(PK!I-P%is(Qh_VpLm=eYLXm}-3GXN7b9@C?tX z1N#~pZOb#$v5M%E?nbCt$aw359^FaL?o)ioe12?Zc1Ejw!ePZnt8g2hUN#Fd=I8U{ zu(PaQ;e{bto8UPdSonS&hSix68HFp$sxGb&J+V}$=Y~m6agXMTIo<78_w7!bcQOo8 zP0%}I18w)O42J~4X_YxpV$xc#5i6BeC|9|*)@Jq+`aI$2&f-*iGlfcZ5(yt7Siu1S zukBWQHhxrPL95pvt)h{Y1ucE0vu&1>(q+;>GDf7batV;6fZQ23(5Ae+TzSI9peo4- z8;_v4xV&K5CryJm?=9MBSJWs)cV|S~;2Fg+lIQK`=a=rESJ(4;bX9UYL?F|LA#2by z=2~)EBE$x_K3G~l6c-=g{2mSCuw!WX zR+_dTiWj-L?4*SJk$CX_Je1kyBa3wadNC22e&~}_fDeL1R@kHjH=E6lZdN=YDSwJ{ z^QQHeu}YhzXwpLs*g(C#y#cQj3}Gs>-(fm>CFtrvwXSWh>CVcnVYYm84mbK84rIwm zc!@R6Slwia5vnTfq1A4L&-3bF&UijElj3d!ixIFabuKbc$ zdU$dFV`_+W>-=*8l=?3u?3)|N6CXeOeSTivW_>H?Xz}^UDvGf8 z;dY(9@th`$KeYM0yu4239f2`wyx5FM(sN>=ep5WC!;&F!bQsh5_RgZ&bL*>!WKY2( z;3?Z<%_;A04p9-DxMwO=3P3@^73}D~)K=+0_FPWR13duUVflkOkJYyPmeuw8YLu_W zXxP8Stk!dJ7b1L|ZlHA(z&&e<;kc~%bG2xz#0rAx-KP*2qB-7sU=>g^g@f@?fQH4} zOF&VKjNSByNV!Jr*fkZ}zB4Rn_WE{ddfLa`luZbFa2;tLJAJVNd(9adC;eQu$!`On zx%+6lt9se60r*7U?W0^xTmLfDL(|5^EM_fEU1{D1+#@G7=98u0VAG|xFym`7Joa}_ zx*kGR5KX-6cm7Ft`QAfX3R+olaoeNWUcMVw{QMs$RQiqP_MfhLc6>NOBnjTmwTZVc zBfN>l$bq7lxDTQtPgfwJ%h6$M34zKhZp zMpD_5gHgZWCph=4YPVKgmJ_N2t5#<$*hd1J1gpuG1XAwfeEuk zxG}ZjH4>iL+lTvUbFD+&R8ods@An&U2?&UQDxMfWg`($C6kCS=<-dFtSfL%;$eIKk zq`o9;f+>fO=pAFWC>Ts#OaunGXX1dh1%JYHJL)+32#7SE#?gzbYfF0;Bi8Rsk`(Ap%g_)Xe1d`|D*j zDh6w6=b}^w234bp`sf$>%Q+2-03>ide)Wnkfr8S5<^jD~R;D3S?N{e)@5EQc;o7~2 zaX=sDZ84};y*{a)vCUtJQOqTCKDumL!2Q*|L_4U}HDjSSG1|0rU!ukLMT`=MVbPp; zG3V;EW2as7;f&(@cWGe&Hl+k)PrP9OE)nJ_?Dq8M=f?L|su`FMA-Ks+#vd}ag+-?{ zN#9MV=5Xj-fj3H=ym?Q+37{o00@VjUiYHt6=D53zoDQ?Db_heDBnF~a|M{HcpjRkh z_z(h-dSUVIT`hb(ykaF2#sO2>*0%t9WIG)^p#Bjfar+*irn!Y-=HE^e^m)6!*~kE% zhJMztZjYU}oYbM}qP?+2i1vXUd%h0Obnj<^we@NIZhmGU-qz{6owqv=(u`v7fJ4%L zxyO6Hc+JLoQ&dz8_idGi{YI>A?z(}qpiV8H+oPS0>gFhA$O|Q~)KXS<1dv)@bUHuv zb@!~xXhq!NiI9_%6PRu-4Qm70n^$?Sn*^q!1N8UQ20t4k!0~-dMEmnxS=nyLG5~xh zGX71Y+RL3Ht|#5b_h|Ug&-mY{{fTY^q_CA$WBYbXwcI2+0-l;K)N|*$)(n8Dt9qGa zWIjIwq6{ubQ%h6Tgv}gGd;fm@vWpbK$B_`*b#?iFASUzumtyySzGf1uskn@0hIE@&Oej23#_H%SzW(%UKx(8O^eern$j``IM&?@gqBXWn3+W&jv%b z#gw?yu0v}Z5`_6W2=im`3u~^@1n1jCB(i1=aR;@X@`tl_&8N4zN@Vz=#6P3rT!3g@ zt~FptlB-5NBplYt4yQ#>0N_zqw({xTr)Gk7C(9vjAR^bVjK-X*il(O*trXGF(M_e} z0KE9~oJnfGJG&ROU4Y{&C@Mw=vz9K+9a7&NZ#vLfTp4ncI&%7PN?W}4ro-^}N&$&G zAG(#*9&nQ|cb0~h*7?@1?h&cWqen7GVSu^9>A1PyVRy-hk#DN>ghjDv90kua>Q?OJ z-2m{pU)2;YHJ*b76hO=&QKrd+FW0t@V&-jGA3tOnsW^ECa)b!tmF@CVl;N%D6X7cs z0)K-yZaBrD^jw$uqd#eDUO;2I@`_U^tnT^BNN$x!0;w1F|HnWFfk)C+%-7J$cJ+0! zmAK&U=$N5+xPwUxm}AUI98sO88bs+MWgj$cRY^Y>1^ormU|IVsq(PAIUqTvA^ikz0 z7T_Mp6HsKvdSQ`F_)!K9uv>T$mY+M8g*Y7VQ^W1u@NWg2gDQAAeTU-O2hWf=;e4hR z6SNnzPb^*dqr~;cstH^G4cxI$u4jk?x)#96ST;NNkaJ0PWkg0xM3{Hjqp;stxdYR_R=A!=M?4twSm#b|x7zT+ zo*KitvzVAMqNefun6Nill{& z<`KWpWj9wmV*7TJwOu>dJ6LE~uME4@y1zf$d(33Ky#@Nj`aL9sIqVIuU`1R3pvlTijg@C!eiKoYd zj?oS;V6TaFdsha<=;mC8^cQ8?DqneNd4BXXdyTtDzgHFN@Swi>jx@iPSv+$t`U2%U zBOUJM`a_h1!~K5?E8MeWX6{+uwIE_^S%&>hUA-dM{>pB1rP~G2F0{lk!_%kwkQqs? z{qQz4k#a2!Y3T*=<&G%D1vz6m#IWh(Bltx{WglvzF_r!YB%r73 zo<#xV`aQ3C+n(tPoPfwk!yy*xHD33&rZ#OVAY%V2j>3NNSIyGg_h{o?@^)i7YKH1( zRjp-_4rEv>BaiiytwMqL z7>47hj$qs%G2z%%k;eHbyKs@zv#n}>=Ni6P; zKEIZ|7YV4BB9S_N1(QX7fs^6 zJsm8dVyOO5n0I%F9J6jCk;wDgmv~OTXUyk421pzRi;jf%-x0v}5Eg!>sB&>Wf>hL) zUyb|RgKgWxHxmD$4pODbDozb-S&rILay`D~;5gQYp(Dif{pd7gG96dM^9bix-C!u_ zJ1hB!$fb9L^X}%%{cco3e=Zb8PmdUx8A1K>^K|+E;FJ0!4-y9hC_q8-4w!}2yIIRw zUPBKbjo{)K+&La8q~C#;%uz@z$BL~$A z;(OB(@9C0@%#=%=8lI9>{{g@3%ktiSKkQ{$_8qPg;#f3Scp(@bA7!`PbE)l25K-e& zoSJ^$hH~{^bl-0D+y7nnoj&r#A2u%;3z^e%dpc8-7zh)VRn>ee0t3lH`}Dn&khYP3V#e9yKE06}?r|vVlf^rW98&*z{FQ>3&8%Osx4CyCwdNqT|Ixw0rD#IqEqy7* zJLbwuNcrC#c1m2f>;A3&bv_Z8@n|(aKLGku#DybSPJQ+pO!F&qP8*Zq09|rP9lfEa zr|iqlT6}R@nNFwTB)0^~wo$=~Mp?F}c}FpkSOBYw`fmz)9x-U>DloE~PFiG~w*+wu zjSS_Mw($O5K|6ajr%nwevTHXSFSfameL>p#4GJ!p5u=MGjhr-|sb2M^j}nU3CI z^59=mpRj1|e(vJ^!Jh3j83ROO92(Zm%?yB?TfqM~H3L8%8;ReZFwg_21%!5yX@{haCmZPAAl9#s zvOZpyY%8DmtqeIPU`n##N0k=r+*oR51Tp3fllRgn#@p8w@MfIyKR?I&-9)3FD*Igx zPJE?qYAY-!^!u-N)LI(SpS?Y(PHxdF{yBK`f6{ILH~sq#mYUz;XjMj*#g9|YXAWg) zv0vFu#ak1|J2vLc0#cFkJrV_J$LDL_9(X8`;_P>e%`#id#| zk^6KsONklW4t+fU7^7ogoTRM-$oUTAJqbtZ(O3yZB;nI%fkF}z2d7D@&x!D6#=kCc zh2bCWF92I_rF9dgkSWNA9qslNt2`7_5QrM$dZGo}^_1US@#?V49)g8=$M=h$Wdr8o zEUu6gGg-s4^-UZ0lQantQArZc#`J;$)5B?u@}exWju^O#ga2s0FOIqtOy;b>G9N0j z7m;f?k9OmKCi{5$Y`+A%@K2e?>$h)jW)n!+cJ_WwxKd*MPZAE^EK-!$X^(!4MLs?4 zGtGY!zwLtheb(V|56NPVE|_kaC*b*L1v`NjgM$9$^YVnxM@qd%DD-O&x&x+RtP zzTb@!pVguv@aA0)c5+93ZYZ@%@_lsMW0?Jbk$Y-3>koMFlOzH!$&sUv`lkBkb|RK} zNfkeWg+`KK1RVjWDn|L-&h7xWg+ofV1_zXXLnID~@L})$*UAesbFn1e!UxcD3i4My zL-B*;>AWYvJWQ3q_j`qcoQ^6c_>IZEA*85r>copUwSH`*`TnrB0S*0w(rN2B>^z;V zbim!9Ul0(80&UR>*3a}}i@lV;b=zAeJ%kep^ET>8aF{_I5Yta5rzne!i`%=DnXh+Z zjr;*Ri}|^8=67zelvvJwfmQ}iYGGkN8tNbI#nC*yuEYq6?F~8juYG)MmJ=~fTA&Hz4^8Zuw#|h zSV+=gwF&SBbUluzC?aD~wi>m*aqTLm+uO+FtmKk{!o0*z>MtvQl_pvcC4xk#5c{*zP{XW1aDCw+lJZP=$ZRKj7sHnVgCmp|5*~Z}& z3Y?v}mavVn+_F?WgcB*3+Ezi?&qoeqBRz&8f{>Un*RgIEDE>ulux`m(FUEcKCDXiQ zqt9t|nCO|rxZ?q=A&^MoaZJI*gb`T-_*&xuj&;!1;6D+w+L&niMwV z59ykh+hA+^-3oiRs{d+*y(k~q^}U4_;QQV0aRr>>%wz@}qh3Sw-96Go#%$x-!Z2ew z7!V4*J|kI~r6djo%00yy5L;Od6}IUR-UBqCzYTfpFN{3qnov0Qr#=7w_pu ztxo6!SM;W)Cz`UQqWrWynR|vXFfd@09Vxfd>bInIXsKB!A*^p+6(9vSLKPuaAhIW5Ee7>4_$w*5Pz6yswpmB0BsH{ zL&D)|WPH2``aB9;=qZ zLi*VYwSue_MiDHG__hb(;Gy*xPk%vNINbC1b1m)MMdQo`b=k^bI*-dnS!O|?=mYu( z2rmjs_xuqvB~499^antDV+35k=3x(FWYE*5<1MW&#KgoDo4yH8PWA#g$2U?AB?`98tD`)fb?a<#0MuX7?Y}U?9}&Nq09f zpe#1XRc79jR&u^=%oNJlY3O&(f*Ay}WW07(lRXFAmjnA2%#%TksXMKErapOE7%NVh z&(IKm1DfcbW+HM}9(bPL*yb!PL9DGBy$CuS6_tgwclB|+ZV#zI?R1wOA*h%1-_Y)H z|4h3}Om25_>j)BNq~C{z*TkV`I9IPV^UkbSC=qMvQ7M=oEStn*K&KHw8%yH6R}Uha z%8}RtrXfx?jn=Kj+XC|!S;@WkH7kHOz&Wl{ql)(?v^y~!{+8j>niD|Rd`svGX%FJ zi`2s+!(_J}7$;n&IwR_fTl61;^aT{{RYI50?4CW2V(H5_%{0iBYid${-T1%@Ti`&e z|Mt7^H*{PY54(m~0pzO(B?+X)z=_9%J0*w( zkNC+wo;MjvPR?WC&mrd3-visVmc7F4kS!!yu@uYv#3v*ot1irwG2yfAUc)8ZU zsmUf{BvO19w1a1GKUvSW_rs)CclPeR1Uh}ASPNgZDAc%L@E!|2=p)_&LLA3+COn}xB+BKW8fbvfI-3KW{Af$|Q`s$xA*52#O zr>3SMyU450E8tczHYbemy67v|qZ<6G0#4hNhIZA2lYeTiednOL=DOITAdmKyffm1c zVp$LQ!%v&<=j)!GmppE#O-|ybB1Q|vVQXI4rHXmWwO`A>i8Q8xvg)VO57TABj|b=# zdw$qmPm|o-?s(Mb`eP$OC(sh5EUg?FyBUXlmy%kP5o`UAzqZSt+h!(`R8v?$BFlb` zjz1!E(E7Nij`a~4l#u>nqKTRf(tFD=nn8E&4x{mX&u=AU$NT$f0nkGOa165OVN6G5 zlb6wpD{z%-LjC-HL@-Nr%l>PdPqgokQva)Hj`shnF8H|+-3@!mh5+;Hmus9?ub(OO zzi+&1-sRo}+0toLybOQ`%sfWm4vEOtr1BC^Tj|RIX1Z|ahMB`qudDGB)Tmb|=Tj`H z_|mXl@=Bmopr#R1vY`0~ll?}YbX@W8Mx0#o`HJLVcM7J@_O`mxrncyFV}s$>Fc3hh z_yu8pTa`vb`-a`|v2;wf`;5_@nQn;!j^Y`d&L`3uSt~$i8nkI}!ymV?oA?}n)h^ms zx$%cERNhO@tp+d=Oup$>ey)o{rUA<1KilrRdB5B4VZpYuC6T|~lBttIZwVp-KjP7H z0jTR4tr})~8E5JDHmaAVJR;)m|E8eb^7j6z2}a5_GTgV_+y3M}FwN9xTQG2O)x7!9 zzvH0Ony+*;UKrlQDj_2xkV03r<_?rMt=sE!LwmaUh6xjABM2AW|u8;ny5hq6W~B>UQ&inZpmt-p}U z(M^d5jN!NN&aYlhN&c*-t1l)ZvS`;AZ_MMqElh46aPt?X?AF>0U+X>#eN}R@e0I3_ zTKouTeb+vxh{2}M9xVs)@6qq2MO1b&ez4Fe4Oi?EbLBdIYtgr}E1$iChlY)euMRAV zE>4iL$}G62pOi)_$hOjb}%T`8EWpfm;H+KK`?J)-e!|dXf6;MQEqY(MRlmQqk)So-Po{ zeD?F=f5%1tH{J37fZhK8-|;_G0wRG>Pk{Sb+mvNkT7Zpd^P8y)g0=WC4;#4&jixy}cz@Vut0cx-* z%GL?-&ZW$$l~sx_XwjZfUVuNQ>eK;J8F7$<#TF(kqq;?P6x!Mo#_TjSG!~$cfhKKL z3~8jI0xC)HRs|EY@Pq9kcoQ5d5|qmJ^B-10sSKs&3!hYWgEJ83^xEk+7yh}H*qcNo zkH?IiYXb5{%u=!}ZW9xBt|u-^gLA}jX7+-83a_g_ltxh09_&9bhaDVji>c}9lPypp z<7fRW0PhGuZD92UWgKh~?{f-+q=d7}ucnA)-JNJVf>- ztNi#{TfZ{x`yJwI+L!r2QuKtcCpI765)FmZYK|a0IPNVCFb5X`a8>soG!fl+q+He^ z3biDsXR*as!J!MwzC?XLdz@`y&3puKHR9Y8r~sj1C#`t)nkv!ci10Fm#6Z9fne1X_n2E9x0z0| zY+Nd->953+{A3emVhlpI93NMQj1ff0$P zjE;xQd610LrQeJHK%`O3AjHt2XPP!;^|hc7FbEEwGLRkJpA$Q1H@***p;st)0<%=A zKD-OYH;_EM5H9(Q$&=uC7UF$iNAE}WG%y$@t-J(uV^JG?wiykrz9HW&O5#ByIQQ|% z5ce(^czaM_K~P~(Ase^(1X}hgftDb}yvJUlIU6FeuVKz7rPnQjh-2|~Fzo&T-uPQW z;1OgC&B(Y2ZfWqrX=*bn0kIfKxMaCo(F_Oma<_E#Cs)+_P42_iasVNhxzu-<$BwMt zt(+r6mG_`za>iCUEV+K{QDePiRtNn+9Sti19-bW5Zx;tV3h`WdHv1jlmQ8&_;S$Qh}4(ppbydHK<97t}~c8*=KfXGP;w`3EDs zwpV)NoP}*Nt#3|;rj|dLztX<)$MrI@Jo|3*-o7T_!sm;*&NPd#^W56u@NwS1zueeO zkTXmlH@(ajP44@b%P$e#iTs<@;XQ3^)_sP(-4iO%Z(_V|^j&Tz9o<{vSOdH@Mv7Ph zz4W*Hm*kSZ-kcy$fVF*I|JvYzTGL)(%$}?N$>fZULET#?c^@6$A~Mc`IPB-;Lv5hb zZFqMS5GvF;1MZ6EA z*Wzjbzkc4^=jNL69$!8Ab`3!OK*aSWAu{qlf@0O=`$045^~*Xto0n#B1+QUXfMaJc zj;%rM6KT9VunKZ5_4FWJ6_GhLaMlOMkkUvc`__8#@B41l)<;qS!o!IVr{q32`lKyD zDHRqL>%2W(3hg?B#?J7UdY@uK6q7+|diJOUaav9}D7?;Az@nXa*0oMJMZSOk&RxEQ zGpG~pp3(KxK)A!`>fyKUoHy1W5VH@7%MR{4==PnSapyFA)yAL1fOu3|QE}W@c@=wRtn zzWp-aMF`z|iq~fL?8e*_K^VkB=y02qwyIML$UJpl%}mR+(yDYlpQu^QJl`LHeG~f@ zSoN6Ax4#H%(WaSRn{9!RfgeM0*Fgr1j>y-Klyk8dOf51`8gve9?6_PN!J!&X$w8j- zriY}$^TfQZ1x(#TtU2!M_m2g7h~iC-yJKh4IXL8!R##WM%Q+Dg)ExS=*KLqL1|`Z* zR)?#dCmg}H2dpTp$jp8fA#fjckY>bQWT#j(hr4IPj84K(@-vR@>|!%E?1_koD7w5~ z!!<)?2lafbhkuM}+5S#YG~o`%tjnMjaKG}Ge?7;xn@o+rw$Zcac-uyu3sJkjcYzG+~=gKvwoS0Cq(E z^ie!64-ow7ND%0eq&N)p+H4Cafhbm9K^5ALfdnpxy{GeF2NAkXB)eitj~`&S(PW2# zu>ro~OL96Rn$=EH=(&-`szSf-4|s+)QgD0sdC->xNe<^7zx6J>qOrRUS0C@lvLp(Tg1%rHQ3d}O>Md0SuV+J1?xt~9(ok5q9mY#mqWAoLw zXI?zP%l+j?Lk?sxOn9Jyzk~Qc??QLZ#;dJDD2Ff*xb;DqrC0AA#~a?9oQzGWD0&;- z+20-HdP=>!rL>C-oBi<-_jVkYMBc%9v=<)lfK#QrPkq;`r-CBzFG7znkdTZkKb1*H z0^a4uwfv`%;-7$|4H)E z7j(FplAb94kE{GKyFTJ8Aw2^_0`_+vvO+@q0D%)ZWo3SJ;Aqt7C`PH{*z9$i@#)lu zD;-vN71d?$($Ws|RX1k+=@wo@=K-Nm-b`X`a#*yKj4#&uhhX+* z1v}f%f|Vm9U~~pG0buLkjr!5l7RBR)6jq)tQwT`w#yQ$OEi6l7S7gVq7Va6jwe6Xr zH8sbuaps=mK_96^mAEU71d}kO>y5ARzkByC>-}8@1|jqZwA6{DT`*ngeA2rVk+TAS z-J*->n#`ijo5tg;UHrFiy+VQaH;w{h7CNHvbo!V98-F9Spdd5IKQFK)CntHOckm|C zmc!V3)%O-IuGx^Fci9WXTr)h#?HcwvkE4L{!xazOdHcp3p!|1-Ck9|7^=RrYaHAW^ zyQFmx^+(bt$Tz_3Q zN2}D^DevGQ_iJa@n#AQ?Fl>$F=$xw00wc1PKm#NfLG2{teYQ>)4HcsInJMP4NLAOv z(^h1z{OHGrcCDB_S}hasR>^POH{p>ih#s0~X`w};m&;}gV#a-4Z0(khVSoz0-!M9+ zU&duR{e*u|M$8lYCh^U-1LoBc-=C0>0s$4fD};w51(-c=Bi$BwUux$nV;V5vA_Xm7 zL**L2yB!`EzM6`decROq9EeDe;I#^(SV#x}oTzL$6wNp@!o9!E3%huU$8OpvuxeI} z${O$XPLlgzNP=C2iV#bB)@qN#C4xmC?2jP{)|8ihdD4y)b^x*XXUs7>$`^$AfH#9K zXLy*)UX*`2AbMavkU08fXXcyWe>U!vb06|eb>F#Z*RGPCKGl>^7Be_^Kz*nIzT7Fq zWr*T|bRO_lyWyQ=zq9uRwMMEbQvA@ zyP^ztsOjmQ4kww9w86}2+j0ddER?0?5HO()$7Nl^cdNeJ<=i`lEMIFbiIHYS1$ z^tgcYQZ(sHLAT%aMNnQ5l z4oCe6OqA9ZV$xDA1Um#gPn^3X;Fo(76R|c$a7G&Hvv836>OW&LN%kx=2pO_Vfcl;m zpJqkq5RWzI!fuL5tGI!15BJ#~AM9KXSUP zPIz?*;Z3^_WL#Lx=0gRtP^heYy^>AJ%HCZXZD?zn$Blh(tN;dS*rsUu%oynDJJ$wF zgZv-w(4M^Eqk72UL};|tyWS2{vG-H3Yd@B5V2FU^h1YetnBS>X@m*`Z_2&XpM4?$t zQ!6Lt;r=Bk1arH8nQu<*77qT@7F8{R_s4U$dR2N2{Ez(tzP{*x^8sPL2Om(8TQ@bS zf4Z8peqXK}0h=7Sz%bWF5)bzxFq)vDHfYgUo$~1So$6MN{00mh^PH1($oF`VLb2BC zrk1Iyed`f!0IxmG`<64N7hK5K8fH$1EPI4ga!TtWB&Vq#-np}oHmd^-CS<1ih(-ZX zx)!{%o!=;2%;oMl*Tt*`ez%>T5SIog`KFVD_;&Pj^U$iJjlM3mV`|(`i;(W7}9r54pq<_Hc zIJSyzQ=gqG@{}^W-j%^mxZ!QR{23A8&4z}+fI+!V@(nh@Tpja{`e3AHg|Xo(y5#9 z<*1y?IXk&l!weu6i$rR5tvAV}^_}a!$-fEck@^A_%YPlvlRijyvF$%4ObgYm;5G-* zFTc!FNq+w1ApgzUT=Mq#pY2h;Yi_MMaNoWJnMAb3YLAL4eqMdN^n<12Lvg~KK(oWL@pH;GVu#UfddmJ_3+q4-?6Y*Z`p98iD+B; zoI*oBA`7xiF*&zRu4afeGHDa~o}x&?{N3>AGkD}v!TkYd^Dn&5I6}b}~ zsT?d+*s-=_x?`T2`Ue9jzJPXySh==X^V5`LJFM5vbF{_1y#bPNlC>c`4`r%3?rg8z zL(ogoNwWtevMU}0oQ9?nos2uB%E`+U~gtb%_FJVBXo;iEndvrFw_S zgK`TrSR-*HY#xrEc&+Cz3Nku=v#5exfieEnY120xq3N? zsi*gqYYvMY?HO3s!$d&y>hFfAf@i;Qv@gH>O-~rT&%9CV7Qg;;hD-e+{geDZoK64f zoBDsr`1CKKVF)8jY|CbaG?|VSc6qfi%dgVis$1&z8$Erj730gD>x=u!x1Ma_Y^}Zn zX!NauTJ)djdJZ;@ICJIYVBknHqs;Z#oxtf)5$?35I6lfpMTAc`oV*z=f*dlwn@{Lx-*fdT4i4DowvZ|N4`ifPUx%+2$Gh2>e z&@bKt?bPH9IfkrHfja(}cIq$*VPRnq_y9Dq92*O#q7PT_@R<_r5~Ia#fm4184iZqR z(=Z{(*7D82aP-m3YaiO6>XZ{M>*0-qX2U;Iqh3@4UIr^fT18|+X57vTDp|HW=(r7tNiYE?FW#)gCDCoD`?%8eVE8IZALE6#33 zbQ0vKyP!5~6zzla-oAo@5SsS#g@sLNoe_afNZT47c^2kVHn>BRVo$<$dVd=Ok*pMA zZt|XCJ~^o_igD)%LUKQhcTl}<924~ygDfM$8;`!kn|w_Dqk&JJDZkT5!9hw3^v!@u z`EOd=7)ULpp(_|-Qf&)_}zR-j9dBg1rG&s z0j~Dpb+Q^QWR1nA#5SsMhI4byO_jK zCF&2K2))_m7Y86MHVj!n_6(xA%3= zb5RNZ7Q6UaF1|3OBjbh5&OTuL0PsYw(C>+N7qvm_rQ0k35h%=BYMPpDX1`a{_wQiD zQO_8Ar4$;Gv2*X&#STh_i;cSEb?3SnT@Yi#ZZJ#C$`k>U+va}!NI8q0wEEHjUkLWw z(EXsg=ghk8+F5@E2}WD~tx9ORf)-oCm6jUfHB?3Q&&)8gvag)y|BGy5r-1ZPQ(M(S zfoqM?-ym4S>S*_u4xBJ?V8m-ni%(S4y4BhQ`2&QD5+v6c5;g0vk^KB#0{o5#g|8ZK zA-ujYjH%wRyOGTKR6X__4LESY!~TETJIk=D)3xo}f+!#gNQ+7cA|fTNOB(5tRzw<> zbhA`Y6eK02r5i*#m!On%cS(15z1Pw`ZfCYL`+bk&Io?mt9P`C-4e`(WKCkoqoe*%B zNYztRP~yjOu2b|>ClnHaFdf{9-FRSwiGIY7kKi^mkh}U<0I*fU{921o;&ioef?^Jj zAprt*(PQ^c?^`JRIBJJ+nuXb%XR!bg31zL23tCiq{&WAjdnT2%Xhf>lnhG1Wd}(aQ z8^o?2Z%b+9K~AXkIm8>c)A&Gqh`Kk;Tk|8q!j4Du-@+qry?xXb1TV zmUx+MtkSqbYpc29Wo4n}?3(r45MBYhIP7sV?&p={=L>>q6UduwS?;HnzsnQAegO6h zB(Y$ccXC#Gg@`a5j=5Ex=%W)ZX7E(ss+}hZ?uKM8aTuTD-a^L`$8lCH%xM@B5W4L) zzD|0J7X995?@9l73505BT<@z$4SaOw8cgx<^q}H2z}5Q-X_J$4jWP!gMO;%>&1Y}R z0v+L0C>KO(KJv`#+)5Of2u?h*0*+1+i$Jq@OQ0j{#m*=2??;Az1`g zC;zvsdeQJ222^so;b<~FcUy1XTX2^;lpWLt!R{#4)V;eaWqttgNg}}w+H{zQE+wzJ zkPsbNxKg`}a#n=Vux;5`Q*)M^S412EEQu;B+%f0=w$BHVY%@2U>>$)qZZB7FW?S2&8eW^Lri>brY~v4*GWxJigSXA z0feON=^nxkhzGQHwBSRYOI8-n-c}u|A24l}x>G^A}=cg%46QYQV7 zU;@Rkl~?_i=1v@-b9u4~QxlcrUUD+f;mZ6G6i_tNvEcvxBh z){4C~?HV0Y1s#@m6FG3I&tX#M^S5tV-jUzP$-^NLfBhPRoIx6!VZJX2+W>)y_won? zqyZV{g6-fb0GZ3EDs>1cJHP-2(wg8gjJ^pA!GGW?oLE5Xvwe0xrjG!pgtS(%^ZE5F zSFYXMQe&VVU3>e`l0iqN*wffS@&cJ3h7o zpR9ml^~Q92@0Zc@2+@myaCBf0wT2@2kh9D=LZbC_zEWqge6HdbbTzWK+k`(`Ye z0*^ykExv{Y%WulTiWPP}ta{BMA8!(^-C(&2>9v^=y2k$N;r>4O6B36!-72)1R7%*d zT=NSDU5gf`bKgOs>q0l-EU}c5`noS96mIJzIz*|IcV4>5tT}mZnwNTHteKE>qXK1>E_5e zzZWELNLuf`Ap5}Mr3G6J4y3?+rOKO>Hy^;35mFJ2nx&fP$AggRo48((cK>EZ($Una zKNzAGV}5oJDb$IL6F|L256zZ6z{*^CbwddDM?BVTO)#SANlre0o@k)ZX=G4Kue?|X9}E5^Kaj@0ouS$-iKOz%1?VvuRX+VUnf?clYLEK=Kx&r;$Sfq z<@ymgxxi`v4)y71gC(d!@EQ)0zR<3LmH-MApclO_M<53u9qc|{wPHrW!|)6abbh|C z*>%}?6&M)k?*5wNZM(#1mNFn3flud6(IYi?Kn{%2jx*52-UW0{6ifxz52x45{2mcn z8(gpd;aT&8qw9BJs`V@1!GGd~e%7?eQ(DeHZQ1_#O^#0(f%VniOXx0#_RlDaqMTe} zF23U5Fh)r+iJ;91L-4-+BkprW!2x^C`?+JvD>wdGOXT2F&)kz;c#uyBMzV+Jkxrc_ z+}b6i{sJTIhL|T%zW{z~NC3gR4j>TB3=H$8H^iaWb2~u32$vjne%lqlB}F8Uy_VRr zr8!w?=x=v!9X+N${4*)xN`hM8G=gX1x}gbesOQ*v&L8ZCzEU4u0RqI|A|rl$18^E@ z3ZVh01vvA`_yiv0xA&4)5(N|`WX!Nfx_;3SvMVUa zN?2>_97ztK|DFBEz4z0!lZ$%0C@PiX$@`YnX4^IeF|isIqH$ovLy(2fzA5+p2mQBm z&tTXP+*;R>u3oKjzP?$p{%r!YiIR!0pMSCCIwTt;8GOfm^m%!ug}En@rT-iX(#Iu( z*9;rK7VRT;Xg+{221KChY2O%%a;?)fDk~_Ys&fy53cjDBDxNo##+}ZuMW5qk$4%vB zDU7R3<@LO8{m7x)F_=rj{dXxGD~og%e(Klw(Q$DN!SuAW$sJN_u&my6jfI?1(ihM; z04F0<;uS^7f64)B=Na=P=wKY2r3IF?--d%$HgRWGhvmtsbrOefQ(E^{I!Ru1U7cxl zS&)?l9tlSe~}Wqc9{00^{8(7 z#ZWjf-mws6ORkJ~<2o>I)D5GK1%XT|+AB_gsV4zeKK#Jq?xz$k)jzz;N)}#;jIFW# zC7d+S*!~7MuX7=d19LNRY8_c0fZ_V}YXdf}llgWZ;lmS;K2BYrOdfgI(7`=kfB-?e zrqwvF1jHv7*m?ehbz}_Ytbn`Sh;w1<{7yyf1nv%7ZPz6Jbfo6dDt~1KKoPX7dO>;( zWb|_g2-45<%*q&1hOoC>Wxz0RFVQi|Gijs;%o6*F@=1t^-h)9d+I(K0ldm5X_o59d zhwX1`^GB=r!lW}-Y;CtealqKjbcT&-gtmbB+Ke33vf4-=(O?s_x2L6!jR_P6$QWSL z_$w=cx@@!1VK=Pq!C$gEWBYJ9ugyr)83JYrlBA6$ahBUGu;c$pI{h2_4`LVAiF48s zk8WHbYQ4u4FJ;^-OhZSv0E*sW8#@)8_TEicD`18wOjk8ZZN0u1bX4>e^Bz1s2@WFT zUK38+2Lj)5AAOy!D?xnVKA0ZEsiS*CL_n~-gp&=#Ha8zgqBzesS<4N^x#*t|*0Y3( zpRU2KP(o$#?(}P;x7i6xm$7F z%bv;9Z+-c>Lzxu`AAi2H2d;(Gr~!ao(}OK*vc>|B@`YC6bO8(`0^G(wM7{Nt8d)*B zX%5rBf^YLom;3!D&O!X_mkibYMsYDTS`0Q(83#qynnmTRXh!Cp-h%KfxQkEiHL8G1 zWPtbi;E%i58b_AMBgdv{8!AWe5!pP3z_@s@nG-&82+53 zDhdBdI;CPSPwY^JNX~Uoui)AJ#aF}AlJnJ&&nwso!*EZ0N~%KB#J3clfGG1Juau^M$9u_1y$q-G?0vqj&|Q~R5fJ{5|>r* ziR3XdH11Poj(nQfi9EBtcFFbFvEyK?^d#dmZswI>=zY$``mL0fV_=Tn;6&xzWHOm@ z+|pyttKp=CqiSa+2R-n$n=(2(H)C07E1_B8-b*|TMjd=E40!d;6*WuGJ^Ei;?Cq@H zvT%&__rJmuu3O7VdH9}iu05v~h%C#dDj|msaa|V^yrb|J!3D@VZf9VFM~e2Pjrj>k zIb&b}2|}PPl@8knrW}=n)sXwXroXvI3fE2oOQVauh*XJP$XqTAjci+I)A2v++}tsr zebK6b6v3*guSLR%6Zit4T*Ilhe)TV0>u#d2JS-xf_J}J9Ye@&;6YOj;@|Xd3$khjQ z7Kg24&;inUEfAg9zWi9ZvH7wm3@1vRteKl8FoZHtIX1|7Rb+dw>itS25g7?TzQJ`p z0$+P~hvhTmXF(2y?iu&I{rDZU>Fiy1>dRT@=LX=0mUB!`ZX`1B}1*YoDh7WBTasTh_oCVQ1_-0g=i zx4Ci0iA{vK7nU<^QUTZ_!b&^BQdwhUuOHA}e$g<_2!aso!p|i%EU-LaYsdWqUi)FC ziJSwY+3i-odvlFw=3BrF?D4ahXjjxl?S(tQaVcM4B|EXv!Ss7#EG0vnfZoi7l|FDs zf@k5!GZxf65ePAyo;cH;6dKDuy+*kucX>^c6C_($Cit$W$dsl0h?iLni~I)H%8L36Cq?FwtKiii zz8>>``+BCS*HWG(yAuq%*^>!?;bwQs@5U_B3eMj5a8vuG2??K^Xp##kk)Wql4E3ZW z2(&I6dqfiU^=ow(T^0NfLV7UA_cz{{F6n0Dt;&lZYDAN4RssN8+ifme|CpEBmAmIT z!lJ9GMmS-K?9oktegSap9gk6RYARdf3y8~p!Y9(q>I>0z{!cOOAtoEb_{XtbDN{y9 z=oZa^$~?eU^kP!LKfaKZdbBp^Si=_(X_oU85U*YqlJaDZYqPVmdf>~=4W&c6190Nt zW(ScLXz*rVo9za^p-uC-%r$eLY4!Ysx$Hgt0dr}4OvuGLF-BaMr$W3kKN3}=)_el)CCu^Ey0 zg)XB}mgjSYy)e|h*cUF~MF&f4^Hh%FN`~bbQYOsdH1-fv<4nDv(=?G$?*RV#HERp) zMwM-*My=;zZ~Wm&K^c(;g$>+O`p`*mUs_p2weYx08JL)+kZhSfI0n|*C75N~Al)O_ zc@@-}-lKh6$d21c`7hdRsn;GV(xKf;z?q(h6byoqU=B>HJu3%(Z(~Ohe)f<_dwcHTG56sF`|_siHF>k z-}!6s1@i6KdF6J`@48~f1(h@PpSmXBq!${E=CbV@KJbJ?t_JNd7^j@qJ8h12BgdA& zaJO!0GXN)34}HIY*Vw6j@jn{-wqJDN2kL5D>7)pXa-EGQ(+eKRkqd8Ge&ypt;26A0 zRAzC-KYMmKMDJ#+GIFGoWxPOW!X;yXS27S5IVsybpU-txQt~Gqcw)i^A}TNdkWp666fpX;tC= znX_9WDlSy*1-KgUYdB1LBYobfL-$mq&uV8pALKV+-ITNdwGw}2(JU$vE5A8&QdC?V zp7zMDTmfjH0~+}9R!TdW^;raT&3)WB3cw6OEHmqp%$ZM1U^A2umuPj4b#oO;AN&}U zmE91|t)!?p*JXntJ(+5jdTTX-=OrYdqjwWbbg^o=`R7(Kx+BX1KjC>INFl?NYt*)^ z!g%`tFO!V5K5R$Z{f`d9y4Jc%>&Nx*v<`~S7~oI4Q@}O^`pFC_BZXOSqzfze zvzx2+ESj-GwkC}zeYgZWKav84u&`{;Hfa?HAX?s0s(kytSULS%UMg(on;+XtwxUfP zMy&fMj2sD|lVHc!xuSmN>E4v#iza}K|G05tMua%Qf6ZreqLD@>`mzfYfZRGr-06ea zB4$?ALjgElgWmbe!_u0ujt8V$T1UJBA%wTj!o!V3!J=;E{lNEos3kY= z8P$7OYO_Z9eO%tQV85U7?9nn#a-@Y2R_4dBVgN*}-~F*~{@`$+MHw3#PvRXBKbVu1 zP#YZbcz-IF>PJp!Oa8`ysg%8jrGAGfIuW)fE5;pW5hsPPFg}o-$0xC|&6YR#irT*` zq$GRmd_3N+`S~*y=Vr~ZsL#bW9Bf`qSJeQK1VFILMfUtcd2VL#lvu4fXKkXklOQv* zz9irGrl2-S-lz)(32reCU7ZnWBS-cO=M)xpidK}Y`tydDma)5vM=nnAGa_EWFzSr5 zNQ2W^T(1T?DXrA?9uTR+HqM?Y-q5em>g8fh)yy3NJda_Fht!ChfL*3#eMiU+Hjp$} z8W*ytz->NZTlRw01$GnzrHB88t9Zf;po0snj14ThT>Zx2=9qP}JThWv*{*h3IqnP< z=jE$h?eDdi_2_o&2c{$2)nb+slOP|PR{}Oa3{YTs_VGq7E}(Vm8*1kCZarIhA`%i~ z1Umz8OySM;i1sHv$Fxn<7{3IJ`U|(LyPem=vF51nRDrIdSEr&hBj1@35p`We5F=uF z;)aA~0E_^RUPdm>4+1ht!>2!X$5eD9F(2q~f=tMv_o}esEQ;%U+1Hl>mITu%1y(&^ zfdE(KiKclVEq9u@UR~?%lUP>sNz^Tl%G|u+?-vEx?Afu<3&Xj!`hyd=Xjx8bkorI6Xw_nVrO(3``M!%e$k z{vrsIOzmkwz^fRFe1Yex)!1$(*x%_HsxhUEva%7ktVf}lhie7mSJ_Uj$q-BcZw^pQ zx*!~7rA!cBNu}S^+qe=v#?}hj%9k&K@FPL2Q{`6UxA-b#Ub5QnTg0f-qJFlS?N8lf z;(lhvUO1iR>udlX#9$RJS07Ui?xpp0yH{va*fUE4;|XMM7?m=1^vIobXeB2&uv{}O z{aag)W*T`&xF}}FJTaof>o5?2|53k`2FX0p@sB-VZi@87RwvrYSJb0c%0%d-N?jeh%-C#)yFrj=Bpfnvka_ITu=#&H?77ADs zAZ&Q3mpDKNAw;={zpPFPQPYpotOPe@-2B5B|hVRf`?MuPWd*^m~Hr6%Z0k=MQ zxTsZbwBdfr(;OCF(Ebyw?F*)oum#oJNRS{Dq?V`6b~&9QeS$?4OKJH zhxEl6(397gJClI`0WK4(g$|WNhZkqz*}^)J-VsjMG?5(tl9XET`xA{Op!&fL)m-DK zw77@drlVI{S%7nFU558b3x;m$h0|M?7`S=plx6Rf8U;y#jSf?~Az$^~v9Q~A3?~-e zxll;}9GUtLs+bb0JdDx}+(^uLkdwn1V2$9d0L+$sZNul<&K`K%v0DjR)(3OZaB%BD z7ZUWi*@{?LvwbctwH@mL4k)~bpuPn2st}XJfoEf3`%UYYu*9zvcKagi7Iy1n7WT+4 z-nXc+(BVTz`3-w&x=xEIvmU4(In;C-Ticsn2yLV%9mMMD18ZCH>5aE~%sVG}yBsD* zf1sg{5uH$0VpjWL-+g5LGqfCnVrKrJQ+VZd%)zSP;z>>*a>L$GP7bARwE+QUvxSFU zE<;tH-EfISs}CJ0K)qI5vlDD_RDd@QdI&RmXlg|nqFkZeY?y0lD2ul1G6PH>Wt4kM z=9odt2B$ULn$NwXGO=-RQgY<`Vff_p%n6&CdRRJ?`_j|#`S*Jyh-!d7fVn#gJpDR1 z$Hr6v$8Haui1E14QUr4GJ%DwlTxDVe-VMe;-HmW(<%4yG1#$nt7e{)A>#}nGoTS9GpN9dJ?-B9)=uaGKxluAF7E5=ZndADX%At7QOH!Z5qRK1c9N6x<6B$9kUScT z!~X;xb`1D)mlTvjVK~JNA0B#2Qn>N*L8=iJ5Q?isvtjDcas`tr z!TSEl1hnu%3n;`IVE|_`{E?SaY7(=r*2D#{>%L*?2^KTG2{ihs{>W5fYbMZt1?dK&lcyE7sT8eH^ydi)Z8$H)MWtKseNECh=_PQ=399wlV|qizaA2!q7AU{qIJ#~m zRzmgOpY{MWrHh9oVf>n%Jqz^h`d&N&#Oaf35S)7~Fb1Xtz2Oj`i-2+pj%m=;+egN) zc)Wl?KymxyqN;uWw<};xfkxCHP$y|I$&tq1_jKd^;I$14yQZAM?KnFNFpA7bG_0!S zN-_HE7e%~pbVf@h*#bR8V#WNC~h6^BPJrE6wtkew=fA+ctC7C(T}$k z98sp7SsLevhBfS%R_~OE5H_wSrjidVKTxQIkv3=&w)+bNOgacC5#N#%?cD_XT|OY0 z_aNy9(?&z4U5u$E7MU^MSNg&}Mm1Nr7kq=!gQblC6u!zNDY|XlU18x@Q4UX_z7C<2 zaB8!PW(Z+1M=h9Gv1L6fw4;iBfqKLDmWF9Qkrfv+SKIGl0 zZq91I+x-UK-Tw8Cox@){Mh7(y9XfXV2~IO$tCKv6ZRU8iQ@6svI3WcMJk|SYv!_8l zg+kU8*w9lT%{v>{6a_y)-7C!OcOuqTeD+ir8Zot4DVOg>@;(DK++1Mo1_yWS_=49@ zlbDtO_A1&;Or+?pm_brdBgNt3Zg?KYj<&&N<%X#ryns6^a*zW!tp=@iK)V_#Uq6j{ z!ul<$5o)rL{iVuGYX7}HOBjoU(~$mc%_`c$d&N-fPxV{Y(HPP+tEpz*n_U&bQ3 zbGwt@6huz4*Q`!h_Sd4u3MpXGme0eJEo)Nm<=$(ZXoKIuXHJ?lHgg;GzGXKtdMnDu z=iM$V-wI?cGa`>|J7~;z7QybbW`2-(ZmmINTT5bk@6cnTawJFt&)anJ2mu#RQ(X<+ z>Dmd$ehFQQ0fH0El|L}iG_J=6Pzo3i6Cqfp!Ltw0B{6Yv$Xmza2H9^6vr-KTNgj0z zhS%)$Iytvv-)TtJ0(>XR#f=0nS_^So<_XkCcgfxb2L)InZd$r;{{NXETHdcuD>=6VxzVHOFdId2w{Y3J@|Zi$n`iVHq) z?AE9H!|@3S_hv#TAtMQRh{#e~Nel%H9Hd#AdK9oHOaGR@Dnl`AlcCO?0cjEV9^bUJ ze*`aSU7xI9qiQDSS}iKl1UsWEJ_}3c%ahU;5-p+&ERfNI2HyA)VMvtc`S?vkkr;H~ z!U=ZP>f|xkH@$c4-eQc0JOxGDmDb35K=5>ExIxa^rr?RX=KMjG4-_1(;g%PQNL|ix zq}b$_nL^Re4(4Q{1|xv?_#=!dCi+SQhHTEnF^gusY6~an)!sV7N*BMFHs8?Hz@QuA zKqaOF&>^!8Z?cP8#M5tgzNa(0o?M&|vix&C(>3~gKC@)C8;|{ufabFGv~c)Na=&b= zWmt_)7K^$D(dJ6fF+{b`9rTfzV(<%$Sl@Ym&V0iN3L_gO-3 zFaVevp2*XAyU=_F9S)$sRNg&pYpeB#cDyHlB1HBq0{oDaz}^JuhABS8x_u_Lxs>T` z%_#MiboXQ%(IygCnDwJcvrlbpo9OqVoY!TJ2W|zSO#1;ANVPO0Bhp%H; zC)<(}+;cKMU}WN5`WTSiPm4g5eu;DBLgHzvozm(5KLGRaD#Hmv_1!*N24Pew@p*KVMMoP(DTz(Oz2WhMMXt+3unU2^cs=JcGVLZ z0Z0hl*`N#v$i$+?LzKgt0cv9#dXv4kFDfX?ZvQy^V9u~x|KbexCqS!_aT-YFInxqZ z1PRz}`~B-qdC!Uk`ETj1{Sgp_>UN|7VF@}qfI|roK)Aw3Y_E@Rhw7Ke{@|`zey!R1 zoRAOGod(*mVbQ}Ouz7-qAoH}*AWSEJo`aW#s&}7HBxlTgf7m^z8)~jd`f<>=c4-D6 z`yLznWqz+JiSQ~J-ki+B#Z-q;Ho4l)XA4II#@!J0wVc(pm?P%lOp#JCXo7^r$pK#K z)l$r{)_kkM9)#Z_q)az7ALAW~mcdi|qb%%kZFSuxSA`wlO|8`L{4^zNT)j)}SYf|9 zD!p0~Jx~yQIF0LhgT!=l#wG~5A0vWBplJfUU{ERv(|6;Hj`)t#Y%$S)AItPe=g51u zltRz#@&V|Iz|4x#ta5xU4Xh>hN7I7P83uDTM?#9v4{q46B|7Iyf42MksHR|PX9Xmg zp0xK{#at)dTJKLN-aW$I$q3am@ECGt)`>yW7`)l&`sBg1l$T*(dM{s2q}D|c!6OZ5sgZYeI(%K6E1T0)H@Gl8EQylxL^={d%&)+o$hb7dB z?^vTv@`LZ4MTpAEW)77To}U(f9p#NGTGNM>uwnOVku|MrANHyAyxY^+jDx)!z_0pz zDZ*;@eH28~^12em`_p&Y!kcW2nHE--(Ez3Ka$Ijs>%2s(hu5B+$MqM2d{!OB$rpCi->o(vv*JkW1)a$G4Ig8hv zpB_21+sRgj$Oa#>p8d;|DoJD{&*;JoJ^|vl!Y$);HWjp&wd2yn;f_M(Y@}a^l8VaJ zMb73|m2cPpHVxhB5$M?n-{A1z0H?C<&l?+R6!?gHRiUDJ#%6Sr!-e$@j|Q9==&#k) z*Ls7;0Opw8mPQR>kV=J84@CJT2;=z&1%*o980N}_JTDeCM1U~Kh%E_6F#`kesKOw3 zO>NzrtQ`_RUXg{H(4Y&&C1zq`Z8H}j3Q^vgj^rm2(!&S&3T)f3Z6k%^KTuL#@ngzP zDDxG=?PAM+7o48IH@_ZS6+r8Xlw5XmwrR~YZP!Lr7f8$l9rnrSHHsOXc3;*2Qpnc z4i{mo3ZrO{zm-#aHPzLlf#6L>_;^|+ef&`g*?_&kZM*lqpYOu}(>jl$5j@&#)MXSQ z65t{;bu!xle9BlnJQ;n>n=o}l#ZX<$+<%nkB?5E4p>JTTyY*tUN; zL5fG?tnLj%P4^od*PZ)fxx%i3u@}1K@9sg8^UHudOVj%ecbXNcu+qtI^O73p-j(ln z#Z$HCwPo*=c|(n`1uc+*-g!;fhu2kM?wa359M_l{f=UFCq!VkWT*<;CvV+{P>co9ULpD94;CMtx~bT)UCNmX z6Rl3q@k&=lt8Ct3^QY`7-_HljShWVt z5ANZYsINC{2|Z8Qy1&rr2Pfz-gquPhcqiU)p=M<4&#>75%VmUsmFT;XH@jl-G0fpP zWN_OTKE{E3CAcm;lFpU%hx-SdPbQ51DOrAO9NXkn`MtEMoBOvlZl?BODS;Nd@6LiQ z3pEEv&)CZgEG-*D67w%X%9~aySf(-D7LJ2o<5mi%rXVQk@8{RnqM*grxEx<)*RVs> z^aa8@C3i%fM#yg_Qdz|Y-Wk+{HD(*90;OT9GL*?`rhCzTJ_AeQii$7XHq*8`gB`%V zKJbt9&WaiEy^F$c$$S#=0xGasE_=eKpg;w9=76r{4KqR~H3A_}ui*xghEaay>b4p| z8M>;_thaM5x`1U^oPwz_K(~F_fBeQCz#d?TP^+D(l$rybw3j8Dar-*D7OBeypNX&B zy(21GT{HX%QXL>*tBQ(|;LZc=>eH4Qw(^Q^!^^w@JQ;=wG0D|5idj{M*5dF(+dF!T+uExE%4*>iqxy!Nh}A zMB6XorsLK1j~)pijl$5X)W1^XuVKh|T(>|d8-h&^48JkLZoQ4+Q7yI>y62xB7-7S^ z@!IqIujC@VKoNzuakF^oV1t-6s^>o}Om^mAST@*@eL#3%k|jg)~&%Bx|VUdc;2DJ^-qDXZR&MM?3B?D?Cl*GAR2yu4y%iiFmN;Y0J5{3UxvyJAg!BW$!RO17@P>fizY<%8)eG z*1wuB(+7@3abB6sPX1f^TVIDLA)$4nN4UVRPSPiV=ScUB%lxbW?J(byyFPoI80vWI zQfH-lRH5REM87FC#n_VjRfB_ij`IX?O=jIZ0+HGW&`pP#o~>C5o0tH};jiqqf^pZl zbEuNJ8{OthP;YUVGsCjUxN!>sffp}g4My>h~hb2Z^a{U?1XPea=QRr7XrHUYik1vIf#sD9acwcSoIqGyEh z^q`T_4a&eANE<9f!s`QiyIYZp0P7Tnix2S%v|c72+~fUt6rA?=(@1_;ITKS;V=3cf z;|uKj95?BAU(Toq9k(-0;c*xkgOLCR96t;jg+aMe5wsmKxovog`QK+4Wd)q$7y2Av zJ#~|l8P&QD=1#7*%&%||z+_i9cPbcNFC-7zLo{J12F8mA$`}we0(#@#f}JVJ|DRl< zV~ZWhznVNX$l5#lPDlgm3aR*?I5<^>T~gODj&91uCA7pI)!O!4`q3&E$BdRn;VhR) zw1_iEjZkFHT9gJA_kem_IL5!b$ncJpOnK?8K%Pstt1OH+Ha7<~ipoR6A?5j%d8qWm zhee3r+S%f)adf_JC8i(`PdbnXNz|t6;dHDSuY#a$$Ud7$LuAwG2`lN;kS?VNfH?GCNBhb*NYC>(_-y4a46Dp}2%iu0Q zyNIv^yBPQ+^KJW5t*Aq^Wb!QzGC{n6ZeR(b1||O>hn@Ym0L+5^U!alUE%cTvYW#yC zb7p%qJ>b7V#=`3zzv;s|2%8!v78?}Si8+b?&1RWS6wMaO_~+bdU+JD90l{0KDPb;2 zN$CwW!yec>MwHr??0hoAn}FBR|Aj3ynLH%z>l7rY=O}qJDAJ($tESehaLSWu4pVYTaFjybEWhnaj_6*-??< zD@YiipkdNwvbgHEB)XxkG`qhu(vH`CR@}AX?97+F3kPNqZGo&`J`URP9w%}Il( z)2yWQo#KHSSm8;M2?fv|5zb|!C@QKC%aE(%BQU~s+FPLHAFsZvw)6pWgYUcQF0S$< z4va$?_l&bQukjPo=lgB%fglAJ%F(<)w(l3pq8fsGsrB{OCo(9joLr~}f%3DOuj2Br zA@2;h{nhzy$&HP>&*0sqc0}V9yhnni$(^RA*YH3cH!X9ydkMhi**R>u^8=7c#}E{195_Cs3x7t_qt zL(T`u{vO4Qcm;HQh!Rx3y8v3+O%nbu`=bS*sunP~qw6aI_KG;`%z~2n~w80}g(1UA- z6$bv#Je6V8aNm93pF_kuCY0n;JnSh5!>Gal(E4))`hO)9OJ2xUQ zQk#dL@7CJeWB{5VUZuWaqmF%C8wfdAG@d}rYj_4ny5`@Suu}&c!|Iylj37AK+U9o#(oVQN+Cl#ogG_@tZ$I zNkeK?a8y|e2r4P{*uFZ9x769I4(;gFuBZ3*YV)99g_0a59P=R~NiZlPeOt@N_PMS*E^gIRr?I!w7&^sYmYHE|Pdhvmsz8QU zXWXe4yQ`5#-K}qp|DwTRH0E#3tZo)n#PIUk4ypYqG@1-%i!1nVUA)2zcK-3xE8L}H zolyhh4D|GuT&oZGU2Ka8$naVt$E&v%f88*dq9Er{zm(=7NX}bj&aRW z1y_n*EY|4Ma(CuOl|8aqspZ+IEp?Kj<{k9!1F**&R$D+QmM$^Gmo zVKFkuAtKABy-7INlg&Tun)>aaoVnCOtU`Ql);;6Hhsm|P0miMFhVY{O;(Zr-ck@s- z_kGUZNWLB+8G`T>rqBst*|s9RiDym@rhVCCxkdAm+M{MluNN5~>gsw@@Zg*}WgT!& zCFnp2Awct*n!@TZ4Fg|^_<11*0^X8c(Uz7L(zXm%9hu0P^wQZ+WiHIAPC28Ia{T+> zGF;N{Z}oYFAJ0g2xSVJ9nNYjsJgO*Cx_z} z;|ThP{VfDV6}Cc!qmMG`wbC!uZv-xh5U$)Jvz5MJvAZ;QWwb4$x3u3brDJy~#+$RW zaK4^WgL{9-b#}D!#BBpY{OAA%MbevzjpIy_&WpozaS1lFiz6f60`oD_9G)bc4_5-E zH)gNap;r2RAI*D3C(gW}AmCF;-$Wd-7-ueU_~!4;Y0S`5WHXaH$6Vrvsx@&dP|=DB zv!9xpVlK@#)!6SnS?WxCf5{Ucns!RfS_7S+iLF1^Y5sZKIXVIVNo2TrQn&^-i9^+w zUCzP&tc1mpGohyWTMgyq(OIfFsq#fi=Y{M_3UwcM*jlf7a1L(ItSz86Bm0YRter~t zGpHIi=N>sRZ%xyZh&8sGJwh6X^y-f#6OS|7-)w$Zr9xAl;R>hCd21{PO@8o6Aon2a zLFYtmq$ZzDS<7YgC?}!CVVdOiqy$0P)y)!kfawGQMue(`}+-Er%S$^EVJV@NqO}^nQwd@#@l;tE5oIJ*PY63@@Jllh{S zlF=%7=bI+CptW_!duP50fzsWt!!hm&{(Xt|4+bMvh_0&z9a!fT*NTHbqAlkwEozRa zm&)nmRP?@bV8l&><mt0Hr%`Wj-%-}my%J>8yEL6p)x7Gjy`>$8^oqbK#o&`VNP^Tfo`B=%y1 zemR%o(WuDpY!;?t;sM+0ZDT$t$>I2L zdG!2ZB(BqDvt6rR!__TzX0kryK>1_YBqQ94v(78tT$|^wzdv895X}>2?uGT50oD47 z>%*;3VHL3jrjf??* z$>$gfTPux|Foo{TCR0max6dAB8PrMVFuZ>-Zx*vL*K6~6l1DtUiclm>@zSDrSK7*P z8?TZi)9@a^^c8rLNk!O-EJTFA-Mq6@p>q56*CZ{QN|$rawo)lqN{iIK4PPBW7Vjss zHtPF^2dI>6FG-S)okkl@zV*58#${g&?moihe}O}<$QfM*iC9C9D7H}`z7y@UfJ>f6HZGNr9qw7xC+K!6Y`Ni zU3lfr#ZElrpx^A5z+v3i7h{w_>uG^Yd26wUY~fz$(hj{lx0Sv_c7jszBl|_4Y}B4Z zRldVK^R;W!17k-BPx&Aa=P&y77j4u9qjef~Y2xT`t7pvCgP9l&MVBaAUEHo1_ve;4 z1s$$HNl8nit-BtN2~a+D#gct``{n7cwbj+OhGQ0F9H#tF7H^LTpE)tQM$Y@eM#!5n z@cXy>8ofl~vKao22iS3kjw!Mz*Mkjm$Uy=luHyUS!yhivQv*fr zwhTrlefr)N<1f8~jZsve6MX-FXfWneEb;&SAO80!{r|r-*0chS-hHzd self.max_context: + raise ValueError( + "Prompt and complete diffusion blocks exceed the cache capacity." + ) + self.engine.reset_cache(self.cache_config) + start = time.perf_counter() + if max_new_tokens == 0: + return CloverResult("", [], [], time.perf_counter() - start) + self._forward(prompt_ids, 0) + output, iterations = [], [] + frontier = FrontierPolicy(confidence_floor=frontier_confidence_floor) + eos = set(self.engine.eos_token_id) + for _ in range(blocks): + ids = torch.full( + (self.block_size,), self.mask_id, device="cuda:0", dtype=torch.int64 + ) + for iteration in range(self.block_size): + result = self._forward( + ids.cpu().tolist(), + len(prompt_ids) + len(output), + preview=True, + expert_budget=expert_budget, + ) + logits = self._logits(result["logits"]) + logits[:, self.mask_id] = -float("inf") + confidence, proposed = logits.softmax(-1).max(-1) + masked = ids == self.mask_id + if policy == "frontier": + selected = frontier.select( + confidence[None], masked[None], iteration + )[0] + else: + selected = masked & (confidence >= threshold) + if not bool(selected.any()): + selected[confidence.masked_fill(~masked, -1).argmax()] = True + ids = torch.where(selected, proposed, ids) + if not bool((ids == self.mask_id).any()): + break + resolved = ids.cpu().tolist() + if self.mask_id in resolved: + raise RuntimeError("An unresolved diffusion block cannot be committed.") + self._forward(resolved, len(prompt_ids) + len(output)) + iterations.append(iteration + 1) + remaining = max_new_tokens - len(output) + for token in resolved[:remaining]: + if not ignore_eos and token in eos: + return CloverResult( + self.tokenizer.decode(output, skip_special_tokens=True), + output, + iterations, + time.perf_counter() - start, + ) + output.append(token) + return CloverResult( + self.tokenizer.decode(output, skip_special_tokens=True), + output, + iterations, + time.perf_counter() - start, + ) diff --git a/python/infinilm/diffusion/frontier.py b/python/infinilm/diffusion/frontier.py new file mode 100644 index 000000000..d1af1a938 --- /dev/null +++ b/python/infinilm/diffusion/frontier.py @@ -0,0 +1,43 @@ +"""Frontier-guided denoising adapted from Ringssss/dLLM-serve (Apache-2.0).""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class FrontierPolicy: + """Choose confident positions with a bounded, adaptive target stride.""" + + target_iterations: int = 8 + min_unmask: int = 2 + max_unmask: int = 16 + confidence_floor: float = 0.9 + confident_threshold: float = 0.95 + + def __post_init__(self): + if not 1 <= self.min_unmask <= self.max_unmask <= 32: + raise ValueError("Unmask bounds must satisfy 1 <= min <= max <= 32.") + if self.target_iterations < 1 or not ( + 0 <= self.confidence_floor <= self.confident_threshold <= 1 + ): + raise ValueError("Invalid target iterations or confidence floor.") + + def select(self, confidence, masked, iteration): + import torch + + remaining = masked.sum(-1) + rounds = max(1, self.target_iterations - iteration) + stride = torch.div(remaining + rounds - 1, rounds, rounding_mode="floor") + stride = torch.minimum( + stride.clamp(self.min_unmask, self.max_unmask), remaining + ) + scores = confidence.masked_fill(~masked, -float("inf")) + order = scores.argsort(dim=-1, descending=True, stable=True) + ranks = torch.arange(masked.shape[-1], device=masked.device) + take = ranks < stride[..., None] + transfer = torch.zeros_like(masked).scatter(-1, order, take) + transfer &= masked & (confidence >= self.confidence_floor) + transfer |= masked & (confidence >= self.confident_threshold) + stalled = masked.any(-1) & ~transfer.any(-1) + best = scores.argmax(-1, keepdim=True) + transfer.scatter_(-1, best, transfer.gather(-1, best) | stalled[..., None]) + return transfer diff --git a/python/infinilm/infer_engine.py b/python/infinilm/infer_engine.py index 67cc645ca..05a8c29f8 100644 --- a/python/infinilm/infer_engine.py +++ b/python/infinilm/infer_engine.py @@ -338,6 +338,8 @@ def _build_input( visual_token_ranges=None, target_hidden_states=None, sample_all_positions=False, + diffusion_preview=False, + diffusion_expert_budget=False, temperature=None, top_k=None, top_p=None, @@ -399,6 +401,8 @@ def convert_tensor_list(tensor_list_): visual_token_ranges=visual_token_ranges, target_hidden_states=target_hidden_states, sample_all_positions=sample_all_positions, + diffusion_preview=diffusion_preview, + diffusion_expert_budget=diffusion_expert_budget, temperature=temperature, top_k=top_k, top_p=top_p, @@ -534,6 +538,10 @@ def forward_raw( visual_token_ranges=None, target_hidden_states=None, sample_all_positions=True, + diffusion_preview=False, + diffusion_expert_budget=False, + mamba_init_state_indices=None, + mamba_final_state_indices=None, temperature=None, top_k=None, top_p=None, @@ -557,6 +565,10 @@ def forward_raw( visual_token_ranges=visual_token_ranges, target_hidden_states=target_hidden_states, sample_all_positions=sample_all_positions, + diffusion_preview=diffusion_preview, + diffusion_expert_budget=diffusion_expert_budget, + mamba_init_state_indices=mamba_init_state_indices, + mamba_final_state_indices=mamba_final_state_indices, temperature=temperature, top_k=top_k, top_p=top_p, diff --git a/python/infinilm/licenses/dLLM-serve/LICENSE b/python/infinilm/licenses/dLLM-serve/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/python/infinilm/licenses/dLLM-serve/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/python/infinilm/licenses/dLLM-serve/NOTICE b/python/infinilm/licenses/dLLM-serve/NOTICE new file mode 100644 index 000000000..8f60423ca --- /dev/null +++ b/python/infinilm/licenses/dLLM-serve/NOTICE @@ -0,0 +1,4 @@ +Frontier-guided stride control is adapted from Ringssss/dLLM-serve, commit +1da50a96c9ce6b888d245d378f2bac0425feba8c, cwsrpt_v2/algorithm/cw_srpt_v3.py. +The adaptation preserves confident-token progress and uses a configurable +confidence floor. Original source is distributed under Apache-2.0. diff --git a/python/infinilm/modeling_utils.py b/python/infinilm/modeling_utils.py index 114aae846..b1916c66d 100644 --- a/python/infinilm/modeling_utils.py +++ b/python/infinilm/modeling_utils.py @@ -328,6 +328,7 @@ def load_model_state_dict_by_file( _remap_qwen3_next, _remap_qwen3_5, _remap_qwen3_5_moe, + _remap_clover_moe, _remap_kimi_k3, ) or ( @@ -1328,6 +1329,12 @@ def _remap_kimi_k3(state_dict, config): return state_dict +def _remap_clover_moe(state_dict, config, *, defer_tie=False): + """Load Clover's Qwen weights while retaining every diffusion parameter.""" + wrapped = dict(config, text_config=config) + return _remap_qwen3_5_moe(state_dict, wrapped, defer_tie=defer_tie) + + _WEIGHT_REMAPPER = { "glm4": _remap_glm4, "chatglm": _remap_chatglm, @@ -1337,6 +1344,7 @@ def _remap_kimi_k3(state_dict, config): "videonsa": _remap_videonsa, "qwen3_5": _remap_qwen3_5, "qwen3_5_moe": _remap_qwen3_5_moe, + "clover_moe": _remap_clover_moe, "qwen3_next": _remap_qwen3_next, "kimi_k3": _remap_kimi_k3, } diff --git a/python/infinilm/server/clover_server.py b/python/infinilm/server/clover_server.py new file mode 100644 index 000000000..338e1bf25 --- /dev/null +++ b/python/infinilm/server/clover_server.py @@ -0,0 +1,146 @@ +"""OpenAI-compatible serial serving for the native Clover diffusion engine.""" + +import argparse +import asyncio +import time +import uuid + +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field + +from infinilm.diffusion.clover import CloverGenerator + + +class ChatRequest(BaseModel): + model: str = "clover" + messages: list[dict] + max_tokens: int = Field(default=128, ge=0) + temperature: float = 0 + stream: bool = False + diffusion_policy: str | None = None + expert_budget: bool | None = None + frontier_confidence_floor: float | None = Field(default=None, ge=0, le=1) + presence_penalty: float = 0 + frequency_penalty: float = 0 + repetition_penalty: float = 1 + chat_template_kwargs: dict = Field(default_factory=dict) + + +def create_app( + generator, *, policy="baseline", expert_budget=False, frontier_floor=0.9 +): + """Serialize requests because each worker owns one recurrent state slot.""" + lock = asyncio.Lock() + + app = FastAPI() + + @app.get("/health") + def health(): + return {"status": "ok"} + + @app.get("/v1/models") + def models(): + return {"object": "list", "data": [{"id": "clover", "object": "model"}]} + + @app.post("/v1/chat/completions") + async def chat(request: ChatRequest): + if ( + request.presence_penalty != 0 + or request.frequency_penalty != 0 + or request.repetition_penalty != 1 + or request.chat_template_kwargs.get("enable_thinking", False) + ): + raise HTTPException( + 400, "This endpoint requires neutral penalties and thinking disabled." + ) + if request.stream or request.temperature != 0: + raise HTTPException( + 400, "Clover currently supports non-streaming greedy requests." + ) + if any( + message.get("role") not in ("system", "user", "assistant") + or not isinstance(message.get("content"), str) + for message in request.messages + ): + raise HTTPException( + 400, "Messages require a supported role and text content." + ) + if not request.messages: + raise HTTPException(400, "At least one message is required.") + async with lock: + try: + result = await asyncio.to_thread( + generator.generate, + request.messages, + max_new_tokens=request.max_tokens, + policy=request.diffusion_policy or policy, + frontier_confidence_floor=frontier_floor + if request.frontier_confidence_floor is None + else request.frontier_confidence_floor, + expert_budget=expert_budget + if request.expert_budget is None + else request.expert_budget, + ) + except ValueError as exc: + raise HTTPException(400, str(exc)) from exc + prompt = generator.tokenizer.apply_chat_template( + request.messages, + tokenize=False, + add_generation_prompt=True, + enable_thinking=False, + ) + prompt_count = len( + generator.tokenizer(prompt, add_special_tokens=False)["input_ids"] + ) + output_count = len(result.token_ids) + return { + "id": "chatcmpl-" + uuid.uuid4().hex, + "object": "chat.completion", + "created": int(time.time()), + "model": request.model, + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": result.text}, + "finish_reason": "length" + if output_count >= request.max_tokens + else "stop", + } + ], + "usage": { + "prompt_tokens": prompt_count, + "completion_tokens": output_count, + "total_tokens": prompt_count + output_count, + }, + } + + return app + + +def main(): + import uvicorn + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--model", required=True) + parser.add_argument("--tp", type=int, default=2) + parser.add_argument("--max-context", type=int, default=4096) + parser.add_argument( + "--policy", choices=("baseline", "frontier"), default="baseline" + ) + parser.add_argument("--expert-budget", action="store_true") + parser.add_argument("--frontier-floor", type=float, default=0.9) + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=8000) + args = parser.parse_args() + generator = CloverGenerator(args.model, tp=args.tp, max_context=args.max_context) + app = create_app( + generator, + policy=args.policy, + expert_budget=args.expert_budget, + frontier_floor=args.frontier_floor, + ) + uvicorn.run(app, host=args.host, port=args.port) + + +if __name__ == "__main__": + main() diff --git a/test/diffusion/test_clover_loading.py b/test/diffusion/test_clover_loading.py new file mode 100644 index 000000000..e7d5d2772 --- /dev/null +++ b/test/diffusion/test_clover_loading.py @@ -0,0 +1,38 @@ +"""Protect the text-only Clover checkpoint layout and learned preview weights.""" + +import numpy as np +from infinilm import _core as core +from infinilm.modeling_utils import _remap_clover_moe + + +def test_clover_remap_preserves_preview_weights_and_splits_qkv(): + config = { + "linear_key_head_dim": 2, + "linear_num_key_heads": 1, + "num_experts": 2, + "moe_intermediate_size": 2, + } + prefix = "model.layers.0.linear_attn." + norm = core.from_list([0.25, -0.5], dtype=core.float32) + scalar = core.from_list([5.0, 6.0], dtype=core.float32) + packed = core.from_numpy(np.arange(24, dtype=np.float32).reshape(6, 4)) + output = _remap_clover_moe( + { + "model.norm.weight": norm, + prefix + "norm.weight": norm, + prefix + "bd_softmax_history_bias": scalar, + prefix + "in_proj_qkv.weight": packed, + }, + config, + ) + np.testing.assert_array_equal(output["model.norm.weight"].to_numpy(), [1.25, 0.5]) + np.testing.assert_array_equal( + output[prefix + "norm.weight"].to_numpy(), [0.25, -0.5] + ) + assert output[prefix + "bd_softmax_history_bias"] is scalar + assert prefix + "in_proj_qkv.weight" not in output + for part, begin in (("q", 0), ("k", 2), ("v", 4)): + np.testing.assert_array_equal( + output[prefix + f"in_proj_{part}.weight"].to_numpy(), + packed.to_numpy()[begin : begin + 2], + ) diff --git a/test/diffusion/test_frontier.py b/test/diffusion/test_frontier.py new file mode 100644 index 000000000..665d17f4c --- /dev/null +++ b/test/diffusion/test_frontier.py @@ -0,0 +1,35 @@ +"""Exercise progress, resolved-token preservation and high-confidence bypass.""" + +import importlib.util +import sys +from pathlib import Path + +import torch + +spec = importlib.util.spec_from_file_location( + "clover_frontier", + Path(__file__).parents[2] / "python/infinilm/diffusion/frontier.py", +) +module = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = module +spec.loader.exec_module(module) + + +def test_confident_tokens_are_not_delayed_by_stride(): + policy = module.FrontierPolicy() + confidence = torch.ones(2, 32) + masked = torch.ones(2, 32, dtype=torch.bool) + masked[1, :7] = False + assert torch.equal(policy.select(confidence, masked, 0), masked) + + +def test_stalled_rows_progress_without_touching_finished_rows(): + policy = module.FrontierPolicy() + confidence = torch.full((3, 32), 0.1) + masked = torch.zeros(3, 32, dtype=torch.bool) + masked[0, 5:] = True + masked[1, 17] = True + selected = policy.select(confidence, masked, 3) + assert selected.sum(-1).tolist() == [1, 1, 0] + assert not (selected & ~masked).any() + assert selected[0, 5] and selected[1, 17] diff --git a/test/diffusion/workload.json b/test/diffusion/workload.json new file mode 100644 index 000000000..883b3870c --- /dev/null +++ b/test/diffusion/workload.json @@ -0,0 +1,194 @@ +[ + { + "id": 0, + "prompt": "What is 2 + 2? Reply with only the number.", + "max_tokens": 32, + "check": "4" + }, + { + "id": 1, + "prompt": "What is the capital of France? Answer in one sentence.", + "max_tokens": 64, + "check": "Paris" + }, + { + "id": 2, + "prompt": "What is the capital of Germany? Answer briefly.", + "max_tokens": 64, + "check": "Berlin" + }, + { + "id": 3, + "prompt": "Sort these numbers in ascending order: 3, 1, 2. Reply only with a JSON array.", + "max_tokens": 64, + "check": "[1, 2, 3]" + }, + { + "id": 4, + "prompt": "Return only this JSON object: {\"ok\": true, \"count\": 3}", + "max_tokens": 64, + "check": "json" + }, + { + "id": 5, + "prompt": "Translate \"good morning\" into Chinese. Answer briefly.", + "max_tokens": 64, + "check": "早" + }, + { + "id": 6, + "prompt": "Reverse the string abc. Output only the reversed string.", + "max_tokens": 32, + "check": "cba" + }, + { + "id": 7, + "prompt": "What is 6 multiplied by 7? Answer with only the number.", + "max_tokens": 32, + "check": "42" + }, + { + "id": 8, + "prompt": "Write a Python function add(a, b) that returns their sum. Return only the function.", + "max_tokens": 128, + "check": "code" + }, + { + "id": 9, + "prompt": "Write a Python function square(x) that returns x squared. Return only the function.", + "max_tokens": 128, + "check": "code" + }, + { + "id": 10, + "prompt": "用一句话解释什么是机器学习。", + "max_tokens": 128, + "check": null + }, + { + "id": 11, + "prompt": "Explain photosynthesis in two short sentences.", + "max_tokens": 128, + "check": null + }, + { + "id": 12, + "prompt": "List three primary colors of light, with no explanation.", + "max_tokens": 64, + "check": "red" + }, + { + "id": 13, + "prompt": "Give two benefits of unit testing in software.", + "max_tokens": 128, + "check": null + }, + { + "id": 14, + "prompt": "用两句话解释为什么需要备份数据。", + "max_tokens": 128, + "check": null + }, + { + "id": 15, + "prompt": "Write a polite one-sentence message declining a meeting invitation.", + "max_tokens": 128, + "check": null + }, + { + "id": 16, + "prompt": "Summarize this in one sentence: Bees transfer pollen between flowers. This helps many plants produce fruit and seeds.", + "max_tokens": 128, + "check": null + }, + { + "id": 17, + "prompt": "What is the opposite of hot? Reply with one word.", + "max_tokens": 32, + "check": "cold" + }, + { + "id": 18, + "prompt": "Return a JSON object with name Ada and age 30. No explanation.", + "max_tokens": 64, + "check": "json" + }, + { + "id": 19, + "prompt": "Complete the sequence: 2, 4, 6, 8, __. Reply with only the next number.", + "max_tokens": 32, + "check": "10" + }, + { + "id": 20, + "prompt": "Write a Python function is_even(n) that returns whether n is even. Return only code.", + "max_tokens": 128, + "check": "code" + }, + { + "id": 21, + "prompt": "Explain the difference between a list and a tuple in Python in two sentences.", + "max_tokens": 128, + "check": null + }, + { + "id": 22, + "prompt": "请用一句话解释什么是开源软件。", + "max_tokens": 128, + "check": null + }, + { + "id": 23, + "prompt": "计算:一个盒子有12个苹果,拿走5个,再放入8个,还剩多少个?", + "max_tokens": 64, + "check": "15" + }, + { + "id": 24, + "prompt": "What is the largest planet in our solar system? Answer briefly.", + "max_tokens": 64, + "check": "Jupiter" + }, + { + "id": 25, + "prompt": "Convert 100 centimeters to meters. Answer briefly.", + "max_tokens": 32, + "check": "1" + }, + { + "id": 26, + "prompt": "Write a short definition of a database index.", + "max_tokens": 128, + "check": null + }, + { + "id": 27, + "prompt": "Translate \"Thank you for your help\" into Chinese.", + "max_tokens": 64, + "check": "帮助" + }, + { + "id": 28, + "prompt": "What is 10 minus 3? Reply with only the number.", + "max_tokens": 32, + "check": "7" + }, + { + "id": 29, + "prompt": "Name the chemical formula of water. Reply with only the formula.", + "max_tokens": 32, + "check": "H2O" + }, + { + "id": 30, + "prompt": "Write a Python function that returns the nth Fibonacci number.", + "max_tokens": 192, + "check": "code" + }, + { + "id": 31, + "prompt": "Describe how to make tea in three short steps.", + "max_tokens": 128, + "check": null + } +] diff --git a/third_party/InfiniCore b/third_party/InfiniCore index 2b4f2f205..6446bc6b9 160000 --- a/third_party/InfiniCore +++ b/third_party/InfiniCore @@ -1 +1 @@ -Subproject commit 2b4f2f20584a69b074f62a455e3de7368e3446c2 +Subproject commit 6446bc6b9e341038ba13da362c7aecd5fdbeebee