From af8b577b972b9beea2a72e6324dbb6867c7f8d69 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:17:52 -0700 Subject: [PATCH 01/14] Fix --build_wasm_static_lib implicitly enable --build_wasm (#27342) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description Moves the `--build_wasm_static_lib → --build_wasm` implication from `build.py` into `build_args.py`'s post-processing, **before** the cmake generator selection. Previously, `build_args.py` chose the generator based on `args.build_wasm` (still `False`), and `build.py` only set it to `True` afterwards—too late. - **`tools/ci_build/build_args.py`**: Set `args.build_wasm = True` when `args.build_wasm_static_lib` is set, prior to generator and cross-compilation logic. - **`tools/ci_build/build.py`**: Remove the now-redundant identical check. ### Motivation and Context Using `--build_wasm_static_lib` without `--build_wasm` caused cmake to use the wrong generator (e.g., Visual Studio instead of Ninja on Windows) and miss Emscripten-specific configuration, leading to build failures like missing `libiconv`. --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fs-eire <7679871+fs-eire@users.noreply.github.com> --- tools/ci_build/build.py | 3 --- tools/ci_build/build_args.py | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/ci_build/build.py b/tools/ci_build/build.py index df59dd1049dc6..54c8d412c02c6 100644 --- a/tools/ci_build/build.py +++ b/tools/ci_build/build.py @@ -2327,9 +2327,6 @@ def main(): if args.nnapi_min_api < 27: raise BuildError("--nnapi_min_api should be 27+") - if args.build_wasm_static_lib: - args.build_wasm = True - if args.build_wasm: if not args.disable_wasm_exception_catching and args.disable_exceptions: # When '--disable_exceptions' is set, we set '--disable_wasm_exception_catching' as well diff --git a/tools/ci_build/build_args.py b/tools/ci_build/build_args.py index 61b91f37eac19..c7d66362da51b 100644 --- a/tools/ci_build/build_args.py +++ b/tools/ci_build/build_args.py @@ -938,6 +938,10 @@ def convert_arg_line_to_args(self, arg_line: str) -> list[str]: # Use list[str] if args.android_ndk_path: args.android_ndk_path = os.path.normpath(args.android_ndk_path) + # Treat --build_wasm_static_lib as implying --build_wasm + if args.build_wasm_static_lib: + args.build_wasm = True + # Handle WASM exception logic if args.enable_wasm_api_exception_catching: args.disable_wasm_exception_catching = True # Catching at API level implies disabling broader catching From 36242c6c1afebeca32219e61fac354790b736602 Mon Sep 17 00:00:00 2001 From: Jambay Kinley Date: Wed, 25 Mar 2026 16:59:57 -0700 Subject: [PATCH 02/14] Route fp16 HQNBIT_CompInt8 (4-bit and 8-bit) through fp32 MLAS path in MatMulNBits (#27820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description Routes fp16 `HQNBIT_CompInt8` through the fp32 MLAS path (`SQNBIT_CompInt8`) at the operator level for both 4-bit and 8-bit MatMulNBits, then removes the ~370 lines of dead HQ CompInt8 wrapper code from MLAS. **Operator changes (matmul_nbits.cc):** - PrePack: Uses `SQNBIT_CompInt8` for sizing/packing, pre-converts fp16 scales and bias to fp32, computes BZpCorr for asymmetric KleidiAI on ARM64. - ComputeBPacked: Bulk fp16→fp32 conversion of A, calls `MlasQNBitGemmBatch` with `SQNBIT_CompInt8`, bulk fp32→fp16 conversion of C. **MLAS cleanup (qnbitgemm.cpp, qnbitgemm_kernel_neon.cpp):** - Removed `HQ4BitGemm_CompInt8`, `HQ8BitGemm_CompInt8`, `HQ8BitCompInt8PerGemmWorkspace`, associated enum values, dispatch branches, workspace entries, and `HQNBIT_CompInt8` NEON kernel conditions. - Added `HQNBIT_CompInt8` → `SQNBIT_CompInt8` redirect in `MlasIsQNBitGemmAvailable` for `GetComputeType` compatibility. ### Motivation and Context The HQ CompInt8 kernels are wrappers that convert fp16→fp32 per-tile before calling the same SQ fp32 kernels. This change: 1. **Eliminates per-tile overhead** via bulk conversion at the operator level. 2. **Enables KleidiAI for fp16 4-bit** — previously bypassed by the `HQNBIT_CompInt8` path. 3. **Removes ~370 lines of dead wrapper code** from MLAS. ### Improvements Measured on `Snapdragon X Elite - X1E78100 - Qualcomm Oryon CPU` **Asymmetric:** | Model | Seq Len | Acc1/Acc4 (before) | Acc1/Acc4 (after) | Acc4 speedup | Acc4 latency (after) | |-------|---------|-------------------|------------------|--------------|----------------------| | Qwen 1.5B | 256 | 1.28× | 1.55× | **1.26×** | 1187.5ms | | Qwen 1.5B | 512 | 1.14× | 1.63× | **1.55×** | 2257.2ms | | Qwen 3B | 256 | 1.32× | 1.82× | **1.29×** | 2351.3ms | | Qwen 3B | 512 | 1.38× | 1.70× | **1.28×** | 4777.2ms | | Qwen 7B | 256 | 1.58× | 2.26× | **1.40×** | 4094.5ms | | Qwen 7B | 512 | 1.49× | 2.23× | **1.52×** | 8002.6ms | **Symmetric:** | Model | Seq Len | Acc1/Acc4 (before) | Acc1/Acc4 (after) | Acc4 speedup | Acc4 latency (after) | |-------|---------|-------------------|------------------|--------------|----------------------| | Qwen 1.5B | 256 | 0.95× | 1.45× | **1.67×** | 1255.5ms | | Qwen 1.5B | 512 | 1.04× | 1.52× | **1.55×** | 2406.7ms | | Qwen 3B | 256 | 1.39× | 1.88× | **1.32×** | 2215.0ms | | Qwen 3B | 512 | 1.42× | 1.85× | **1.31×** | 4318.3ms | | Qwen 7B | 256 | 1.66× | 2.58× | **1.55×** | 3564.4ms | | Qwen 7B | 512 | 1.57× | 2.60× | **1.64×** | 7227.9ms | **NOTE**: The 8-bit accuracy level 4 path shows some regression (5–25% on 1.5B/3B models, neutral on 7B) due to the bulk fp16↔fp32 conversion overhead replacing the old per-tile approach. The old HQ CompInt8 wrappers kept small tiles cache-hot, while the new unified path does full-matrix conversion passes. This trade-off is acceptable since 4-bit is the dominant quantization format (gaining 26–67%), 8-bit acc4 still outperforms acc1 by 1.7–2.2×, and the regression is most pronounced at smaller model sizes where absolute latencies are already low. A proper fix would be 8-bit KleidiAI-style kernels rather than restoring the wrapper code. --- .../cpu/quantization/matmul_nbits.cc | 207 ++++++++-- onnxruntime/core/mlas/lib/qnbitgemm.cpp | 372 +----------------- .../core/mlas/lib/qnbitgemm_kernel_neon.cpp | 13 +- 3 files changed, 198 insertions(+), 394 deletions(-) diff --git a/onnxruntime/contrib_ops/cpu/quantization/matmul_nbits.cc b/onnxruntime/contrib_ops/cpu/quantization/matmul_nbits.cc index cb7cfbb4fb97a..d2996b122c5f7 100644 --- a/onnxruntime/contrib_ops/cpu/quantization/matmul_nbits.cc +++ b/onnxruntime/contrib_ops/cpu/quantization/matmul_nbits.cc @@ -69,7 +69,7 @@ GetComputeType(size_t nbits, size_t block_size, int64_t accuracy_leve // By converting Fp16 to Fp32, there is not precision increase, and the performance // becomes worse. if (accuracy_level_attr == static_cast(Level4) && - MlasIsQNBitGemmAvailable(nbits, block_size, HQNBIT_CompInt8)) { + MlasIsQNBitGemmAvailable(nbits, block_size, SQNBIT_CompInt8)) { return HQNBIT_CompInt8; } @@ -258,20 +258,50 @@ Status MatMulNBits::PrePack(const Tensor& tensor, int input_idx, /*out*/ All prepacked_weights->buffer_sizes_.push_back(packed_b_size_); } } else { - packed_b_size_ = MlasQNBitGemmPackQuantBDataSize(N_, K_, nbits_, block_size_, has_zp_input_, compute_type_, &mlas_backend_kernel_selector_config_); + // For HQNBIT_CompInt8, route through SQNBIT_CompInt8 for sizing and packing. + // This gets KleidiAI-sized buffer when available for 4-bit and packs B+scales correctly. + const auto effective_compute_type = (compute_type_ == HQNBIT_CompInt8) + ? SQNBIT_CompInt8 + : compute_type_; + + packed_b_size_ = MlasQNBitGemmPackQuantBDataSize(N_, K_, nbits_, block_size_, has_zp_input_, effective_compute_type, &mlas_backend_kernel_selector_config_); if (packed_b_size_ == 0) { return Status::OK(); } + auto qptr = tensor.DataRaw(); - // For HQNBIT compute types, scales are fp16 and cannot be passed directly - // to packing functions that expect float*. Pass nullptr here; scales will - // be properly converted and packed in a subsequent PrePack call. - auto scale_ptr = (scales && compute_type_ != HQNBIT_CompInt8 && compute_type_ != HQNBIT_CompFp16) - ? scales->DataRaw() - : nullptr; + const void* scale_ptr = nullptr; + + // For HQNBIT_CompInt8: convert constant fp16 scales to fp32 for packing. + // KleidiAI bakes scales into packed B for 4-bit; 8-bit needs fp32 scales for SQ8BitGemmPackQuantBDataAndBlkSum. + if (compute_type_ == HQNBIT_CompInt8 && scales) { + auto sptr_fp16 = scales->Data(); + auto scales_size = static_cast(scales->Shape().Size()); + scales_fp32_ = IAllocator::MakeUniquePtr(alloc, scales_size, true); + MlasConvertHalfToFloatBuffer(sptr_fp16, scales_fp32_.get(), scales_size); + scale_ptr = scales_fp32_.get(); + } else if (scales && compute_type_ != HQNBIT_CompInt8 && compute_type_ != HQNBIT_CompFp16) { + // For non-HQNBIT compute types, scales are already float. + scale_ptr = scales->DataRaw(); + } + packed_b_ = IAllocator::MakeUniquePtr(alloc, packed_b_size_, true); - MlasQNBitGemmPackQuantBData(N_, K_, nbits_, block_size_, compute_type_, qptr, packed_b_.get(), scale_ptr, + MlasQNBitGemmPackQuantBData(N_, K_, nbits_, block_size_, effective_compute_type, qptr, packed_b_.get(), scale_ptr, has_zp_input_, nullptr, threadpool_ptr, &mlas_backend_kernel_selector_config_); + +#if defined(MLAS_TARGET_ARM64) + // For KleidiAI asymmetric 4-bit path: compute BZpCorr now while scales and zero_points are accessible. + if (compute_type_ == HQNBIT_CompInt8 && nbits_ == 4 && has_zp_input_ && scales_fp32_ && + MlasQNBitGemmScalesPacked(K_, nbits_, block_size_, SQNBIT_CompInt8, has_zp_input_, &mlas_backend_kernel_selector_config_)) { + const Tensor* zp_tensor = nullptr; + OpKernel::Info().TryGetConstantInput(InputIndex::zero_points, &zp_tensor); + if (zp_tensor != nullptr) { + auto zptr = zp_tensor->Data(); + MlasQNBitGemmPackQuantBData(N_, K_, nbits_, block_size_, SQNBIT_CompInt8, nullptr, packed_b_.get(), + scales_fp32_.get(), has_zp_input_, zptr, nullptr, &mlas_backend_kernel_selector_config_); + } + } +#endif // MLAS_TARGET_ARM64 } is_packed = true; } else if (compute_type_ == SQNBIT_CompInt8) { @@ -337,25 +367,75 @@ Status MatMulNBits::PrePack(const Tensor& tensor, int input_idx, /*out*/ All } } #endif // MLAS_TARGET_ARM64 - } else if (compute_type_ == HQNBIT_CompInt8 && nbits_ == 8) { - // For 8-bit HQNBIT_CompInt8, scales are fp16 but the SQ8 packing functions expect float. + } else if (compute_type_ == HQNBIT_CompInt8) { + // For HQNBIT_CompInt8 (both 4-bit and 8-bit), scales are fp16 but packing functions expect float. // Convert fp16 scales to float and pack using the SQNBIT_CompInt8 path. + // At compute time, we delegate to MlasQNBitGemmBatch with SQNBIT_CompInt8. if (input_idx == InputIndex::scales && packed_b_ != nullptr) { - auto sptr_fp16 = tensor.Data(); - std::vector scales_fp32(static_cast(tensor.Shape().Size())); - MlasConvertHalfToFloatBuffer(sptr_fp16, scales_fp32.data(), scales_fp32.size()); - MlasQNBitGemmPackQuantBData(N_, K_, nbits_, block_size_, SQNBIT_CompInt8, nullptr, packed_b_.get(), - scales_fp32.data(), has_zp_input_, nullptr, nullptr, - &mlas_backend_kernel_selector_config_); - is_packed = false; +#if defined(MLAS_TARGET_ARM64) + // For 4-bit on ARM64: check if KleidiAI packs scales into B (scales already packed during B packing). + if (nbits_ == 4 && + MlasQNBitGemmScalesPacked(K_, nbits_, block_size_, SQNBIT_CompInt8, + has_zp_input_, &mlas_backend_kernel_selector_config_)) { + // For asymmetric quantization, require zero_points to be constant for KleidiAI. + if (has_zp_input_) { + const Tensor* zp_tensor = nullptr; + OpKernel::Info().TryGetConstantInput(InputIndex::zero_points, &zp_tensor); + if (zp_tensor == nullptr) { + // zero_points is dynamic: fall back to non-KleidiAI path. + // Convert scales to fp32 for use at compute time. + auto sptr_fp16 = tensor.Data(); + auto tensor_size = static_cast(tensor.Shape().Size()); + if (!scales_fp32_) { + scales_fp32_ = IAllocator::MakeUniquePtr(alloc, tensor_size, true); + MlasConvertHalfToFloatBuffer(sptr_fp16, scales_fp32_.get(), tensor_size); + } + return Status::OK(); + } + } + + // BZpCorr was already computed during B packing in Step 1 (if applicable). + scales_are_packed_ = true; + is_packed = true; + } else +#endif // MLAS_TARGET_ARM64 + { + // Non-KleidiAI path (or 8-bit): convert fp16 scales to fp32. + auto sptr_fp16 = tensor.Data(); + auto tensor_size = static_cast(tensor.Shape().Size()); + if (!scales_fp32_) { + scales_fp32_ = IAllocator::MakeUniquePtr(alloc, tensor_size, true); + MlasConvertHalfToFloatBuffer(sptr_fp16, scales_fp32_.get(), tensor_size); + } + // Pack scales separately only for 8-bit. For 4-bit on ARM64, scales are already packed + // during B packing or used as a raw pointer at compute time (matching standard + // SQNBIT_CompInt8 behavior where should_pack_scale_and_zp_inputs = (nbits_ == 8) on ARM64). + if (nbits_ == 8) { + MlasQNBitGemmPackQuantBData(N_, K_, nbits_, block_size_, SQNBIT_CompInt8, nullptr, packed_b_.get(), + scales_fp32_.get(), has_zp_input_, nullptr, nullptr, + &mlas_backend_kernel_selector_config_); + } + is_packed = false; + } } - if (input_idx == InputIndex::zero_points && packed_b_ != nullptr) { + // Pack zero_points separately only for 8-bit (matching standard SQNBIT_CompInt8 behavior). + // For 4-bit, zero_points are passed directly in data params or handled via KleidiAI BZpCorr. + if (input_idx == InputIndex::zero_points && packed_b_ != nullptr && nbits_ == 8) { auto zptr = tensor.Data(); MlasQNBitGemmPackQuantBData(N_, K_, nbits_, block_size_, SQNBIT_CompInt8, nullptr, packed_b_.get(), nullptr, has_zp_input_, zptr, nullptr, &mlas_backend_kernel_selector_config_); is_packed = false; } + + // Pre-convert fp16 bias to fp32 for use at compute time. + if (input_idx == InputIndex::bias) { + auto bptr_fp16 = tensor.Data(); + auto tensor_size = static_cast(tensor.Shape().Size()); + bias_fp32_ = IAllocator::MakeUniquePtr(alloc, tensor_size, true); + MlasConvertHalfToFloatBuffer(bptr_fp16, bias_fp32_.get(), tensor_size); + is_packed = false; + } } else if (prefer_lut_gemm_) { // Pack scales/zero_points for LUT GEMM if B was already packed but scales weren't available then if (input_idx == InputIndex::scales && packed_b_ != nullptr) { @@ -519,9 +599,7 @@ Status MatMulNBits::ComputeBPacked(const Tensor* a, concurrency::ThreadPool* thread_pool, const MatMulComputeHelper& helper) const { const auto* a_data = a->Data(); - const auto* scales_data = scales == nullptr ? nullptr : scales->Data(); const auto* zero_points_data = zero_points == nullptr ? nullptr : zero_points->DataRaw(); - const auto* bias_data = bias == nullptr ? nullptr : bias->Data(); auto* y_data = y->MutableData(); const size_t batch_count = helper.OutputOffsets().size(); @@ -530,6 +608,91 @@ Status MatMulNBits::ComputeBPacked(const Tensor* a, const size_t K = static_cast(helper.K()); const size_t lda = helper.Lda(false); + // For HQNBIT_CompInt8 with fp16 inputs: delegate to fp32 MLAS path (SQNBIT_CompInt8). + // The HQ CompInt8 kernels are just wrappers that convert fp16->fp32 per-tile and call the same + // SQ fp32 kernels. By doing bulk conversion at the operator level we eliminate per-tile overhead + // and automatically get KleidiAI support for 4-bit (since SQ4BitGemm_CompInt8 checks KleidiAI). + // This matches the approach used by x64 and Apple ARM64 (non-fp16-intrinsics fallback). + if constexpr (std::is_same_v) { + if (compute_type_ == HQNBIT_CompInt8) { + const auto* a_data_fp16 = a->Data(); + const auto* bias_data_fp16 = bias == nullptr ? nullptr : bias->Data(); + + // Bulk convert A from fp16 to fp32. + auto a_size = static_cast(a->Shape().Size()); + auto tmp_a_data_ptr = IAllocator::MakeUniquePtr(allocator, a_size, true); + MlasConvertHalfToFloatBuffer(a_data_fp16, tmp_a_data_ptr.get(), a_size); + + // Use pre-converted fp32 scales, or nullptr if scales are baked into packed B (KleidiAI). + // For non-KleidiAI 4-bit: scales_fp32_ was set during PrePack. + // For 8-bit: scales are packed inside PackedQuantBDataStruct and extracted at dispatch. + float* scales_ptr = nullptr; + IAllocatorUniquePtr tmp_scales; + if (!scales_are_packed_) { + if (scales_fp32_) { + scales_ptr = scales_fp32_.get(); + } else { + // Dynamic scales (non-constant input): convert fp16 to fp32 at compute time. + ORT_ENFORCE(scales != nullptr, "scales must be provided when not packed and not pre-converted"); + auto scales_size = static_cast(scales->Shape().Size()); + tmp_scales = IAllocator::MakeUniquePtr(allocator, scales_size, true); + MlasConvertHalfToFloatBuffer(scales->Data(), tmp_scales.get(), scales_size); + scales_ptr = tmp_scales.get(); + } + } + + // Use pre-converted fp32 bias, or convert on the fly. + float* bias_ptr = nullptr; + IAllocatorUniquePtr tmp_bias; + if (bias_data_fp16) { + if (bias_fp32_) { + bias_ptr = bias_fp32_.get(); + } else { + auto bias_size = static_cast(bias->Shape().Size()); + tmp_bias = IAllocator::MakeUniquePtr(allocator, bias_size, true); + MlasConvertHalfToFloatBuffer(bias_data_fp16, tmp_bias.get(), bias_size); + bias_ptr = tmp_bias.get(); + } + } + + // Allocate fp32 output buffer. + auto c_size = static_cast(y->Shape().Size()); + auto tmp_c = IAllocator::MakeUniquePtr(allocator, c_size, true); + + // Compute workspace sized for SQNBIT_CompInt8 (includes KleidiAI workspace when available). + IAllocatorUniquePtr workspace{}; + const size_t workspace_size = MlasQNBitGemmBatchWorkspaceSize( + M, N, K, batch_count, nbits_, block_size_, zero_points, SQNBIT_CompInt8, &mlas_backend_kernel_selector_config_); + if (workspace_size > 0) { + workspace = IAllocator::MakeUniquePtr(allocator, workspace_size, true); + } + + InlinedVector> data(batch_count); + for (size_t i = 0; i < batch_count; ++i) { + data[i].A = tmp_a_data_ptr.get() + helper.LeftOffsets()[i]; + data[i].lda = lda; + data[i].QuantBDataWorkspace = packed_b_.get(); + data[i].PackedQuantBData = static_cast(packed_b_.get()); + data[i].QuantBScale = scales_ptr; + data[i].QuantBZeroPoint = zero_points_data; + data[i].Bias = bias_ptr; + data[i].C = tmp_c.get() + helper.OutputOffsets()[i]; + data[i].ldc = N; + } + + MlasQNBitGemmBatch(M, N, K, batch_count, nbits_, block_size_, SQNBIT_CompInt8, data.data(), workspace.get(), + thread_pool, &mlas_backend_kernel_selector_config_); + + // Bulk convert output from fp32 to fp16. + MlasConvertFloatToHalfBuffer(tmp_c.get(), y_data, c_size); + return Status::OK(); + } + } + + // Standard path for non-HQNBIT_CompInt8 compute types (fp32 inputs, CompFp32, CompFp16, etc.) + const auto* scales_data = scales == nullptr ? nullptr : scales->Data(); + const auto* bias_data = bias == nullptr ? nullptr : bias->Data(); + IAllocatorUniquePtr workspace{}; const size_t workspace_size = MlasQNBitGemmBatchWorkspaceSize( M, N, K, batch_count, nbits_, block_size_, zero_points, compute_type_, &mlas_backend_kernel_selector_config_); @@ -542,7 +705,7 @@ Status MatMulNBits::ComputeBPacked(const Tensor* a, for (size_t i = 0; i < batch_count; ++i) { data[i].A = a_data + helper.LeftOffsets()[i]; data[i].lda = lda; - if (compute_type_ == SQNBIT_CompInt8 || (compute_type_ == HQNBIT_CompInt8 && nbits_ == 8)) { + if (compute_type_ == SQNBIT_CompInt8) { data[i].QuantBDataWorkspace = packed_b_.get(); } data[i].PackedQuantBData = static_cast(packed_b_.get()); diff --git a/onnxruntime/core/mlas/lib/qnbitgemm.cpp b/onnxruntime/core/mlas/lib/qnbitgemm.cpp index e861a26f188ba..f649d8ab38648 100644 --- a/onnxruntime/core/mlas/lib/qnbitgemm.cpp +++ b/onnxruntime/core/mlas/lib/qnbitgemm.cpp @@ -31,10 +31,8 @@ enum QNBitGemmVariant { SQ4BitGemmVariant_CompFp32 = 0, SQ4BitGemmVariant_CompInt8, HQ4BitGemmVariant_CompFp16, - HQ4BitGemmVariant_CompInt8, SQ8BitGemmVariant_CompInt8, HQ8BitGemmVariant_CompFp16, - HQ8BitGemmVariant_CompInt8, // End of valid variants @@ -58,16 +56,12 @@ GetQNBitGemmVariant( return HQ4BitGemmVariant_CompFp16; } else if (ComputeType == SQNBIT_CompInt8) { return SQ4BitGemmVariant_CompInt8; - } else if (ComputeType == HQNBIT_CompInt8) { - return HQ4BitGemmVariant_CompInt8; } } else if (BlkBitWidth == 8) { if (ComputeType == SQNBIT_CompInt8) { return SQ8BitGemmVariant_CompInt8; } else if (ComputeType == HQNBIT_CompFp16) { return HQ8BitGemmVariant_CompFp16; - } else if (ComputeType == HQNBIT_CompInt8) { - return HQ8BitGemmVariant_CompInt8; } } } @@ -84,6 +78,12 @@ MlasIsQNBitGemmAvailable( MLAS_QNBIT_GEMM_COMPUTE_TYPE ComputeType ) { + // HQNBIT_CompInt8 uses the same MLAS kernels as SQNBIT_CompInt8. + // The operator handles fp16<->fp32 conversion and delegates to the SQ path. + if (ComputeType == HQNBIT_CompInt8) { + ComputeType = SQNBIT_CompInt8; + } + const auto* Dispatch = GetMlasPlatform().QNBitGemmDispatch; if (Dispatch == nullptr) { return false; @@ -101,7 +101,7 @@ MlasIsQNBitGemmAvailable( Dispatch->HQ4BitGemmKernel_CompFp16 != nullptr && Dispatch->HQ4BitBlkDequantBForHgemm_CompFp16 != nullptr; } - case SQ4BitGemmVariant_CompInt8: { // SQ4BitGemmKernel_BlkSum_CompInt8 + case SQ4BitGemmVariant_CompInt8: { return (Dispatch->SQ4BitGemmKernel_Packed_CompInt8 != nullptr && Dispatch->QuantizeA_Packed_CompInt8 != nullptr) || (Dispatch->SQ4BitGemmKernel_CompInt8 != nullptr && Dispatch->QuantizeARow_CompInt8 != nullptr) || @@ -117,16 +117,6 @@ MlasIsQNBitGemmAvailable( Dispatch->HQ8BitBlkDequantBForHgemm_CompFp16 != nullptr && Dispatch->HQ4BitGemmKernel_CompFp16 != nullptr; } - case HQ4BitGemmVariant_CompInt8: { - return - (Dispatch->SQ4BitGemmKernel_CompInt8 != nullptr && Dispatch->QuantizeARow_CompInt8 != nullptr) || - (Dispatch->SQ4BitGemmKernel_BlkSum_CompInt8 != nullptr && Dispatch->QuantizeARowComputeBlkSum_CompInt8 != nullptr); - } - case HQ8BitGemmVariant_CompInt8: { - return Dispatch->SQ8BitGemmPackQuantBDataAndBlkSum != nullptr && - Dispatch->SQ8BitGemmKernel_BlkSum_CompInt8 != nullptr && - Dispatch->QuantizeARowComputeBlkSum_CompInt8 != nullptr; - } default: { return false; } @@ -270,16 +260,6 @@ struct PerGemmQuantAWorkspace { size_t M_, BlockCountK_, BlkLen_; }; -// Workspace bundle for HQ8BitGemm_CompInt8. -// Contains QuantA workspace and pre-extracted float B pointers from PackedQuantBDataStruct. -struct HQ8BitCompInt8PerGemmWorkspace { - PerGemmQuantAWorkspace quant_a; - std::byte* PackedQuantBData; - float* PackedQuantBScale; - float* QuantBBlkSum; - float* BlkUnsignedQuantAZeroPointCorrection; -}; - void MLASCALL MlasQNBitGemmPackQuantBData( size_t N, @@ -318,20 +298,6 @@ MlasQNBitGemmPackQuantBData( ThreadPool, BackendKernelSelectorConfig ); - } else if (ComputeType == HQNBIT_CompInt8 && Dispatch->SQ4BitGemmPackQuantBData != nullptr) { - // Use SQ4BitGemmPackQuantBData directly with SQNBIT_CompInt8 to get the correct int8 - // sub-block packing format. Bypass SQ4BitGemmPackQuantBDataAndBlkSum to avoid KleidiAI - // path which would incorrectly interpret fp16 scales as float. - Dispatch->SQ4BitGemmPackQuantBData( - N, - K, - BlkLen, - SQNBIT_CompInt8, - static_cast(QuantBData), - static_cast(PackedQuantBDataAndOrBlkSumWorkspace), - ThreadPool, - BackendKernelSelectorConfig - ); } else if (ComputeType == HQNBIT_CompFp16 && Dispatch->HQ4BitGemmPackQuantBData != nullptr) { Dispatch->HQ4BitGemmPackQuantBData( N, @@ -371,7 +337,7 @@ MlasQNBitGemmPackQuantBData( ThreadPool, BackendKernelSelectorConfig ); - } else if ((ComputeType == SQNBIT_CompInt8 || ComputeType == HQNBIT_CompInt8) && Dispatch->SQ8BitGemmPackQuantBDataAndBlkSum != nullptr) { + } else if (ComputeType == SQNBIT_CompInt8 && Dispatch->SQ8BitGemmPackQuantBDataAndBlkSum != nullptr) { const size_t BlockCountK = MlasDivRoundup(K, BlkLen); PackedQuantBDataStruct packed_quant_b(PackedQuantBDataAndOrBlkSumWorkspace, N, BlockCountK, BlkLen, GetMlasPlatform().ArmNeonIsQuantActivationsUnsigned); @@ -716,213 +682,6 @@ HQ8BitGemm_CompFp16( } } -void -HQ8BitGemm_CompInt8( - const size_t BlkLen, - const size_t K, - const MLAS_QNBIT_GEMM_DATA_PARAMS* const DataParams, - void* const PerGemmWorkspace, - const size_t RangeStartM, - const size_t RangeCountM, - const size_t RangeStartN, - const size_t RangeCountN, - const MLAS_BACKEND_KERNEL_SELECTOR_CONFIG* BackendKernelSelectorConfig -) -{ - MLAS_UNREFERENCED_PARAMETER(BackendKernelSelectorConfig); - constexpr size_t BlkBitWidth = 8; - - const size_t k_blks = MlasDivRoundup(K, BlkLen); - const size_t lda = k_blks * BlkLen; // separate scale array, not Q8BlkSize - const size_t ldc = DataParams->ldc; - const size_t ldb = k_blks * MlasQNBitBlkDataSizeInBytes(BlkBitWidth, BlkLen); - - auto* ws = static_cast(PerGemmWorkspace); - - const std::byte* QuantA = ws->quant_a.QuantData + RangeStartM * lda; - const float* QuantAScale = ws->quant_a.QuantScale + RangeStartM * k_blks; - const float* ABlockSum = ws->quant_a.BlockSum + RangeStartM * k_blks; - - const std::byte* QuantBData = ws->PackedQuantBData + RangeStartN * ldb; - const float* QuantBScale = ws->PackedQuantBScale + RangeStartN * k_blks; - const float* QuantBBlkSum = ws->QuantBBlkSum + RangeStartN * k_blks; - const float* BlkUnsignedQuantAZeroPointCorrection = - ws->BlkUnsignedQuantAZeroPointCorrection - ? ws->BlkUnsignedQuantAZeroPointCorrection + RangeStartN * k_blks - : nullptr; - - MLAS_FP16* C = DataParams->C + RangeStartM * ldc + RangeStartN; - - const MLAS_FP16* BiasFp16 = (DataParams->Bias == nullptr) ? nullptr : DataParams->Bias + RangeStartN; - - // Convert fp16 bias to fp32 - std::vector bias_fp32; - float* bias_fp32_ptr = nullptr; - if (BiasFp16 != nullptr) { - bias_fp32.resize(RangeCountN); - MlasConvertHalfToFloatBuffer(BiasFp16, bias_fp32.data(), RangeCountN); - bias_fp32_ptr = bias_fp32.data(); - } - - size_t CountN; - const size_t MaxCountN = std::min(RangeCountN, size_t{128}); - // Temporary fp32 C buffer reused across N-chunks to avoid per-iteration allocations. - std::vector c_temp(RangeCountM * MaxCountN); - - for (size_t n = 0; n < RangeCountN; n += CountN) { - CountN = std::min(RangeCountN - n, size_t{128}); - - const std::byte* b_col = QuantBData + n * ldb; - const float* b_col_scale = QuantBScale + n * k_blks; - const float* bias = (bias_fp32_ptr == nullptr) ? nullptr : bias_fp32_ptr + n; - const float* b_blk_sum = QuantBBlkSum + n * k_blks; - const float* blk_unsigned = - BlkUnsignedQuantAZeroPointCorrection - ? BlkUnsignedQuantAZeroPointCorrection + n * k_blks - : nullptr; - - GetMlasPlatform().QNBitGemmDispatch->SQ8BitGemmKernel_BlkSum_CompInt8( - BlkLen, - QuantA, - QuantAScale, - b_col, - b_col_scale, - nullptr, // zero points baked into BlkSum - c_temp.data(), - RangeCountM, - CountN, - K, - k_blks, - bias, - CountN, // ldc for temp buffer - ABlockSum, - b_blk_sum, - blk_unsigned - ); - - // Convert fp32 C output to fp16 and write to actual output - MLAS_FP16* c_out = C + n; - for (size_t m = 0; m < RangeCountM; m++) { - MlasConvertFloatToHalfBuffer( - c_temp.data() + m * CountN, - c_out + m * ldc, - CountN - ); - } - - if (DataParams->PostProcessor != nullptr) { - DataParams->PostProcessor->Process( - DataParams->C, RangeStartM, RangeStartN + n, - RangeCountM, CountN, ldc - ); - } - } -} - -void -HQ4BitGemm_CompInt8( - const size_t BlkLen, - const size_t K, - const MLAS_QNBIT_GEMM_DATA_PARAMS* const DataParams, - void* const PerGemmWorkspace, - const size_t RangeStartM, - const size_t RangeCountM, - const size_t RangeStartN, - const size_t RangeCountN, - const MLAS_BACKEND_KERNEL_SELECTOR_CONFIG* BackendKernelSelectorConfig -) -{ - MLAS_UNREFERENCED_PARAMETER(BackendKernelSelectorConfig); - constexpr size_t BlkBitWidth = 4; - - const size_t k_blks = MlasDivRoundup(K, BlkLen); - - const size_t lda = k_blks * Q8BlkSize(BlkLen); - const size_t ldc = DataParams->ldc; - const size_t ldb = k_blks * MlasQNBitBlkDataSizeInBytes(BlkBitWidth, BlkLen); - const size_t k_blks_zp_bytes = MlasQNBitZeroPointsForBlksSizeInBytes(k_blks); - - const std::byte* QuantA = static_cast(PerGemmWorkspace) + RangeStartM * lda; - - const std::byte* QuantBData = static_cast(DataParams->PackedQuantBData) + RangeStartN * ldb; - const MLAS_FP16* QuantBScaleFp16 = DataParams->QuantBScale + RangeStartN * k_blks; - const std::byte* QuantBZeroPoint = - (DataParams->QuantBZeroPoint == nullptr) - ? nullptr - : static_cast(DataParams->QuantBZeroPoint) + RangeStartN * k_blks_zp_bytes; - - MLAS_FP16* C = DataParams->C + RangeStartM * ldc + RangeStartN; - - const MLAS_FP16* BiasFp16 = (DataParams->Bias == nullptr) ? nullptr : DataParams->Bias + RangeStartN; - - if (GetMlasPlatform().QNBitGemmDispatch->SQ4BitGemmKernel_CompInt8 == nullptr) { - return; - } - - size_t CountN; - const size_t maxCountN = std::min(RangeCountN, size_t{128}); - // Pre-allocate reusable buffers sized for the maximum column chunk - std::vector b_col_scale_fp32(maxCountN * k_blks); - std::vector bias_fp32(maxCountN); - std::vector c_temp(RangeCountM * maxCountN); - - for (size_t n = 0; n < RangeCountN; n += CountN) { - CountN = std::min(RangeCountN - n, size_t{128}); - - const std::byte* a_row = QuantA; - const std::byte* b_col = QuantBData + n * ldb; - const std::byte* b_col_zp = - (QuantBZeroPoint == nullptr) ? nullptr : QuantBZeroPoint + n * k_blks_zp_bytes; - MLAS_FP16* c_blk = C + n; - const MLAS_FP16* bias_fp16 = (BiasFp16 == nullptr) ? nullptr : BiasFp16 + n; - - // Convert fp16 scales to fp32 for this column chunk - b_col_scale_fp32.resize(CountN * k_blks); - MlasConvertHalfToFloatBuffer(QuantBScaleFp16 + n * k_blks, b_col_scale_fp32.data(), CountN * k_blks); - - // Convert fp16 bias to fp32 - float* bias_fp32_ptr = nullptr; - if (bias_fp16 != nullptr) { - bias_fp32.resize(CountN); - MlasConvertHalfToFloatBuffer(bias_fp16, bias_fp32.data(), CountN); - bias_fp32_ptr = bias_fp32.data(); - } - - size_t RowsRemaining = RangeCountM; - size_t RowsProcessed = 0; - while (RowsRemaining > 0) { - const auto RowsHandled = GetMlasPlatform().QNBitGemmDispatch->SQ4BitGemmKernel_CompInt8( - BlkLen, - a_row, b_col, b_col_scale_fp32.data(), b_col_zp, - c_temp.data() + RowsProcessed * CountN, - RowsRemaining, CountN, K, k_blks, CountN, bias_fp32_ptr - ); - - // Convert fp32 C output to fp16 and write to actual output - for (size_t m = 0; m < RowsHandled; m++) { - MlasConvertFloatToHalfBuffer( - c_temp.data() + (RowsProcessed + m) * CountN, - c_blk + m * ldc, - CountN - ); - } - - if (DataParams->PostProcessor != nullptr) { - DataParams->PostProcessor->Process( - DataParams->C, RangeStartM + RowsProcessed, RangeStartN + n, - RowsHandled, CountN, ldc - ); - } - - c_blk += RowsHandled * ldc; - a_row += RowsHandled * lda; - - RowsProcessed += RowsHandled; - RowsRemaining -= RowsHandled; - } - } -} - void SQ4BitGemm_CompInt8( const size_t BlkLen, @@ -1305,86 +1064,6 @@ InitializeWorkspace_CompInt8( } } -template <> -void -InitializeWorkspace_CompInt8( - size_t M, - size_t N, - size_t K, - size_t BatchN, - size_t BlkLen, - const MLAS_QNBIT_GEMM_DATA_PARAMS* DataParams, - void* Workspace, - size_t PerGemmWorkspaceStride, - MLAS_THREADPOOL* ThreadPool, - size_t BlkBitWidth, - const MLAS_BACKEND_KERNEL_SELECTOR_CONFIG* BackendKernelSelectorConfig -) { - MLAS_UNREFERENCED_PARAMETER(N); - MLAS_UNREFERENCED_PARAMETER(BackendKernelSelectorConfig); - - const size_t BlockCountK = MlasDivRoundup(K, BlkLen); - - if (BlkBitWidth == 8) { - // For 8-bit, use QuantizeARowComputeBlkSum to produce separate QuantData, QuantScale, BlockSum. - // This matches the workspace layout expected by PerGemmQuantAWorkspace / HQ8BitCompInt8PerGemmWorkspace. - const auto QuantizeARow2 = GetMlasPlatform().QNBitGemmDispatch->QuantizeARowComputeBlkSum_CompInt8; - if (QuantizeARow2) { - MlasTrySimpleParallel(ThreadPool, BatchN, [&](ptrdiff_t gemm_idx) { - const auto& data = DataParams[gemm_idx]; - - const MLAS_FP16* ARowPtr = data.A; - void* PerGemmWs = static_cast(Workspace) + gemm_idx * PerGemmWorkspaceStride; - PerGemmQuantAWorkspace quant_a_data(PerGemmWs, M, BlockCountK, BlkLen); - std::byte* QuantARowPtr = quant_a_data.QuantData; - float* QuantARowScalePtr = quant_a_data.QuantScale; - float* QuantARowBlkSum = quant_a_data.BlockSum; - - static thread_local std::vector a_row_fp32; - if (a_row_fp32.size() < K) { - a_row_fp32.resize(K); - } - - for (size_t m = 0; m < M; ++m) { - MlasConvertHalfToFloatBuffer(ARowPtr, a_row_fp32.data(), K); - QuantizeARow2(BlkLen, a_row_fp32.data(), K, QuantARowPtr, QuantARowScalePtr, QuantARowBlkSum); - - ARowPtr += data.lda; - QuantARowPtr += BlockCountK * BlkLen; - QuantARowScalePtr += BlockCountK; - QuantARowBlkSum += BlockCountK; - } - }); - } - } else { - // For 4-bit, use QuantizeARow to produce Q8BlkSize format (embedded scales). - const auto QuantizeARow = GetMlasPlatform().QNBitGemmDispatch->QuantizeARow_CompInt8; - const size_t QuantAStride = BlockCountK * Q8BlkSize(BlkLen); - - if (QuantizeARow) { - MlasTrySimpleParallel(ThreadPool, BatchN, [&](ptrdiff_t gemm_idx) { - const auto& data = DataParams[gemm_idx]; - - const MLAS_FP16* ARowPtr = data.A; - std::byte* QuantARowPtr = static_cast(Workspace) + gemm_idx * PerGemmWorkspaceStride; - - static thread_local std::vector a_row_fp32; - if (a_row_fp32.size() < K) { - a_row_fp32.resize(K); - } - - for (size_t m = 0; m < M; ++m) { - MlasConvertHalfToFloatBuffer(ARowPtr, a_row_fp32.data(), K); - QuantizeARow(BlkLen, a_row_fp32.data(), K, QuantARowPtr); - - ARowPtr += data.lda; - QuantARowPtr += QuantAStride; - } - }); - } - } -} - template using InitializeWorkspaceFn = std::function InitializeWorkspaceFn GetInitializeWorkspace(QNBitGemmVariant variant) { - switch (variant) { - case HQ4BitGemmVariant_CompInt8: - case HQ8BitGemmVariant_CompInt8: - return InitializeWorkspace_CompInt8; - default: - return nullptr; - } + MLAS_UNREFERENCED_PARAMETER(variant); + return nullptr; } template @@ -1472,10 +1146,6 @@ GetQNBitGemm(QNBitGemmVariant variant) return HQ4BitGemm_CompFp16; case HQ8BitGemmVariant_CompFp16: return HQ8BitGemm_CompFp16; - case HQ4BitGemmVariant_CompInt8: - return HQ4BitGemm_CompInt8; - case HQ8BitGemmVariant_CompInt8: - return HQ8BitGemm_CompInt8; default: return nullptr; } @@ -1587,18 +1257,6 @@ MlasQNBitGemmBatch( PerGemmQuantAWorkspace per_gemm_quant_a_workspace(PerGemmWorkspace, M, BlockCountK, BlkLen); ComputeOperation(BlkLen, K, Data, &per_gemm_quant_a_workspace, 0, M, 0, N, BackendKernelSelectorConfig); - } else if (Variant == HQ8BitGemmVariant_CompInt8 && GetMlasPlatform().QNBitGemmDispatch->SQ8BitGemmKernel_BlkSum_CompInt8 != nullptr) { - // Use PackedQuantBDataStruct to extract float pointers from the packed workspace. - // The packed workspace was created with float scales during PrePack. - PackedQuantBDataStruct packed_quant_b(const_cast(Data->QuantBDataWorkspace), N, BlockCountK, BlkLen, GetMlasPlatform().ArmNeonIsQuantActivationsUnsigned); - HQ8BitCompInt8PerGemmWorkspace hw{ - PerGemmQuantAWorkspace(PerGemmWorkspace, M, BlockCountK, BlkLen), - packed_quant_b.PackedQuantBData, - packed_quant_b.PackedQuantBScale, - packed_quant_b.QuantBBlkSum, - packed_quant_b.BlkUnsignedQuantAZeroPointCorrection - }; - ComputeOperation(BlkLen, K, Data, &hw, 0, M, 0, N, BackendKernelSelectorConfig); } else { ComputeOperation(BlkLen, K, Data, PerGemmWorkspace, 0, M, 0, N, BackendKernelSelectorConfig); } @@ -1680,16 +1338,6 @@ MlasQNBitGemmBatch( PerGemmQuantAWorkspace per_gemm_quant_a_workspace(PerGemmWorkspace, M, BlockCountK, BlkLen); ComputeOperation(BlkLen, K, Data, &per_gemm_quant_a_workspace, RangeStartM, RangeCountM, RangeStartN, RangeCountN, BackendKernelSelectorConfig); - } else if (Variant == HQ8BitGemmVariant_CompInt8 && GetMlasPlatform().QNBitGemmDispatch->SQ8BitGemmKernel_BlkSum_CompInt8 != nullptr) { - PackedQuantBDataStruct packed_quant_b(const_cast(Data->QuantBDataWorkspace), N, BlockCountK, BlkLen, GetMlasPlatform().ArmNeonIsQuantActivationsUnsigned); - HQ8BitCompInt8PerGemmWorkspace hw{ - PerGemmQuantAWorkspace(PerGemmWorkspace, M, BlockCountK, BlkLen), - packed_quant_b.PackedQuantBData, - packed_quant_b.PackedQuantBScale, - packed_quant_b.QuantBBlkSum, - packed_quant_b.BlkUnsignedQuantAZeroPointCorrection - }; - ComputeOperation(BlkLen, K, Data, &hw, RangeStartM, RangeCountM, RangeStartN, RangeCountN, BackendKernelSelectorConfig); } else { ComputeOperation(BlkLen, K, Data, PerGemmWorkspace, RangeStartM, RangeCountM, RangeStartN, RangeCountN, BackendKernelSelectorConfig); } diff --git a/onnxruntime/core/mlas/lib/qnbitgemm_kernel_neon.cpp b/onnxruntime/core/mlas/lib/qnbitgemm_kernel_neon.cpp index ac42ced83f36c..5a3c8005d8318 100644 --- a/onnxruntime/core/mlas/lib/qnbitgemm_kernel_neon.cpp +++ b/onnxruntime/core/mlas/lib/qnbitgemm_kernel_neon.cpp @@ -90,7 +90,7 @@ QNBitGemmPackQuantBDataSize( const size_t BlockCountK = MlasDivRoundup(K, BlkLen); size_t PackedQuantBDataSize = N * BlockCountK * MlasQNBitBlkDataSizeInBytes(BlkBitWidth, BlkLen); - if (ComputeType == SQNBIT_CompInt8 || ComputeType == HQNBIT_CompInt8) { + if (ComputeType == SQNBIT_CompInt8) { const size_t ScaleSize = N * BlockCountK * sizeof(float); size_t BlkSumSize = MlasDivRoundup(N, 16) * BlockCountK * 16 * sizeof(float); @@ -132,7 +132,7 @@ SQ4BitGemmPackQuantBData( const size_t BlkDataSize = MlasQNBitBlkDataSizeInBytes(BlkBitWidth, BlkLen); const size_t Iterations = N * BlockCountK; // one iteration per block - const size_t SubBlkLen = (ComputeType == SQNBIT_CompInt8 || ComputeType == HQNBIT_CompInt8) + const size_t SubBlkLen = (ComputeType == SQNBIT_CompInt8) ? ((BlkLen == 16) ? 16 : 32) : 16; @@ -488,12 +488,6 @@ QNBitGemmPerGemmWorkspaceSize( return PerGemmWorkspaceSize; } } - case HQNBIT_CompInt8: { - // Same workspace layout as SQNBIT_CompInt8 for block quantization of A to int8 - const size_t BlockCountK = MlasDivRoundup(K, BlkLen); - const size_t PerGemmWorkspaceSize = M * BlockCountK * (Q8BlkSize(BlkLen) + sizeof(float)); - return PerGemmWorkspaceSize; - } default: { return 0; } @@ -509,8 +503,7 @@ QNBitGemmPerGemmWorkspaceAlignment( MLAS_UNREFERENCED_PARAMETER(BlkLen); switch (ComputeType) { - case SQNBIT_CompInt8: - case HQNBIT_CompInt8: { + case SQNBIT_CompInt8: { return Q8BlkAlignment(); } default: { From 0c2674109e866cdb5b341198fbaa1daf8d24885f Mon Sep 17 00:00:00 2001 From: Colm Donelan <52702205+Colm-in-Arm@users.noreply.github.com> Date: Thu, 26 Mar 2026 02:00:04 +0000 Subject: [PATCH 03/14] Disable KleidiAI for older versions of MSVC without Aarch64 SME support. (#27825) ### Description Support for Aarch64 SME intrinsics was added to version 19.40 of MSVC. The ONNX Runtime stated supported version of Visual Studio 2022 can go back before version 19.40. This patch modifies cmake/CMakeLists.txt to check the version of MSVC, if it is the target compiler. For versions less than 19.40 KleidiAi will be disabled in the build. ### Motivation and Context This issue was raised when cross compiling 1.24 for Windows on Arm. https://github.com/microsoft/onnxruntime/issues/27304 --------- Signed-off-by: Colm Donelan Co-authored-by: Colm Donelan Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmake/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 385342479913a..0b70e01d15dbe 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -564,6 +564,14 @@ if(onnxruntime_USE_KLEIDIAI) set(${is_supported_var} FALSE PARENT_SCOPE) return() endif() + + if(MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND MSVC_VERSION VERSION_LESS 1940) + message(WARNING "KleidiAI requires MSVC compiler version 19.40 or newer, KleidiAI will be disabled in this build.") + + set(${is_supported_var} FALSE PARENT_SCOPE) + return() + endif() + set(${is_supported_var} TRUE PARENT_SCOPE) endfunction() From c7b8fd27ed3094ce679b7d0c6dc848b72d6ffa6a Mon Sep 17 00:00:00 2001 From: Sanaa Hamel Date: Thu, 26 Mar 2026 09:44:41 -0400 Subject: [PATCH 04/14] [CI] feat: use ccache & vcpkg cache for linux workflows (#27623) ### Description Enable ccache and vcpkg caching for Linux workflows that use `reusable_linux_build.yml`. Saves about ~15-20 min on a 100% cache hit. Also parallelises tests. Saves ~6 minutes. Additionally, enable vcpkg and ccache for other Linux workflows. No numbers avail for comparison. ### Motivation and Context This change reduces wasted CO2 and time. ### Known Issues Benign - Android workflow doesn't seem to be populating its ccache. --- .github/workflows/android.yml | 79 ++++++- .../linux-wasm-ci-build-and-test-workflow.yml | 21 +- .github/workflows/linux_cuda_ci.yml | 4 +- .github/workflows/linux_minimal_build.yml | 217 +++++++++++++++--- .github/workflows/linux_tensorrt_ci.yml | 4 +- .github/workflows/reusable_linux_build.yml | 30 ++- .github/workflows/web.yml | 5 + .github/workflows/windows-web-ci-workflow.yml | 3 + tools/ci_build/build.py | 13 +- .../python/cpu/scripts/install_centos.sh | 6 +- .../x86_64/python/openvino/Dockerfile | 3 + .../scripts/manylinux/install_centos.sh | 6 +- ...minimal_build_minimal_ort_and_run_tests.sh | 5 +- 13 files changed, 345 insertions(+), 51 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index e2d421af4d647..6e1ff1ef7aedc 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -29,6 +29,8 @@ jobs: "1ES.Pool=onnxruntime-github-Ubuntu2204-AMD-CPU", "JobId=AndroidBinarySizeCheckJob_MinimalBaseline-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}" ] + env: + CCACHE_DIR: ~/.cache/ccache # explicitly set to prevent any fallback to `~/.ccache` steps: - name: Checkout repository uses: actions/checkout@v6 @@ -41,7 +43,7 @@ jobs: ndk-version: 28.0.13004108 - name: Get Docker Image using Action - uses: microsoft/onnxruntime-github-actions/build-docker-image@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-docker-image@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 id: build_docker_image_step with: dockerfile: ${{ github.workspace }}/tools/ci_build/github/linux/docker/inference/x86_64/default/cpu/Dockerfile @@ -71,6 +73,7 @@ jobs: shell: python working-directory: ${{ github.workspace }} + # FUTURE WORK: ccache, vcpkg cache - name: 1a. Build onnxruntime run: | set -e -x @@ -119,6 +122,8 @@ jobs: "1ES.Pool=onnxruntime-github-Ubuntu2204-AMD-CPU", "JobId=android_nnapi_ep-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}" ] + env: + CCACHE_DIR: ~/.cache/ccache # explicitly set to prevent any fallback to `~/.ccache` steps: - uses: actions/checkout@v6 @@ -129,9 +134,10 @@ jobs: java-version: '17' architecture: x64 - - - uses: microsoft/onnxruntime-github-actions/setup-build-tools@v0.0.9 + - uses: microsoft/onnxruntime-github-actions/setup-build-tools@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: + ccache-version: 4.13.1 + ccache-hash: 626407a9b81dd86f8ec9867bff396b32dd1f00344f5b323526579a64f6d4104927f83e8d7a05ad9806fd78f4491e0adb4cff73388000a62050cb1b00766214ee vcpkg-version: '2025.08.27' vcpkg-hash: '9a4b32849792e13bee1d24726f073b3881acae4165206ddf1a6378e44a4ddd05b3ee93f55ff46d8e8873b3cbcd06606212989e248f0bd615a5bf365070074079' cmake-version: '3.31.6' @@ -144,6 +150,23 @@ jobs: with: ndk-version: 28.0.13004108 + - name: Setup CCache + uses: actions/cache@v4 + with: + # Fully qualify by workflow. `actions/cache` does not isolate by workflow, unlike ADO cache actions. + key: ccache | android.yml | android_nnapi_ep + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: vcpkg-cache | android.yml | android_nnapi_ep + path: ~/.cache/vcpkg + + - name: CCache reset stats + run: ccache --zero-stats + shell: bash + - name: NNAPI EP, Build, Test on Android Emulator run: >- python3 tools/ci_build/build.py @@ -155,7 +178,10 @@ jobs: --android_abi=x86_64 --android_api=29 --skip_submodule_sync - --parallel --use_vcpkg --use_vcpkg_ms_internal_asset_cache + --parallel + --use_cache + --use_vcpkg + --use_vcpkg_ms_internal_asset_cache --use_nnapi --build_shared_lib --cmake_generator=Ninja @@ -163,13 +189,16 @@ jobs: --update --build --test shell: bash - - name: Build Minimal ORT with NNAPI and run tests run: tools/ci_build/github/linux/ort_minimal/nnapi_minimal_build_minimal_ort_and_run_tests.sh "$(pwd)" shell: bash + - name: CCache stats + run: ccache --show-stats -vv + shell: bash + - name: Install psutil for emulator shutdown by run_android_emulator.py if: always() run: python3 -m pip install psutil @@ -198,7 +227,8 @@ jobs: "1ES.Pool=onnxruntime-github-Ubuntu2204-AMD-CPU", "JobId=android_cpu_ep-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}" ] - + env: + CCACHE_DIR: ~/.cache/ccache # explicitly set to prevent any fallback to `~/.ccache` steps: - uses: actions/checkout@v6 @@ -209,11 +239,39 @@ jobs: java-version: '17' architecture: x64 + - uses: microsoft/onnxruntime-github-actions/setup-build-tools@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 + with: + ccache-version: 4.13.1 + ccache-hash: 626407a9b81dd86f8ec9867bff396b32dd1f00344f5b323526579a64f6d4104927f83e8d7a05ad9806fd78f4491e0adb4cff73388000a62050cb1b00766214ee + vcpkg-version: '2025.08.27' + vcpkg-hash: '9a4b32849792e13bee1d24726f073b3881acae4165206ddf1a6378e44a4ddd05b3ee93f55ff46d8e8873b3cbcd06606212989e248f0bd615a5bf365070074079' + cmake-version: '3.31.6' + cmake-hash: '42395e20b10a8e9ef3e33014f9a4eed08d46ab952e02d2c1bbc8f6133eca0d7719fb75680f9bbff6552f20fcd1b73d86860f7f39388d631f98fb6f622b37cf04' + add-cmake-to-path: 'true' + disable-terrapin: 'true' + - name: Setup Android NDK uses: ./.github/actions/setup-android-ndk with: ndk-version: 28.0.13004108 + - name: Setup CCache + uses: actions/cache@v4 + with: + # Fully qualify by workflow. `actions/cache` does not isolate by workflow, unlike ADO cache actions. + key: ccache | android.yml | android_cpu_ep + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: vcpkg-cache | android.yml | android_cpu_ep + path: ~/.cache/vcpkg + + - name: CCache reset stats + run: ccache --zero-stats + shell: bash + - name: CPU EP, Build and Test run: >- python3 tools/ci_build/build.py @@ -225,12 +283,19 @@ jobs: --android_abi=x86_64 --android_api=30 --skip_submodule_sync - --parallel --use_vcpkg --use_vcpkg_ms_internal_asset_cache + --parallel + --use_cache + --use_vcpkg + --use_vcpkg_ms_internal_asset_cache --cmake_generator=Ninja --build_java --update --build --test shell: bash + - name: CCache stats + run: ccache --show-stats -vv + shell: bash + - name: Install psutil for emulator shutdown by run_android_emulator.py if: always() run: python3 -m pip install psutil diff --git a/.github/workflows/linux-wasm-ci-build-and-test-workflow.yml b/.github/workflows/linux-wasm-ci-build-and-test-workflow.yml index 4288442720493..c53c61242e6bc 100644 --- a/.github/workflows/linux-wasm-ci-build-and-test-workflow.yml +++ b/.github/workflows/linux-wasm-ci-build-and-test-workflow.yml @@ -4,6 +4,9 @@ description: "This is a reusable workflow for Linux WASM CI pipelines to build a on: workflow_call: inputs: + job_name: # workflow-scope unique key + required: true + type: string build_config: required: true type: string @@ -43,6 +46,7 @@ jobs: buildArch: x64 common_build_args: >- --parallel + --use_cache ${{ inputs.use_vcpkg == true && '--use_vcpkg --use_vcpkg_ms_internal_asset_cache' || '' }} --config ${{ inputs.build_config }} --skip_submodule_sync @@ -77,8 +81,23 @@ jobs: - name: Install python dependencies run: python -m pip install flatbuffers - - uses: microsoft/onnxruntime-github-actions/setup-build-tools@v0.0.9 + - name: Setup CCache + uses: actions/cache@v4 + with: + # Fully qualify by workflow. `actions/cache` does not isolate by workflow, unlike ADO cache actions. + key: ccache | web.yml | ${{ inputs.job_name }} + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: vcpkg-cache | web.yml | ${{ inputs.job_name }} + path: ~/.cache/vcpkg + + - uses: microsoft/onnxruntime-github-actions/setup-build-tools@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: + ccache-version: 4.13.1 + ccache-hash: 626407a9b81dd86f8ec9867bff396b32dd1f00344f5b323526579a64f6d4104927f83e8d7a05ad9806fd78f4491e0adb4cff73388000a62050cb1b00766214ee vcpkg-version: '2025.08.27' vcpkg-hash: '9a4b32849792e13bee1d24726f073b3881acae4165206ddf1a6378e44a4ddd05b3ee93f55ff46d8e8873b3cbcd06606212989e248f0bd615a5bf365070074079' cmake-version: '3.31.6' diff --git a/.github/workflows/linux_cuda_ci.yml b/.github/workflows/linux_cuda_ci.yml index 948b28b276edb..92840f46bc1cf 100644 --- a/.github/workflows/linux_cuda_ci.yml +++ b/.github/workflows/linux_cuda_ci.yml @@ -52,7 +52,7 @@ jobs: - name: Checkout code uses: actions/checkout@v6 - - uses: microsoft/onnxruntime-github-actions/build-docker-image@v0.0.9 + - uses: microsoft/onnxruntime-github-actions/build-docker-image@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 id: build_docker_image_step with: dockerfile: ${{ github.workspace }}/tools/ci_build/github/linux/docker/Dockerfile.manylinux2_28_cuda @@ -95,7 +95,7 @@ jobs: # So build.py --build_dir build/Release inside the container correctly finds the artifacts. - name: Test ONNX Runtime id: test_step - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: Release diff --git a/.github/workflows/linux_minimal_build.yml b/.github/workflows/linux_minimal_build.yml index 4058e7af99070..4705812d78548 100644 --- a/.github/workflows/linux_minimal_build.yml +++ b/.github/workflows/linux_minimal_build.yml @@ -40,8 +40,24 @@ jobs: with: node-version: 20 - - uses: microsoft/onnxruntime-github-actions/setup-build-tools@v0.0.9 + - name: Setup CCache + uses: actions/cache@v4 with: + # Fully qualify by workflow. `actions/cache` does not isolate by workflow, unlike ADO cache actions. + key: ccache | linux_minimal_build.yml | build_full_ort + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + # ostensibly should be able to use the same cache for most of these, but in practice the hash does not match. + key: vcpkg-cache | linux_minimal_build.yml | build_full_ort + path: ~/.cache/vcpkg + + - uses: microsoft/onnxruntime-github-actions/setup-build-tools@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 + with: + ccache-version: 4.13.1 + ccache-hash: 626407a9b81dd86f8ec9867bff396b32dd1f00344f5b323526579a64f6d4104927f83e8d7a05ad9806fd78f4491e0adb4cff73388000a62050cb1b00766214ee vcpkg-version: '2025.08.27' vcpkg-hash: '9a4b32849792e13bee1d24726f073b3881acae4165206ddf1a6378e44a4ddd05b3ee93f55ff46d8e8873b3cbcd06606212989e248f0bd615a5bf365070074079' cmake-version: '3.31.6' @@ -50,7 +66,7 @@ jobs: disable-terrapin: 'true' - name: Build Full ORT and Prepare Test Files - uses: microsoft/onnxruntime-github-actions/build-and-prep-ort-files@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-and-prep-ort-files@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 - name: Upload Test Data Artifact uses: actions/upload-artifact@v6 @@ -80,8 +96,20 @@ jobs: with: node-version: 20 + - name: Setup CCache + uses: actions/cache@v4 + with: + key: ccache | linux_minimal_build.yml | build_minimal_exceptions_disabled + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: vcpkg-cache | linux_minimal_build.yml | build_minimal_exceptions_disabled + path: ~/.cache/vcpkg + - name: Get Docker Image using Action - uses: microsoft/onnxruntime-github-actions/build-docker-image@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-docker-image@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 id: build_docker_image_step with: dockerfile: ${{ github.workspace }}/tools/ci_build/github/linux/docker/inference/x86_64/default/cpu/Dockerfile @@ -92,28 +120,32 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run Build 2 (Update) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: Debug # From original --config Debug mode: 'update' # CMake configure step extra_build_flags: >- --cmake_generator Ninja + --parallel --use_binskim_compliant_compile_flags + --use_cache --skip_tests --minimal_build --disable_exceptions --enable_training_ops - name: Run Build 2 (Build) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: Debug # From original --config Debug mode: 'build' # Actual build step extra_build_flags: >- --cmake_generator Ninja + --parallel --use_binskim_compliant_compile_flags + --use_cache --skip_tests --minimal_build --disable_exceptions @@ -141,8 +173,22 @@ jobs: with: node-version: 20 - - uses: microsoft/onnxruntime-github-actions/setup-build-tools@v0.0.9 + - name: Setup CCache + uses: actions/cache@v4 + with: + key: ccache | linux_minimal_build.yml | build_minimal_custom_ops + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: vcpkg-cache | linux_minimal_build.yml | build_minimal_custom_ops + path: ~/.cache/vcpkg + + - uses: microsoft/onnxruntime-github-actions/setup-build-tools@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: + ccache-version: 4.13.1 + ccache-hash: 626407a9b81dd86f8ec9867bff396b32dd1f00344f5b323526579a64f6d4104927f83e8d7a05ad9806fd78f4491e0adb4cff73388000a62050cb1b00766214ee vcpkg-version: '2025.08.27' vcpkg-hash: '9a4b32849792e13bee1d24726f073b3881acae4165206ddf1a6378e44a4ddd05b3ee93f55ff46d8e8873b3cbcd06606212989e248f0bd615a5bf365070074079' cmake-version: '3.31.6' @@ -151,7 +197,7 @@ jobs: disable-terrapin: 'true' - name: Build Full ORT and Prepare Test Files - uses: microsoft/onnxruntime-github-actions/build-minimal-ort-and-run-tests@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-minimal-ort-and-run-tests@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: reduced-ops-config-file: required_ops.ort_models.config enable-custom-ops: 'true' @@ -179,16 +225,31 @@ jobs: with: node-version: 20 - - uses: microsoft/onnxruntime-github-actions/setup-build-tools@v0.0.9 + - name: Setup CCache + uses: actions/cache@v4 + with: + key: ccache | linux_minimal_build.yml | build_minimal_type_reduction + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 with: + key: vcpkg-cache | linux_minimal_build.yml | build_minimal_type_reduction + path: ~/.cache/vcpkg + + - uses: microsoft/onnxruntime-github-actions/setup-build-tools@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 + with: + ccache-version: 4.13.1 + ccache-hash: 626407a9b81dd86f8ec9867bff396b32dd1f00344f5b323526579a64f6d4104927f83e8d7a05ad9806fd78f4491e0adb4cff73388000a62050cb1b00766214ee vcpkg-version: '2025.08.27' vcpkg-hash: '9a4b32849792e13bee1d24726f073b3881acae4165206ddf1a6378e44a4ddd05b3ee93f55ff46d8e8873b3cbcd06606212989e248f0bd615a5bf365070074079' cmake-version: '3.31.6' cmake-hash: '42395e20b10a8e9ef3e33014f9a4eed08d46ab952e02d2c1bbc8f6133eca0d7719fb75680f9bbff6552f20fcd1b73d86860f7f39388d631f98fb6f622b37cf04' add-cmake-to-path: 'true' disable-terrapin: 'true' + - name: Build Full ORT and Prepare Test Files - uses: microsoft/onnxruntime-github-actions/build-minimal-ort-and-run-tests@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-minimal-ort-and-run-tests@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: reduced-ops-config-file: required_ops_and_types.ort_models.config enable-type-reduction: 'true' @@ -215,8 +276,22 @@ jobs: with: node-version: 20 - - uses: microsoft/onnxruntime-github-actions/setup-build-tools@v0.0.9 + - name: Setup CCache + uses: actions/cache@v4 with: + key: ccache | linux_minimal_build.yml | build_minimal_globally_allowed_types + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: vcpkg-cache | linux_minimal_build.yml | build_minimal_globally_allowed_types + path: ~/.cache/vcpkg + + - uses: microsoft/onnxruntime-github-actions/setup-build-tools@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 + with: + ccache-version: 4.13.1 + ccache-hash: 626407a9b81dd86f8ec9867bff396b32dd1f00344f5b323526579a64f6d4104927f83e8d7a05ad9806fd78f4491e0adb4cff73388000a62050cb1b00766214ee vcpkg-version: '2025.08.27' vcpkg-hash: '9a4b32849792e13bee1d24726f073b3881acae4165206ddf1a6378e44a4ddd05b3ee93f55ff46d8e8873b3cbcd06606212989e248f0bd615a5bf365070074079' cmake-version: '3.31.6' @@ -225,7 +300,7 @@ jobs: disable-terrapin: 'true' - name: Build Full ORT and Prepare Test Files - uses: microsoft/onnxruntime-github-actions/build-minimal-ort-and-run-tests@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-minimal-ort-and-run-tests@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: globally_allowed_types: 'bool,float,int8_t,uint8_t' enable-type-reduction: 'true' @@ -253,8 +328,20 @@ jobs: with: node-version: 20 + - name: Setup CCache + uses: actions/cache@v4 + with: + key: ccache | linux_minimal_build.yml | build_extended_minimal + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: vcpkg-cache | linux_minimal_build.yml | build_extended_minimal + path: ~/.cache/vcpkg + - name: Get Docker Image using Action - uses: microsoft/onnxruntime-github-actions/build-docker-image@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-docker-image@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 id: build_docker_image_step with: dockerfile: ${{ github.workspace }}/tools/ci_build/github/linux/docker/inference/x86_64/default/cpu/Dockerfile @@ -266,7 +353,7 @@ jobs: - name: Run Build 5 (Update) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: Debug @@ -274,11 +361,13 @@ jobs: extra_build_flags: >- --cmake_generator Ninja --build_shared_lib + --parallel --use_binskim_compliant_compile_flags + --use_cache --minimal_build extended - name: Run Build 5 (Build) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: Debug @@ -286,10 +375,13 @@ jobs: extra_build_flags: >- --cmake_generator Ninja --build_shared_lib + --parallel --use_binskim_compliant_compile_flags + --use_cache --minimal_build extended + - name: Run Build 5 (Test) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: Debug @@ -297,7 +389,9 @@ jobs: extra_build_flags: >- --cmake_generator Ninja --build_shared_lib + --parallel --use_binskim_compliant_compile_flags + --use_cache --minimal_build extended # Job 6a: Regular build with python and all optional features disabled. @@ -319,7 +413,7 @@ jobs: submodules: false - name: Get Docker Image using Action - uses: microsoft/onnxruntime-github-actions/build-docker-image@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-docker-image@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 id: build_docker_image_step with: dockerfile: ${{ github.workspace }}/tools/ci_build/github/linux/docker/inference/x86_64/default/cpu/Dockerfile @@ -335,8 +429,20 @@ jobs: mkdir -p ${{ runner.temp }}/.test_data touch ${{ runner.temp }}/.test_data/include_no_operators.config + - name: Setup CCache + uses: actions/cache@v4 + with: + key: ccache | linux_minimal_build.yml | build_regular_no_optional + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: vcpkg-cache | linux_minimal_build.yml | build_regular_no_optional + path: ~/.cache/vcpkg + - name: Run Build 6a (Update) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: MinSizeRel @@ -344,14 +450,16 @@ jobs: extra_build_flags: >- --cmake_generator Ninja --build_wheel + --parallel --use_binskim_compliant_compile_flags + --use_cache --disable_ml_ops --disable_types string sparsetensor float4 float8 optional --include_ops_by_config /onnxruntime_src/build/.test_data/include_no_operators.config --cmake_extra_defines onnxruntime_BUILD_UNIT_TESTS=OFF - name: Run Build 6a (Build) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: MinSizeRel @@ -359,15 +467,16 @@ jobs: extra_build_flags: >- --cmake_generator Ninja --build_wheel + --parallel --use_binskim_compliant_compile_flags + --use_cache --disable_ml_ops --disable_types string sparsetensor float4 float8 optional --include_ops_by_config /onnxruntime_src/build/.test_data/include_no_operators.config --cmake_extra_defines onnxruntime_BUILD_UNIT_TESTS=OFF - - name: Run Build 6a (Test) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: MinSizeRel @@ -375,7 +484,9 @@ jobs: extra_build_flags: >- --cmake_generator Ninja --build_wheel + --parallel --use_binskim_compliant_compile_flags + --use_cache --disable_ml_ops --disable_types string sparsetensor float4 float8 optional --include_ops_by_config /onnxruntime_src/build/.test_data/include_no_operators.config @@ -406,7 +517,7 @@ jobs: touch ${{ runner.temp }}/.test_data/include_no_operators.config - name: Get Docker Image using Action - uses: microsoft/onnxruntime-github-actions/build-docker-image@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-docker-image@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 id: build_docker_image_step with: dockerfile: ${{ github.workspace }}/tools/ci_build/github/linux/docker/inference/x86_64/default/cpu/Dockerfile @@ -416,15 +527,29 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Setup CCache + uses: actions/cache@v4 + with: + key: ccache | linux_minimal_build.yml | build_minimal_no_optional + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: vcpkg-cache | linux_minimal_build.yml | build_minimal_no_optional + path: ~/.cache/vcpkg + - name: Run Build 6b (Update) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: MinSizeRel # From original --config MinSizeRel mode: 'update' extra_build_flags: >- --cmake_generator Ninja + --parallel --use_binskim_compliant_compile_flags + --use_cache --minimal_build --disable_exceptions --disable_ml_ops @@ -435,14 +560,16 @@ jobs: --cmake_extra_defines onnxruntime_BUILD_UNIT_TESTS=OFF - name: Run Build 6b (Build) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: MinSizeRel # From original --config MinSizeRel mode: 'build' extra_build_flags: >- --cmake_generator Ninja + --parallel --use_binskim_compliant_compile_flags + --use_cache --minimal_build --disable_exceptions --disable_ml_ops @@ -477,7 +604,7 @@ jobs: touch ${{ runner.temp }}/.test_data/include_no_operators.config - name: Get Docker Image using Action - uses: microsoft/onnxruntime-github-actions/build-docker-image@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-docker-image@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 id: build_docker_image_step with: dockerfile: ${{ github.workspace }}/tools/ci_build/github/linux/docker/inference/x86_64/default/cpu/Dockerfile @@ -493,15 +620,29 @@ jobs: mkdir -p ${{ runner.temp }}/.test_data touch ${{ runner.temp }}/.test_data/include_no_operators.config + - name: Setup CCache + uses: actions/cache@v4 + with: + key: ccache | linux_minimal_build.yml | build_extended_minimal_no_optional + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: vcpkg-cache | linux_minimal_build.yml | build_extended_minimal_no_optional + path: ~/.cache/vcpkg + - name: Run Build 6c (Update) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: MinSizeRel # From original --config MinSizeRel mode: 'update' extra_build_flags: >- --cmake_generator Ninja + --parallel --use_binskim_compliant_compile_flags + --use_cache --minimal_build extended --disable_exceptions --disable_ml_ops @@ -512,14 +653,16 @@ jobs: --cmake_extra_defines onnxruntime_BUILD_UNIT_TESTS=OFF - name: Run Build 6c (Build) - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: MinSizeRel # From original --config MinSizeRel mode: 'build' extra_build_flags: >- --cmake_generator Ninja + --parallel --use_binskim_compliant_compile_flags + --use_cache --minimal_build extended --disable_exceptions --disable_ml_ops @@ -558,7 +701,7 @@ jobs: path: ${{ runner.temp }}/.test_data/ - name: Get Docker Image using Action - uses: microsoft/onnxruntime-github-actions/build-docker-image@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-docker-image@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 id: build_docker_image_step with: dockerfile: ${{ github.workspace }}/tools/ci_build/github/linux/docker/inference/x86_64/default/cpu/Dockerfile @@ -574,6 +717,18 @@ jobs: ndk-version: 28.0.13004108 # Use default android-sdk-root if not specified + - name: Setup CCache + uses: actions/cache@v4 + with: + key: ccache | linux_minimal_build.yml | build_extended_minimal_android + path: ~/.cache/ccache + + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: vcpkg-cache | linux_minimal_build.yml | build_extended_minimal_android + path: ~/.cache/vcpkg + - name: Run Build 7 (Using docker run) shell: bash run: | @@ -593,7 +748,11 @@ jobs: export ANDROID_HOME=/usr/local/lib/android/sdk fi + # mount `~/.cache` inside docker. assume `onnxruntimedev` is the container user (should match the docker-file specified earlier) + mkdir -p ~/.cache/vcpkg + docker run --rm \ + --volume ~/.cache:/home/onnxruntimedev/.cache \ --volume ${{ env.BUILD_SOURCES_DIRECTORY }}:/onnxruntime_src \ --volume ${{ runner.temp }}:/build \ --volume $ANDROID_HOME:/android_home \ @@ -607,7 +766,9 @@ jobs: --cmake_generator Ninja \ --config MinSizeRel \ --skip_submodule_sync \ - --parallel --use_binskim_compliant_compile_flags \ + --parallel \ + --use_binskim_compliant_compile_flags \ + --use_cache \ --android \ --android_sdk_path /android_home \ --android_ndk_path /ndk_home \ diff --git a/.github/workflows/linux_tensorrt_ci.yml b/.github/workflows/linux_tensorrt_ci.yml index e7e17eff75d7e..dd53a9f88ff52 100644 --- a/.github/workflows/linux_tensorrt_ci.yml +++ b/.github/workflows/linux_tensorrt_ci.yml @@ -54,7 +54,7 @@ jobs: # --- Build the Docker image needed for testing --- - name: Build Docker Image for Testing - uses: microsoft/onnxruntime-github-actions/build-docker-image@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-docker-image@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 id: build_docker_image_step with: dockerfile: ${{ github.workspace }}/tools/ci_build/github/linux/docker/Dockerfile.manylinux2_28_cuda @@ -97,7 +97,7 @@ jobs: # So build.py --build_dir build/Release inside the container correctly finds the artifacts. - name: Test ONNX Runtime id: test_step - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: Release diff --git a/.github/workflows/reusable_linux_build.yml b/.github/workflows/reusable_linux_build.yml index 9d2700683bedb..c5de2fcbfc762 100644 --- a/.github/workflows/reusable_linux_build.yml +++ b/.github/workflows/reusable_linux_build.yml @@ -89,7 +89,7 @@ jobs: python-version: ${{ inputs.python_version }} - name: Build Docker Image (${{ inputs.architecture }} / ${{ inputs.build_config }}) - uses: microsoft/onnxruntime-github-actions/build-docker-image@v0.0.9 + uses: microsoft/onnxruntime-github-actions/build-docker-image@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 id: build_docker_image_step with: dockerfile: ${{ github.workspace }}/${{ inputs.dockerfile_path }} @@ -100,41 +100,57 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + # FUTURE WORK: Re-enable once total cache size limit has been increased. + # We're prioritising getting vcpkg cache on all workflows and pipelines due to + # reliability issues when fetching pkg contents from upstream. + - name: Setup CCache + uses: actions/cache@v4 + with: + key: 'ccache | "${{ inputs.job_identifier }}" | "${{ inputs.architecture }}" | "${{ inputs.build_config }}"' + path: ~/.cache/cache + + # same idea as ccache, but for vcpkg artifacts. ideally we'd use vcpkg's nuget remote cache facility instead. + - name: Setup VCPKG Cache + uses: actions/cache@v4 + with: + key: '"vcpkg-cache" | "${{ inputs.job_identifier }}" | "${{ inputs.architecture }}" | "${{ inputs.build_config }}"' + path: ~/.cache/vcpkg + # ------------- Update Step (CMake Generation) ------------- - name: Generate Build Files (CMake) (${{ inputs.architecture }} / ${{ inputs.build_config }}) id: update_step - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: ${{ inputs.build_config }} mode: 'update' execution_providers: ${{ inputs.execution_providers }} # Pass down EP list - extra_build_flags: ${{ inputs.extra_build_flags }} + extra_build_flags: ${{ inputs.extra_build_flags }} --use_cache python_path_prefix: ${{ inputs.python_path_prefix }} # ------------- Build Step (Compilation) ------------- - name: Build ONNX Runtime (${{ inputs.architecture }} / ${{ inputs.build_config }}) id: build_step - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: ${{ inputs.build_config }} mode: 'build' execution_providers: ${{ inputs.execution_providers }} # Pass down EP list - extra_build_flags: ${{ inputs.extra_build_flags }} + extra_build_flags: ${{ inputs.extra_build_flags }} --use_cache python_path_prefix: ${{ inputs.python_path_prefix }} # ------------- Test Step ------------- - name: Test ONNX Runtime (${{ inputs.architecture }} / ${{ inputs.build_config }}) id: test_step if: inputs.run_tests == true - uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@v0.0.9 + uses: microsoft/onnxruntime-github-actions/run-build-script-in-docker@8bad63a3c05d448311dfa8e5f531171c97471aa1 # v0.0.12 with: docker_image: ${{ steps.build_docker_image_step.outputs.full-image-name }} build_config: ${{ inputs.build_config }} mode: 'test' execution_providers: ${{ inputs.execution_providers }} # Pass down EP list - extra_build_flags: ${{ inputs.extra_build_flags }} + extra_build_flags: ${{ inputs.extra_build_flags }} --use_cache python_path_prefix: ${{ inputs.python_path_prefix }} # ------------- Prepare Artifact Step ------------- diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 6ae25ccc0bf3e..e9974fc66de4d 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -38,6 +38,7 @@ jobs: needs: precheck uses: ./.github/workflows/linux-wasm-ci-build-and-test-workflow.yml with: + job_name: wasm_Debug build_config: Debug extra_build_args: "--enable_wasm_profiling" build_jsep: true @@ -47,6 +48,7 @@ jobs: needs: precheck uses: ./.github/workflows/linux-wasm-ci-build-and-test-workflow.yml with: + job_name: wasm_Release build_config: Release extra_build_args: "--target onnxruntime_webassembly --skip_tests --disable_rtti" build_jsep: true @@ -56,6 +58,7 @@ jobs: needs: precheck uses: ./.github/workflows/linux-wasm-ci-build-and-test-workflow.yml with: + job_name: wasm_Release_static_library build_config: Release extra_build_args: "--skip_tests --disable_rtti --build_wasm_static_lib" use_vcpkg: false @@ -68,6 +71,7 @@ jobs: - wasm_Debug uses: ./.github/workflows/windows-web-ci-workflow.yml with: + job_name: web_Debug commit_override: ${{ needs.precheck.outputs.commit_sha }} build_config: Debug @@ -77,5 +81,6 @@ jobs: - wasm_Release uses: ./.github/workflows/windows-web-ci-workflow.yml with: + job_name: web_Release commit_override: ${{ needs.precheck.outputs.commit_sha }} build_config: Release diff --git a/.github/workflows/windows-web-ci-workflow.yml b/.github/workflows/windows-web-ci-workflow.yml index 266177623e9c5..9b40f8ee1dc17 100644 --- a/.github/workflows/windows-web-ci-workflow.yml +++ b/.github/workflows/windows-web-ci-workflow.yml @@ -4,6 +4,9 @@ description: "Windows Web CI pipeline for building and testing ONNX Runtime Web" on: workflow_call: inputs: + job_name: # workflow-scope unique key + required: true + type: string commit_override: type: string default: "" diff --git a/tools/ci_build/build.py b/tools/ci_build/build.py index 54c8d412c02c6..7eeb9cb59d0b2 100644 --- a/tools/ci_build/build.py +++ b/tools/ci_build/build.py @@ -1723,7 +1723,18 @@ def run_onnxruntime_tests(args, source_dir, ctest_path, build_dir, configs): test_output = f"--gtest_output=xml:{cwd}/{exe}.{config}.results.xml" run_subprocess([os.path.join(cwd, exe), test_output], cwd=cwd, dll_path=dll_path) else: - ctest_cmd = [ctest_path, "--build-config", config, "--verbose", "--timeout", args.ctest_timeout] + num_parallel_jobs = number_of_parallel_jobs(args) + ctest_cmd = [ + ctest_path, + "--build-config", + config, + "--verbose", + "--timeout", + args.ctest_timeout, + "--parallel", + str(num_parallel_jobs), + "--output-on-failure", + ] run_subprocess(ctest_cmd, cwd=cwd, dll_path=dll_path) if args.enable_pybind: diff --git a/tools/ci_build/github/linux/docker/inference/aarch64/python/cpu/scripts/install_centos.sh b/tools/ci_build/github/linux/docker/inference/aarch64/python/cpu/scripts/install_centos.sh index 1ced7cd2f90c8..0517fa7be9daa 100755 --- a/tools/ci_build/github/linux/docker/inference/aarch64/python/cpu/scripts/install_centos.sh +++ b/tools/ci_build/github/linux/docker/inference/aarch64/python/cpu/scripts/install_centos.sh @@ -1,7 +1,11 @@ #!/bin/bash set -e -os_major_version=$(tr -dc '0-9.' < /etc/redhat-release |cut -d \. -f1) +os_major_version=$(tr -dc '0-9.' /dev/null; then + dnf install -y ccache # FIXME: base image should already have ccache installed +fi diff --git a/tools/ci_build/github/linux/docker/inference/x86_64/python/openvino/Dockerfile b/tools/ci_build/github/linux/docker/inference/x86_64/python/openvino/Dockerfile index 2ffe21159fd1f..7a29fd7fc728c 100644 --- a/tools/ci_build/github/linux/docker/inference/x86_64/python/openvino/Dockerfile +++ b/tools/ci_build/github/linux/docker/inference/x86_64/python/openvino/Dockerfile @@ -9,6 +9,9 @@ ARG BUILD_USER=onnxruntimedev USER root WORKDIR / +# FIXME: base image should already have ccache installed +RUN if ! command -v ccache; then dnf install -y ccache; fi + RUN dnf install -y --nodocs \ wget \ tar \ diff --git a/tools/ci_build/github/linux/docker/scripts/manylinux/install_centos.sh b/tools/ci_build/github/linux/docker/scripts/manylinux/install_centos.sh index a487bf7f91507..093da075be13c 100755 --- a/tools/ci_build/github/linux/docker/scripts/manylinux/install_centos.sh +++ b/tools/ci_build/github/linux/docker/scripts/manylinux/install_centos.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -os_major_version=$(tr -dc '0-9.' < /etc/redhat-release |cut -d \. -f1) +os_major_version=$(tr -dc '0-9.' /dev/null; then + "$PACKAGE_MANAGER" install -y ccache # FIXME: base image should already have ccache installed +fi diff --git a/tools/ci_build/github/linux/ort_minimal/nnapi_minimal_build_minimal_ort_and_run_tests.sh b/tools/ci_build/github/linux/ort_minimal/nnapi_minimal_build_minimal_ort_and_run_tests.sh index 946820299c6a7..1a4e0cf77c57b 100755 --- a/tools/ci_build/github/linux/ort_minimal/nnapi_minimal_build_minimal_ort_and_run_tests.sh +++ b/tools/ci_build/github/linux/ort_minimal/nnapi_minimal_build_minimal_ort_and_run_tests.sh @@ -21,6 +21,9 @@ python3 "$ORT_ROOT/tools/ci_build/build.py" \ --build_dir "$MIN_BUILD_DIR" \ --config Debug \ --skip_submodule_sync \ + --use_cache \ + --use_vcpkg \ + --use_vcpkg_ms_internal_asset_cache \ --parallel \ --cmake_generator=Ninja \ --use_nnapi \ @@ -35,7 +38,7 @@ python3 "$ORT_ROOT/tools/ci_build/build.py" \ --disable_generation_ops \ --disable_exceptions \ --include_ops_by_config "$ORT_ROOT/onnxruntime/test/testdata/required_ops_and_types.config" \ - --skip_tests --use_vcpkg --use_vcpkg_ms_internal_asset_cache + --skip_tests # Push onnxruntime_test_all and testdata to emulator adb push "$MIN_BUILD_DIR/Debug/onnxruntime_test_all" /data/local/tmp/ From 651c7cf7a6d75d1a1a10804efc8dc9ace45b5134 Mon Sep 17 00:00:00 2001 From: David Fan <30608893+jiafatom@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:26:53 -0700 Subject: [PATCH 05/14] Shell injection constant strings (#27840) ### Description See below ### Motivation and Context Summary:The vulnerability lies in the ONNX Runtime's validate_package.py script, which uses unsanitized string concatenation with os.system() to construct shell commands. This allows attackers to inject arbitrary shell commands via the --package_name argument, leading to potential remote code execution. The issue affects the release validation pipeline, which operates with elevated privileges, exposing sensitive credentials and secrets. The root cause is the lack of input sanitization and the use of os.system() for command execution. Affected code locations: tools/nuget/validate_package.py line 241: os.system("tar zxvf " + package_name) tools/nuget/validate_package.py line 339: os.system("copy " + full_nuget_path + " " + nupkg_copy_name) Suggested fix: Replace os.system() with subprocess.run() using argument lists (no shell interpolation): ``` # Instead of: os.system("tar zxvf " + package_name) subprocess.run(["tar", "zxvf", package_name], check=True) # Instead of: os.system("copy " + full_nuget_path + " " + nupkg_copy_name) shutil.copy2(full_nuget_path, nupkg_copy_name) ``` --- tools/nuget/validate_package.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/nuget/validate_package.py b/tools/nuget/validate_package.py index 0ad1fc07eafd7..59e88ea15e7c6 100644 --- a/tools/nuget/validate_package.py +++ b/tools/nuget/validate_package.py @@ -5,6 +5,8 @@ import glob import os import re +import shutil +import subprocess import sys import zipfile # Available Python 3.2 or higher @@ -238,7 +240,7 @@ def validate_tarball(args): package_folder = re.search("(.*)[.].*", package_name).group(1) print("tar zxvf " + package_name) - os.system("tar zxvf " + package_name) + subprocess.run(["tar", "zxvf", package_name], check=True) is_windows_ai_package = False zip_file = None @@ -336,7 +338,7 @@ def validate_nuget(args): # Make a copy of the Nuget package print("Copying [" + full_nuget_path + "] -> [" + nupkg_copy_name + "], and extracting its contents") - os.system("copy " + full_nuget_path + " " + nupkg_copy_name) + shutil.copy2(full_nuget_path, nupkg_copy_name) # Convert nupkg to zip os.rename(nupkg_copy_name, zip_copy_name) From aeda0c773bfd4d588cd10d4c3b2fd5ea391860ce Mon Sep 17 00:00:00 2001 From: Guenther Schmuelling Date: Thu, 26 Mar 2026 12:08:42 -0700 Subject: [PATCH 06/14] update jsvascript dependencies (#27838) --- js/common/package-lock.json | 17 +- js/node/package-lock.json | 180 +++------ js/node/package.json | 2 +- js/package-lock.json | 647 ++++++++++-------------------- js/package.json | 4 +- js/react_native/package-lock.json | 25 +- js/web/package-lock.json | 329 +++++---------- 7 files changed, 372 insertions(+), 832 deletions(-) diff --git a/js/common/package-lock.json b/js/common/package-lock.json index fa3d42faffb52..dc125288eaee5 100644 --- a/js/common/package-lock.json +++ b/js/common/package-lock.json @@ -265,12 +265,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -639,12 +640,12 @@ } }, "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" } }, "path-type": { diff --git a/js/node/package-lock.json b/js/node/package-lock.json index 8b8582d06e779..d94de095555b3 100644 --- a/js/node/package-lock.json +++ b/js/node/package-lock.json @@ -16,7 +16,7 @@ ], "dependencies": { "adm-zip": "^0.5.16", - "global-agent": "^3.0.0", + "global-agent": "^4.1.3", "onnxruntime-common": "file:../common" }, "devDependencies": { @@ -158,12 +158,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info." - }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -258,6 +252,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -274,6 +269,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -286,11 +282,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -310,6 +301,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -318,15 +310,11 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -340,6 +328,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -377,16 +366,15 @@ } }, "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz", + "integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==", + "license": "BSD-3-Clause", "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" + "globalthis": "^1.0.2", + "matcher": "^4.0.0", + "semver": "^7.3.5", + "serialize-error": "^8.1.0" }, "engines": { "node": ">=10.0" @@ -396,6 +384,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" @@ -411,6 +400,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -428,6 +418,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -471,11 +462,6 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, "node_modules/jsonc": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jsonc/-/jsonc-2.0.0.tgz", @@ -512,14 +498,18 @@ "dev": true }, "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz", + "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==", + "license": "MIT", "dependencies": { "escape-string-regexp": "^4.0.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/minimist": { @@ -586,6 +576,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -664,22 +655,6 @@ "node": ">=0.10.0" } }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -691,17 +666,13 @@ "node": ">=10" } }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==" - }, "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", "dependencies": { - "type-fest": "^0.13.1" + "type-fest": "^0.20.2" }, "engines": { "node": ">=10" @@ -710,11 +681,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -779,9 +745,10 @@ } }, "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -988,11 +955,6 @@ "color-convert": "^2.0.1" } }, - "boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==" - }, "chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -1077,11 +1039,6 @@ "object-keys": "^1.1.1" } }, - "detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" - }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1107,11 +1064,6 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, - "es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" - }, "escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1147,16 +1099,14 @@ "dev": true }, "global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz", + "integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==", "requires": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" + "globalthis": "^1.0.2", + "matcher": "^4.0.0", + "semver": "^7.3.5", + "serialize-error": "^8.1.0" } }, "globalthis": { @@ -1217,11 +1167,6 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" - }, "jsonc": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jsonc/-/jsonc-2.0.0.tgz", @@ -1253,9 +1198,9 @@ "dev": true }, "matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz", + "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==", "requires": { "escape-string-regexp": "^4.0.0" } @@ -1376,42 +1321,19 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, - "roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "requires": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - } - }, "semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==" }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==" - }, "serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", "requires": { - "type-fest": "^0.13.1" + "type-fest": "^0.20.2" } }, - "sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" - }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1458,9 +1380,9 @@ } }, "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==" + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" }, "universalify": { "version": "2.0.1", diff --git a/js/node/package.json b/js/node/package.json index 4d35ec8c424d5..18c2b2ce9c905 100644 --- a/js/node/package.json +++ b/js/node/package.json @@ -14,7 +14,7 @@ "version": "1.25.0", "dependencies": { "adm-zip": "^0.5.16", - "global-agent": "^3.0.0", + "global-agent": "^4.1.3", "onnxruntime-common": "file:../common" }, "scripts": { diff --git a/js/package-lock.json b/js/package-lock.json index 1ba8fc900bbd8..29d45184920d1 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -4,13 +4,14 @@ "requires": true, "packages": { "": { + "name": "js", "license": "MIT", "devDependencies": { "@eslint/compat": "^1.4.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.38.0", "@types/fs-extra": "^11.0.4", - "@types/global-agent": "^2.1.3", + "@types/global-agent": "^3.0.0", "@types/mocha": "^10.0.2", "@types/node": "^20.10.0", "@types/npmlog": "^4.1.4", @@ -27,7 +28,7 @@ "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-unicorn": "^62.0.0", "fs-extra": "^11.2.0", - "global-agent": "^3.0", + "global-agent": "^4.1.3", "globals": "^16.4.0", "jszip": "^3.10.1", "mocha": "^11.0.1", @@ -979,9 +980,9 @@ } }, "node_modules/@types/global-agent": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@types/global-agent/-/global-agent-2.1.3.tgz", - "integrity": "sha512-rGtZZcgZcKWuKNTkGBGsqyOQ7Nn2MjXh4+xeZbf+5b5KMUx8H1rTqLRackxos7pUlreszbYjQcop5JvqCnZlLw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-OmvaPJtTaY/wd1hxelLJmf8oKQpmKZdrlfQ+MWL59eKSEHJDDEifIo69248bdJ0yLIN+iMNQ6sKMtnwU6AxajA==", "dev": true, "license": "MIT" }, @@ -1231,13 +1232,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -1337,10 +1338,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1352,16 +1354,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1386,19 +1378,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -1632,23 +1611,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1871,42 +1833,19 @@ "license": "MIT" }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/ci-info": { @@ -1947,14 +1886,18 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/color-convert": { @@ -2178,17 +2121,10 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT" - }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2389,13 +2325,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT" - }, "node_modules/esbuild": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", @@ -3079,20 +3008,6 @@ "node": ">=14.14" } }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3168,6 +3083,7 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -3273,13 +3189,13 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3289,18 +3205,16 @@ } }, "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz", + "integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" + "globalthis": "^1.0.2", + "matcher": "^4.0.0", + "semver": "^7.3.5", + "serialize-error": "^8.1.0" }, "engines": { "node": ">=10.0" @@ -3644,18 +3558,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -3871,6 +3773,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -4119,13 +4031,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC" - }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -4233,9 +4138,9 @@ } }, "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz", + "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4243,6 +4148,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/math-intrinsics": { @@ -4312,31 +4220,32 @@ } }, "node_modules/mocha": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.0.1.tgz", - "integrity": "sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A==", + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", "dev": true, "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", + "chokidar": "^4.0.1", "debug": "^4.3.5", - "diff": "^5.2.0", + "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", + "minimatch": "^9.0.5", "ms": "^2.1.3", + "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { @@ -4358,16 +4267,19 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mocha/node_modules/supports-color": { @@ -4405,15 +4317,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npmlog": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-7.0.1.tgz", @@ -4821,15 +4724,17 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, + "license": "MIT", "engines": { - "node": ">=8.10.0" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/reflect.getprototypeof": { @@ -4903,6 +4808,7 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4961,24 +4867,6 @@ "node": ">=0.10.0" } }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5091,21 +4979,14 @@ "node": ">=10" } }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT" - }, "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.13.1" + "type-fest": "^0.20.2" }, "engines": { "node": ">=10" @@ -5114,19 +4995,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -5345,13 +5213,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5635,6 +5496,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -5928,9 +5802,9 @@ } }, "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true, "license": "Apache-2.0" }, @@ -5939,6 +5813,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -5975,36 +5850,38 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-unparser": { @@ -6552,9 +6429,9 @@ } }, "@types/global-agent": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@types/global-agent/-/global-agent-2.1.3.tgz", - "integrity": "sha512-rGtZZcgZcKWuKNTkGBGsqyOQ7Nn2MjXh4+xeZbf+5b5KMUx8H1rTqLRackxos7pUlreszbYjQcop5JvqCnZlLw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-OmvaPJtTaY/wd1hxelLJmf8oKQpmKZdrlfQ+MWL59eKSEHJDDEifIo69248bdJ0yLIN+iMNQ6sKMtnwU6AxajA==", "dev": true }, "@types/json-schema": { @@ -6712,12 +6589,12 @@ } }, "minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" } } } @@ -6775,9 +6652,9 @@ "requires": {} }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -6786,12 +6663,6 @@ "uri-js": "^4.2.2" } }, - "ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true - }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -6807,16 +6678,6 @@ "color-convert": "^2.0.1" } }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, "aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -6972,18 +6833,6 @@ "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", "dev": true }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "dev": true - }, "brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -7111,30 +6960,12 @@ "dev": true }, "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } + "readdirp": "^4.0.1" } }, "ci-info": { @@ -7161,13 +6992,13 @@ } }, "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, @@ -7324,16 +7155,10 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, - "detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true - }, "diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true }, "dir-compare": { @@ -7490,12 +7315,6 @@ "is-symbol": "^1.0.4" } }, - "es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true - }, "esbuild": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", @@ -7987,13 +7806,6 @@ "universalify": "^2.0.0" } }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8111,12 +7923,12 @@ } }, "minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" } } } @@ -8131,17 +7943,15 @@ } }, "global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz", + "integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==", "dev": true, "requires": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" + "globalthis": "^1.0.2", + "matcher": "^4.0.0", + "semver": "^7.3.5", + "serialize-error": "^8.1.0" } }, "globals": { @@ -8346,15 +8156,6 @@ "has-bigints": "^1.0.2" } }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, "is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -8481,6 +8282,12 @@ "has-tostringtag": "^1.0.2" } }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -8636,12 +8443,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true - }, "json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -8727,9 +8528,9 @@ } }, "matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz", + "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==", "dev": true, "requires": { "escape-string-regexp": "^4.0.0" @@ -8779,30 +8580,31 @@ "dev": true }, "mocha": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.0.1.tgz", - "integrity": "sha512-+3GkODfsDG71KSCQhc4IekSW+ItCK/kiez1Z28ksWvYhKXV/syxMlerR/sC7whDp7IyreZ4YxceMLdTs5hQE8A==", + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", "dev": true, "requires": { - "ansi-colors": "^4.1.3", "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", + "chokidar": "^4.0.1", "debug": "^4.3.5", - "diff": "^5.2.0", + "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", + "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", + "minimatch": "^9.0.5", "ms": "^2.1.3", + "picocolors": "^1.1.1", "serialize-javascript": "^6.0.2", "strip-json-comments": "^3.1.1", "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "dependencies": { @@ -8816,12 +8618,12 @@ } }, "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" } }, "supports-color": { @@ -8853,12 +8655,6 @@ "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", "dev": true }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, "npmlog": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-7.0.1.tgz", @@ -9138,13 +8934,10 @@ } }, "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true }, "reflect.getprototypeof": { "version": "1.0.10", @@ -9226,20 +9019,6 @@ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true }, - "roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "requires": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - } - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9311,27 +9090,13 @@ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true - }, "serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", "dev": true, "requires": { - "type-fest": "^0.13.1" - }, - "dependencies": { - "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true - } + "type-fest": "^0.20.2" } }, "serialize-javascript": { @@ -9499,12 +9264,6 @@ "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "dev": true }, - "sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true - }, "stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -9694,6 +9453,12 @@ "prelude-ls": "^1.2.1" } }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, "typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -9890,9 +9655,9 @@ } }, "workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true }, "wrap-ansi": { @@ -9924,24 +9689,24 @@ "dev": true }, "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" } }, "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true }, "yargs-unparser": { diff --git a/js/package.json b/js/package.json index cb8b09f4247a6..65cfa4a59e4e3 100644 --- a/js/package.json +++ b/js/package.json @@ -4,7 +4,7 @@ "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.38.0", "@types/fs-extra": "^11.0.4", - "@types/global-agent": "^2.1.3", + "@types/global-agent": "^3.0.0", "@types/mocha": "^10.0.2", "@types/node": "^20.10.0", "@types/npmlog": "^4.1.4", @@ -21,7 +21,7 @@ "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-unicorn": "^62.0.0", "fs-extra": "^11.2.0", - "global-agent": "^3.0", + "global-agent": "^4.1.3", "globals": "^16.4.0", "jszip": "^3.10.1", "mocha": "^11.0.1", diff --git a/js/react_native/package-lock.json b/js/react_native/package-lock.json index 6073725939e87..fdbc414b284a7 100644 --- a/js/react_native/package-lock.json +++ b/js/react_native/package-lock.json @@ -92,7 +92,6 @@ "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -1943,7 +1942,6 @@ "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-compilation-targets": "^7.26.5", @@ -3341,9 +3339,9 @@ } }, "node_modules/babel-plugin-module-resolver/node_modules/minimatch": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", - "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.7.tgz", + "integrity": "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==", "dev": true, "license": "ISC", "dependencies": { @@ -3511,7 +3509,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -4349,9 +4346,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.5.tgz", + "integrity": "sha512-cK9c5I/DwIOI7/Q7AlGN3DuTdwN61gwSfL8rvuVPK+0mcCNHHGxRrpiFtaZZRfRMJL3Gl8B2AFlBG6qXf03w9A==", "dev": true, "funding": [ { @@ -4361,7 +4358,7 @@ ], "license": "MIT", "dependencies": { - "strnum": "^1.1.1" + "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" @@ -7006,7 +7003,6 @@ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7036,7 +7032,6 @@ "integrity": "sha512-yvQIX+ZXOHMFnhmwZ1fBpRI/53k+iLN8DxVf24Fx4ABU63RGAYfyCZC0/3W+5OUVx4KSIZUv4Tv+/NGIieBOwg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.6.3", "@react-native-community/cli": "12.3.7", @@ -7241,9 +7236,9 @@ } }, "node_modules/react-native-builder-bob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { diff --git a/js/web/package-lock.json b/js/web/package-lock.json index 0e6d47f952c43..a02b86ec1ddc9 100644 --- a/js/web/package-lock.json +++ b/js/web/package-lock.json @@ -531,16 +531,16 @@ } }, "node_modules/browserstack-local": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/browserstack-local/-/browserstack-local-1.5.1.tgz", - "integrity": "sha512-T/wxyWDzvBHbDvl7fZKpFU7mYze6nrUkBhNy+d+8bXBqgQX10HTYvajIGO0wb49oGSLCPM0CMZTV/s7e6LF0sA==", + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/browserstack-local/-/browserstack-local-1.5.12.tgz", + "integrity": "sha512-xrdpG4rw6Ktxa/gM8x0esnohFlw0V33bQiUX08rrHWKbnJAG57KTHGvJ4mvgc9eRL63pEKal+WuNDg3vEUz4hA==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^6.0.2", "https-proxy-agent": "^5.0.1", "is-running": "^2.1.0", - "ps-tree": "=1.2.0", - "temp-fs": "^0.9.9" + "tree-kill": "^1.2.2" } }, "node_modules/browserstack-local/node_modules/https-proxy-agent": { @@ -1061,12 +1061,6 @@ "node": ">= 0.4" } }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, "node_modules/edge-launcher": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/edge-launcher/-/edge-launcher-1.2.2.tgz", @@ -1243,21 +1237,6 @@ "node": ">=0.8.0" } }, - "node_modules/event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dev": true, - "dependencies": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -1415,10 +1394,11 @@ "license": "Apache-2.0" }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.6", @@ -1440,12 +1420,6 @@ } } }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true - }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -2369,12 +2343,6 @@ "node": ">=10" } }, - "node_modules/map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", - "dev": true - }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -2484,12 +2452,13 @@ } }, "node_modules/minimatch": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.2.tgz", - "integrity": "sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA==", + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz", + "integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=10" @@ -2722,15 +2691,6 @@ "node": "*" } }, - "node_modules/pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dev": true, - "dependencies": { - "through": "~2.3" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -2786,21 +2746,6 @@ "node": ">=12.0.0" } }, - "node_modules/ps-tree": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", - "dev": true, - "dependencies": { - "event-stream": "=3.3.4" - }, - "bin": { - "ps-tree": "bin/ps-tree.js" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -3227,18 +3172,44 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", "dev": true, + "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "debug": "~4.4.1" }, "engines": { "node": ">=10.0.0" } }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -3248,18 +3219,6 @@ "node": ">= 8" } }, - "node_modules/split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", - "dev": true, - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, "node_modules/sprintf-js": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", @@ -3276,15 +3235,6 @@ "node": ">= 0.6" } }, - "node_modules/stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", - "dev": true, - "dependencies": { - "duplexer": "~0.1.1" - } - }, "node_modules/streamroller": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", @@ -3391,36 +3341,6 @@ "node": ">=4" } }, - "node_modules/temp-fs": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/temp-fs/-/temp-fs-0.9.9.tgz", - "integrity": "sha512-WfecDCR1xC9b0nsrzSaxPf3ZuWeWLUWblW4vlDQAa1biQaKHiImHnJfeQocQe/hXKMcolRzgkcVX/7kK4zoWbw==", - "dev": true, - "dependencies": { - "rimraf": "~2.5.2" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/temp-fs/node_modules/rimraf": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", - "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", - "dev": true, - "dependencies": { - "glob": "^7.0.5" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -3451,6 +3371,16 @@ "node": ">=0.6" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4119,16 +4049,15 @@ } }, "browserstack-local": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/browserstack-local/-/browserstack-local-1.5.1.tgz", - "integrity": "sha512-T/wxyWDzvBHbDvl7fZKpFU7mYze6nrUkBhNy+d+8bXBqgQX10HTYvajIGO0wb49oGSLCPM0CMZTV/s7e6LF0sA==", + "version": "1.5.12", + "resolved": "https://registry.npmjs.org/browserstack-local/-/browserstack-local-1.5.12.tgz", + "integrity": "sha512-xrdpG4rw6Ktxa/gM8x0esnohFlw0V33bQiUX08rrHWKbnJAG57KTHGvJ4mvgc9eRL63pEKal+WuNDg3vEUz4hA==", "dev": true, "requires": { "agent-base": "^6.0.2", "https-proxy-agent": "^5.0.1", "is-running": "^2.1.0", - "ps-tree": "=1.2.0", - "temp-fs": "^0.9.9" + "tree-kill": "^1.2.2" }, "dependencies": { "https-proxy-agent": { @@ -4536,12 +4465,6 @@ "gopd": "^1.2.0" } }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true - }, "edge-launcher": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/edge-launcher/-/edge-launcher-1.2.2.tgz", @@ -4683,21 +4606,6 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, - "event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dev": true, - "requires": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, "eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -4832,9 +4740,9 @@ "integrity": "sha512-Ni+KCqYquU30UEgGkrrwpbYtUcUmNuLFcQ5Xdy9DK7WUaji+AAov+Bf12FEYmu0eI15y31oD38utnBexe0cAYA==" }, "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, "follow-redirects": { @@ -4843,12 +4751,6 @@ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true - }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -5572,12 +5474,6 @@ "yallist": "^4.0.0" } }, - "map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", - "dev": true - }, "matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -5653,12 +5549,12 @@ "dev": true }, "minimatch": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.2.tgz", - "integrity": "sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA==", + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz", + "integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" } }, "minimist": { @@ -5827,15 +5723,6 @@ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, - "pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dev": true, - "requires": { - "through": "~2.3" - } - }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -5878,15 +5765,6 @@ "long": "^5.0.0" } }, - "ps-tree": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", - "dev": true, - "requires": { - "event-stream": "=3.3.4" - } - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -6183,13 +6061,30 @@ } }, "socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", "dev": true, "requires": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "debug": "~4.4.1" + }, + "dependencies": { + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } } }, "source-map": { @@ -6198,15 +6093,6 @@ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true }, - "split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", - "dev": true, - "requires": { - "through": "2" - } - }, "sprintf-js": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", @@ -6220,15 +6106,6 @@ "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true }, - "stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", - "dev": true, - "requires": { - "duplexer": "~0.1.1" - } - }, "streamroller": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", @@ -6307,32 +6184,6 @@ "has-flag": "^3.0.0" } }, - "temp-fs": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/temp-fs/-/temp-fs-0.9.9.tgz", - "integrity": "sha512-WfecDCR1xC9b0nsrzSaxPf3ZuWeWLUWblW4vlDQAa1biQaKHiImHnJfeQocQe/hXKMcolRzgkcVX/7kK4zoWbw==", - "dev": true, - "requires": { - "rimraf": "~2.5.2" - }, - "dependencies": { - "rimraf": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", - "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", - "dev": true, - "requires": { - "glob": "^7.0.5" - } - } - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -6354,6 +6205,12 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", From 1c05b13ddef3f40fcca7781d437029848d3b2fdb Mon Sep 17 00:00:00 2001 From: Jie Chen Date: Fri, 27 Mar 2026 23:31:26 +0800 Subject: [PATCH 07/14] Fix WebGPU buffer segment offset alignment (#27853) Align maxStorageBufferBindingSize down to the nearest multiple of minStorageBufferOffsetAlignment after querying device limits. This ensures that when large buffers are split into segments, each segment's byte offset satisfies WebGPU's bind group offset alignment requirement (typically 256 bytes). --- onnxruntime/core/providers/webgpu/webgpu_context.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/onnxruntime/core/providers/webgpu/webgpu_context.cc b/onnxruntime/core/providers/webgpu/webgpu_context.cc index c61d5826cb885..ec20bf2fdbdfb 100644 --- a/onnxruntime/core/providers/webgpu/webgpu_context.cc +++ b/onnxruntime/core/providers/webgpu/webgpu_context.cc @@ -124,6 +124,12 @@ void WebGpuContext::Initialize(const WebGpuContextConfig& config) { device_queue_ = device_.GetQueue(); // cache device limits ORT_ENFORCE(Device().GetLimits(&device_limits_)); + // Align maxStorageBufferBindingSize down to minStorageBufferOffsetAlignment so that + // buffer segment offsets are always properly aligned for WebGPU bind group creation. + if (device_limits_.minStorageBufferOffsetAlignment > 0) { + device_limits_.maxStorageBufferBindingSize -= + (device_limits_.maxStorageBufferBindingSize % device_limits_.minStorageBufferOffsetAlignment); + } // cache device features wgpu::SupportedFeatures supported_features; Device().GetFeatures(&supported_features); From 722743c0e2f8c8cb86543a2435189f2df9022a7a Mon Sep 17 00:00:00 2001 From: kunal-vaishnavi <115581922+kunal-vaishnavi@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:06:25 -0700 Subject: [PATCH 08/14] Add MHA fusion for Nemotron speech conformer encoder (#27764) ### Description This PR updates the pattern matchings to perform multi-head attention fusion for the conformer encoder inside [Nemotron speech](https://huggingface.co/nvidia/nemotron-speech-streaming-en-0.6b). image ### Motivation and Context These changes allow the `MultiHeadAttention` op to appear in the encoder ONNX model. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../fusion_conformer_attention.py | 111 ++++- .../transformers/conformer_model_generator.py | 421 ++++++++++++++++++ .../python/transformers/test_conformer.py | 88 +++- 3 files changed, 601 insertions(+), 19 deletions(-) diff --git a/onnxruntime/python/tools/transformers/fusion_conformer_attention.py b/onnxruntime/python/tools/transformers/fusion_conformer_attention.py index 2b7fbffa842f7..af14dedd005b8 100644 --- a/onnxruntime/python/tools/transformers/fusion_conformer_attention.py +++ b/onnxruntime/python/tools/transformers/fusion_conformer_attention.py @@ -32,8 +32,14 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node): [1, None, 0, 0, 0], ) if qkv_nodes is None: - logger.debug("fuse_conformer_attention: failed to match qkv path") - return + qkv_nodes = self.model.match_parent_path( + normalize_node, + ["MatMul", "Reshape", "Transpose", "MatMul"], + [1, 0, 0, 0], + ) + if qkv_nodes is None: + logger.debug("fuse_conformer_attention: failed to match qkv path") + return reshape_qkv, transpose_qkv, matmul_qkv = qkv_nodes[-3], qkv_nodes[-2], qkv_nodes[-1] @@ -50,15 +56,22 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node): [1, 0, 0, 0], ) if v_nodes is None: - logger.debug("fuse_conformer_attention: failed to match v path") - return + v_nodes = self.model.match_parent_path( + matmul_qkv, + ["Transpose", "Reshape", "MatMul"], + [1, 0, 0], + ) + if v_nodes is None: + logger.debug("fuse_conformer_attention: failed to match v path") + return else: concat_v = v_nodes[0] concat_parent = self.model.get_parent(concat_v, 0, None) present_v = concat_v.output[0] past_v = concat_parent.output[0] - add_v, matmul_v = v_nodes[-2], v_nodes[-1] + add_v = v_nodes[-2] if len(v_nodes) >= 2 and v_nodes[-2].op_type == "Add" else None + matmul_v = v_nodes[-1] attn_mask = "" qk_nodes = self.model.match_parent_path( @@ -66,6 +79,7 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node): ["Softmax", "Add", "MatMul"], [0, 0, 0], ) + where_qk = None if qk_nodes is None: qk_nodes = self.model.match_parent_path( matmul_qkv, @@ -73,10 +87,19 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node): [0, 2, 0, 2, 0], ) if qk_nodes is None: - logger.debug("fuse_conformer_attention: failed to match qk path") - return + qk_nodes = self.model.match_parent_path( + matmul_qkv, + ["Where", "Softmax", "Where", "Div", "Add", "MatMul"], + [0, 2, 0, 2, 0, 0], + ) + if qk_nodes is None: + logger.debug("fuse_conformer_attention: failed to match qk path") + return + where_qk = qk_nodes[2] + else: + where_qk = qk_nodes[2] - where_qk = qk_nodes[2] + if where_qk is not None: mask_nodes = self.model.match_parent_path( where_qk, ["Equal", "Unsqueeze", "Cast"], @@ -99,20 +122,46 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node): [0, 0, 0, 0, 0], ) if q_nodes is None: - logger.debug("fuse_conformer_attention: failed to match q path") - return + q_nodes = self.model.match_parent_path( + matmul_qk, + ["Transpose", "Add", "Reshape", "MatMul"], + [0, 0, 0, 1], + ) + if q_nodes is None: + q_nodes = self.model.match_parent_path( + matmul_qk, + ["Transpose", "Add", "Reshape", "MatMul"], + [0, 0, 0, 0], + ) + if q_nodes is None: + logger.debug("fuse_conformer_attention: failed to match q path") + return - reshape_q, add_q, matmul_q = q_nodes[-3], q_nodes[-2], q_nodes[-1] + reshape_q = next((node for node in q_nodes if node.op_type == "Reshape"), None) + add_q = next((node for node in q_nodes if node.op_type == "Add"), None) + matmul_q = next((node for node in reversed(q_nodes) if node.op_type == "MatMul"), None) + if reshape_q is None or add_q is None or matmul_q is None: + logger.debug("fuse_conformer_attention: failed to identify q reshape/add/matmul nodes") + return extra_q_nodes = self.model.match_parent_path( add_qk, ["Reshape", "Transpose", "MatMul", "Transpose", "Reshape", "Div"], [1, 0, 0, 0, 0, 0], ) - if extra_q_nodes is not None and q_nodes[0] != extra_q_nodes[-1]: + if extra_q_nodes is not None and q_nodes[0].op_type in ["Div", "Mul"] and q_nodes[0] != extra_q_nodes[-1]: logger.debug("fuse_conformer_attention: failed to match extra q path") return + if extra_q_nodes is None: + nemotron_extra_q_nodes = self.model.match_parent_path( + add_qk, + ["Slice", "Reshape", "Slice", "Reshape", "Pad", "MatMul", "Transpose", "Add"], + [1, 0, 0, 0, 0, 0, 0, 0], + ) + if nemotron_extra_q_nodes is not None: + extra_q_nodes = nemotron_extra_q_nodes + past_k, present_k = "", "" k_nodes = self.model.match_parent_path( matmul_qk, @@ -132,24 +181,50 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node): [1, 0, 0, 0], ) if k_nodes is None: - logger.debug("fuse_conformer_attention: failed to match k path") - return + k_nodes = self.model.match_parent_path( + matmul_qk, + ["Transpose", "Reshape", "MatMul"], + [1, 0, 0], + ) + if k_nodes is None: + logger.debug("fuse_conformer_attention: failed to match k path") + return else: concat_k = k_nodes[1] concat_parent = self.model.get_parent(concat_k, 0, None) past_k = concat_parent.output[0] present_k = concat_k.output[0] - add_k, matmul_k = k_nodes[-2], k_nodes[-1] + add_k = k_nodes[-2] if len(k_nodes) >= 2 and k_nodes[-2].op_type == "Add" else None + matmul_k = k_nodes[-1] num_heads, hidden_size = self.get_num_heads_and_hidden_size(reshape_q) if num_heads <= 0 or hidden_size <= 0 or (hidden_size % num_heads) != 0: logger.debug("fuse_conformer_attention: failed to detect num_heads or hidden_size") return + # Validate attention_bias: the Attention and MultiHeadAttention kernels require a 4-D + # tensor with shape [batch_size or 1, num_heads or 1, sequence_length, total_sequence_length]. + # Scalar or 1-D initializers (e.g. a plain QK scaling constant) must not be forwarded as + # attention_bias. Non-initializer values (computed positional-bias outputs) are kept as-is. + attention_bias = add_qk.input[1] + bias_init = self.model.get_initializer(attention_bias) + if bias_init is not None and len(bias_init.dims) != 4: + logger.debug( + "fuse_conformer_attention: skipping attention_bias %s with dims %s (expected 4-D)", + attention_bias, + list(bias_init.dims), + ) + attention_bias = "" + new_node = None use_packed_attention_op = ( - matmul_q.input[0] == matmul_k.input[0] and matmul_k.input[0] == matmul_v.input[0] and extra_q_nodes is None + matmul_q.input[0] == matmul_k.input[0] + and matmul_k.input[0] == matmul_v.input[0] + and extra_q_nodes is None + and add_q is not None + and add_k is not None + and add_v is not None ) if use_packed_attention_op: # Self-attention, use Attention op @@ -165,7 +240,7 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node): hidden_size=hidden_size, first_input=matmul_q.input[0], output=reshape_qkv.output[0], - add_qk_str=add_qk.input[1], + add_qk_str=attention_bias, past_k=past_k, past_v=past_v, present_k=present_k, @@ -183,7 +258,7 @@ def fuse(self, normalize_node, input_name_to_nodes, output_name_to_node): hidden_size=hidden_size, output=reshape_qkv.output[0], key_padding_mask=attn_mask, - add_qk=add_qk.input[1], + add_qk=attention_bias, past_k=past_k, past_v=past_v, present_k=present_k, diff --git a/onnxruntime/test/python/transformers/conformer_model_generator.py b/onnxruntime/test/python/transformers/conformer_model_generator.py index 4e76478bfb649..d067c484b2edd 100644 --- a/onnxruntime/test/python/transformers/conformer_model_generator.py +++ b/onnxruntime/test/python/transformers/conformer_model_generator.py @@ -10,6 +10,11 @@ from bert_model_generator import float_tensor from onnx import TensorProto, helper, numpy_helper +# Minimum non-zero value used for the QK attention bias initializer in test models. +# A zero bias would be eliminated by ORT's basic constant folding (it removes Add(x, 0) +# as a no-op), breaking the fusion patterns that expect an Add node before Softmax. +_NON_ZERO_QK_BIAS = 1e-4 + # Adapted from bert_model_generator.py def get_tensor_and_weight(name: str, shape: list[int], random=False, zeros=False): @@ -530,6 +535,422 @@ def create_conformer_attention( return helper.make_model(graph, opset_imports=(opsetid,)) +def create_conformer_attention_simple_bias( + hidden_size=64, + num_heads=4, + epsilon=0.000009999999747378752, +): + """ + Standard conformer attention where the QK add_bias is a plain initializer (no positional + embedding computation). The extra_q_nodes match_parent_path will return None for both the + conformer-transducer and Nemotron patterns, so fusion proceeds with extra_q_nodes=None. + + This is a regression test to verify that the fix restoring optional extra_q_nodes semantics + works correctly: graphs that never had an auxiliary Q branch must still fuse. + + Q path: MatMul -> Add(bias, matmul_out) -> Reshape -> Transpose([0,2,1,3]) -> Div -> matmul_qk + K path: MatMul -> Add(matmul_out, bias) -> Reshape -> Transpose([0,2,3,1]) -> matmul_qk + V path: MatMul -> Add(matmul_out, bias) -> Reshape -> Transpose([0,2,1,3]) -> matmul_qkv + QK: MatMul -> Add(qk_out, qk_bias_init) -> Softmax -> MatMul + Output: Transpose -> Reshape -> MatMul -> Add(bias, matmul) -> SkipLayerNorm + """ + assert hidden_size % num_heads == 0 + head_size = hidden_size // num_heads + + inputs = [ + helper.make_tensor_value_info("input_0", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + helper.make_tensor_value_info("input_1", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + ] + outputs = [ + helper.make_tensor_value_info("output_0", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + helper.make_tensor_value_info("output_1", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + ] + nodes = [] + + # SkipLayerNorm + nodes.append( + helper.make_node( + "SkipLayerNormalization", + ["input_0", "input_1", "ln_weight", "ln_bias"], + ["ln_out", "", "", "ln_skip_out"], + "skiplayernorm", + domain="com.microsoft", + epsilon=epsilon, + ) + ) + + # Q path: MatMul -> Add(bias[0], matmul[1]) -> Reshape -> Transpose -> Div + nodes.extend( + [ + helper.make_node("MatMul", ["ln_out", "q_weight"], ["q_matmul_out"], "q_matmul"), + helper.make_node("Add", ["q_bias", "q_matmul_out"], ["q_add_out"], "q_add"), + helper.make_node("Reshape", ["q_add_out", "qkv_reshape_shape"], ["q_4d_bsnh"], "q_reshape"), + helper.make_node("Transpose", ["q_4d_bsnh"], ["q_4d_bnsh"], "q_transpose", perm=[0, 2, 1, 3]), + helper.make_node("Div", ["q_4d_bnsh", "q_scale"], ["q_scaled"], "q_div"), + ] + ) + + # K path: MatMul -> Add(matmul[0], bias[1]) -> Reshape -> Transpose (single, for K^T) + nodes.extend( + [ + helper.make_node("MatMul", ["ln_out", "k_weight"], ["k_matmul_out"], "k_matmul"), + helper.make_node("Add", ["k_matmul_out", "k_bias"], ["k_add_out"], "k_add"), + helper.make_node("Reshape", ["k_add_out", "qkv_reshape_shape"], ["k_4d_bsnh"], "k_reshape"), + # perm=[0,2,3,1]: [B,S,H,D] -> [B,H,D,S] giving K^T for attention dot product + helper.make_node("Transpose", ["k_4d_bsnh"], ["k_transposed"], "k_transpose", perm=[0, 2, 3, 1]), + ] + ) + + # V path: MatMul -> Add(matmul[0], bias[1]) -> Reshape -> Transpose (BNSH) + nodes.extend( + [ + helper.make_node("MatMul", ["ln_out", "v_weight"], ["v_matmul_out"], "v_matmul"), + helper.make_node("Add", ["v_matmul_out", "v_bias"], ["v_add_out"], "v_add"), + helper.make_node("Reshape", ["v_add_out", "qkv_reshape_shape"], ["v_4d_bsnh"], "v_reshape"), + helper.make_node("Transpose", ["v_4d_bsnh"], ["v_4d_bnsh"], "v_transpose", perm=[0, 2, 1, 3]), + ] + ) + + # QK: MatMul -> Add(qk_out, simple_bias_init) -> Softmax -> MatMul + # qk_bias is a plain initializer, so extra_q_nodes will be None. + nodes.extend( + [ + helper.make_node("MatMul", ["q_scaled", "k_transposed"], ["qk_out"], "matmul_qk"), + helper.make_node("Add", ["qk_out", "qk_bias"], ["qk_add_out"], "add_qk"), + helper.make_node("Softmax", ["qk_add_out"], ["softmax_out"], "softmax_qk", axis=3), + helper.make_node("MatMul", ["softmax_out", "v_4d_bnsh"], ["qkv_bnsh"], "matmul_qkv"), + ] + ) + + # Output: Transpose -> Reshape -> MatMul -> Add -> SkipLayerNorm + nodes.extend( + [ + helper.make_node("Transpose", ["qkv_bnsh"], ["qkv_bsnh"], "qkv_transpose", perm=[0, 2, 1, 3]), + helper.make_node("Reshape", ["qkv_bsnh", "out_reshape_shape"], ["attn_out"], "out_reshape"), + helper.make_node("MatMul", ["attn_out", "out_weight"], ["out_matmul"], "out_matmul"), + helper.make_node("Add", ["out_bias", "out_matmul"], ["out_add"], "out_add"), + helper.make_node( + "SkipLayerNormalization", + ["ln_skip_out", "out_add", "ln_weight", "ln_bias"], + ["output_0", "", "", "output_1"], + "next_skiplayernorm", + domain="com.microsoft", + epsilon=epsilon, + ), + ] + ) + + q_weight, _ = get_tensor_and_weight("q_weight", [hidden_size, hidden_size]) + k_weight, _ = get_tensor_and_weight("k_weight", [hidden_size, hidden_size]) + v_weight, _ = get_tensor_and_weight("v_weight", [hidden_size, hidden_size]) + + initializers = [ + float_tensor("ln_weight", [hidden_size]), + float_tensor("ln_bias", [hidden_size]), + float_tensor("out_weight", [hidden_size, hidden_size]), + float_tensor("out_bias", [hidden_size]), + q_weight, + k_weight, + v_weight, + numpy_helper.from_array(np.array([1.0] * hidden_size, dtype="float32"), name="q_bias"), + numpy_helper.from_array(np.array([1.0] * hidden_size, dtype="float32"), name="k_bias"), + numpy_helper.from_array(np.array([1.0] * hidden_size, dtype="float32"), name="v_bias"), + # QK bias: a simple non-zero initializer so extra_q_nodes won't match any positional-embed pattern. + # Non-zero so ORT's constant folding (which removes Add(x, 0)) doesn't eliminate this node. + numpy_helper.from_array(np.array([_NON_ZERO_QK_BIAS], dtype="float32"), name="qk_bias"), + numpy_helper.from_array(np.array(1.0 / np.sqrt(head_size), dtype="float32"), name="q_scale"), + # Reshape shape [0, 0, num_heads, head_size] for Q/K/V + numpy_helper.from_array(np.array([0, 0, num_heads, head_size], dtype="int64"), name="qkv_reshape_shape"), + # Reshape shape [0, 0, hidden_size] for output + numpy_helper.from_array(np.array([0, 0, hidden_size], dtype="int64"), name="out_reshape_shape"), + ] + + graph = helper.make_graph( + nodes, "conformer_simple_bias_graph", inputs, outputs, initializers, doc_string="conformer" + ) + opsetid = helper.make_opsetid("ai.onnx", min(onnx.defs.onnx_opset_version(), 16)) + return helper.make_model(graph, opset_imports=(opsetid,)) + + +def create_conformer_attention_no_add_kv( + hidden_size=64, + num_heads=4, + epsilon=0.000009999999747378752, +): + """ + Nemotron-like conformer attention model with no Add-bias nodes in the K and V paths, + and a Q path that begins with Transpose→Add→Reshape→MatMul (no leading Div/Mul). + The QKV output path also omits the trailing Add before the SkipLayerNorm. + + This exercises the following new fallback patterns: + - QKV output: ["MatMul", "Reshape", "Transpose", "MatMul"] with [1, 0, 0, 0] + - Q path: ["Transpose", "Add", "Reshape", "MatMul"] with [0, 0, 0, 0] + - K path: ["Transpose", "Reshape", "MatMul"] with [1, 0, 0] + - V path: ["Transpose", "Reshape", "MatMul"] with [1, 0, 0] + """ + assert hidden_size % num_heads == 0 + head_size = hidden_size // num_heads + + inputs = [ + helper.make_tensor_value_info("input_0", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + helper.make_tensor_value_info("input_1", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + ] + outputs = [ + helper.make_tensor_value_info("output_0", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + helper.make_tensor_value_info("output_1", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + ] + nodes = [] + + # SkipLayerNorm + nodes.append( + helper.make_node( + "SkipLayerNormalization", + ["input_0", "input_1", "ln_weight", "ln_bias"], + ["ln_out", "", "", "ln_skip_out"], + "skiplayernorm", + domain="com.microsoft", + epsilon=epsilon, + ) + ) + + # Q path: MatMul -> Reshape -> Add(reshape[0], bias[1]) -> Transpose -> matmul_qk + # Matches: ["Transpose", "Add", "Reshape", "MatMul"] with [0, 0, 0, 0] + nodes.extend( + [ + helper.make_node("MatMul", ["ln_out", "q_weight"], ["q_matmul_out"], "q_matmul"), + helper.make_node("Reshape", ["q_matmul_out", "qkv_reshape_shape"], ["q_4d_bsnh"], "q_reshape"), + helper.make_node("Add", ["q_4d_bsnh", "q_bias_4d"], ["q_4d_biased"], "q_add"), + helper.make_node("Transpose", ["q_4d_biased"], ["q_4d_bnsh"], "q_transpose", perm=[0, 2, 1, 3]), + ] + ) + + # K path: MatMul -> Reshape -> Transpose (no Add) + # Matches: ["Transpose", "Reshape", "MatMul"] with [1, 0, 0] + nodes.extend( + [ + helper.make_node("MatMul", ["ln_out", "k_weight"], ["k_matmul_out"], "k_matmul"), + helper.make_node("Reshape", ["k_matmul_out", "qkv_reshape_shape"], ["k_4d_bsnh"], "k_reshape"), + # perm=[0,2,3,1]: [B,S,H,D] -> [B,H,D,S] for K^T + helper.make_node("Transpose", ["k_4d_bsnh"], ["k_transposed"], "k_transpose", perm=[0, 2, 3, 1]), + ] + ) + + # V path: MatMul -> Reshape -> Transpose (no Add) + # Matches: ["Transpose", "Reshape", "MatMul"] with [1, 0, 0] + nodes.extend( + [ + helper.make_node("MatMul", ["ln_out", "v_weight"], ["v_matmul_out"], "v_matmul"), + helper.make_node("Reshape", ["v_matmul_out", "qkv_reshape_shape"], ["v_4d_bsnh"], "v_reshape"), + helper.make_node("Transpose", ["v_4d_bsnh"], ["v_4d_bnsh"], "v_transpose", perm=[0, 2, 1, 3]), + ] + ) + + # QK: MatMul -> Add(qk_out, bias) -> Softmax -> MatMul + nodes.extend( + [ + helper.make_node("MatMul", ["q_4d_bnsh", "k_transposed"], ["qk_out"], "matmul_qk"), + helper.make_node("Add", ["qk_out", "qk_bias"], ["qk_add_out"], "add_qk"), + helper.make_node("Softmax", ["qk_add_out"], ["softmax_out"], "softmax_qk", axis=3), + helper.make_node("MatMul", ["softmax_out", "v_4d_bnsh"], ["qkv_bnsh"], "matmul_qkv"), + ] + ) + + # Output: Transpose -> Reshape -> MatMul (no trailing Add before SkipLayerNorm) + # Matches QKV path: ["MatMul", "Reshape", "Transpose", "MatMul"] with [1, 0, 0, 0] + nodes.extend( + [ + helper.make_node("Transpose", ["qkv_bnsh"], ["qkv_bsnh"], "qkv_transpose", perm=[0, 2, 1, 3]), + helper.make_node("Reshape", ["qkv_bsnh", "out_reshape_shape"], ["attn_out"], "out_reshape"), + helper.make_node("MatMul", ["attn_out", "out_weight"], ["out_matmul"], "out_matmul"), + helper.make_node( + "SkipLayerNormalization", + ["ln_skip_out", "out_matmul", "ln_weight", "ln_bias"], + ["output_0", "", "", "output_1"], + "next_skiplayernorm", + domain="com.microsoft", + epsilon=epsilon, + ), + ] + ) + + q_weight, _ = get_tensor_and_weight("q_weight", [hidden_size, hidden_size]) + k_weight, _ = get_tensor_and_weight("k_weight", [hidden_size, hidden_size]) + v_weight, _ = get_tensor_and_weight("v_weight", [hidden_size, hidden_size]) + + initializers = [ + float_tensor("ln_weight", [hidden_size]), + float_tensor("ln_bias", [hidden_size]), + float_tensor("out_weight", [hidden_size, hidden_size]), + q_weight, + k_weight, + v_weight, + # Q bias in 4D shape [1, 1, num_heads, head_size] for broadcasting after Reshape + numpy_helper.from_array(np.ones([1, 1, num_heads, head_size], dtype="float32"), name="q_bias_4d"), + # Non-zero qk_bias so ORT's constant folding (which removes Add(x, 0)) doesn't eliminate this node. + numpy_helper.from_array(np.array([_NON_ZERO_QK_BIAS], dtype="float32"), name="qk_bias"), + numpy_helper.from_array(np.array([0, 0, num_heads, head_size], dtype="int64"), name="qkv_reshape_shape"), + numpy_helper.from_array(np.array([0, 0, hidden_size], dtype="int64"), name="out_reshape_shape"), + ] + + graph = helper.make_graph(nodes, "conformer_no_add_kv_graph", inputs, outputs, initializers, doc_string="conformer") + opsetid = helper.make_opsetid("ai.onnx", min(onnx.defs.onnx_opset_version(), 16)) + return helper.make_model(graph, opset_imports=(opsetid,)) + + +def create_conformer_attention_qk_div_masking( + hidden_size=64, + num_heads=4, + epsilon=0.000009999999747378752, +): + """ + Conformer attention with QK masking using Where→Softmax→Where→Div→Add→MatMul. + + This exercises the new QK path: + ["Where", "Softmax", "Where", "Div", "Add", "MatMul"] with [0, 2, 0, 2, 0, 0] + + The graph structure for the masked QK computation is: + MatMul(Q,K^T) → Add(qk_bias) → Div(scale) → inner_Where → Softmax → outer_Where → MatMul(V) + """ + assert hidden_size % num_heads == 0 + head_size = hidden_size // num_heads + + inputs = [ + helper.make_tensor_value_info("input_0", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + helper.make_tensor_value_info("input_1", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + ] + outputs = [ + helper.make_tensor_value_info("output_0", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + helper.make_tensor_value_info("output_1", TensorProto.FLOAT, ["batch_size", "seq_len", hidden_size]), + ] + nodes = [] + + # SkipLayerNorm + nodes.append( + helper.make_node( + "SkipLayerNormalization", + ["input_0", "input_1", "ln_weight", "ln_bias"], + ["ln_out", "", "", "ln_skip_out"], + "skiplayernorm", + domain="com.microsoft", + epsilon=epsilon, + ) + ) + + # Q path: MatMul -> Add(bias, matmul_out) -> Reshape -> Transpose -> Div + # Matches: ["Div", "Transpose", "Reshape", "Add", "MatMul"] with [0, 0, 0, 0, 1] + nodes.extend( + [ + helper.make_node("MatMul", ["ln_out", "q_weight"], ["q_matmul_out"], "q_matmul"), + helper.make_node("Add", ["q_bias", "q_matmul_out"], ["q_add_out"], "q_add"), + helper.make_node("Reshape", ["q_add_out", "qkv_reshape_shape"], ["q_4d_bsnh"], "q_reshape"), + helper.make_node("Transpose", ["q_4d_bsnh"], ["q_4d_bnsh"], "q_transpose", perm=[0, 2, 1, 3]), + helper.make_node("Div", ["q_4d_bnsh", "q_scale"], ["q_scaled"], "q_div"), + ] + ) + + # K path: MatMul -> Add(matmul_out, bias) -> Reshape -> Transpose + # Matches: ["Transpose", "Reshape", "Add", "MatMul"] with [1, 0, 0, 0] + nodes.extend( + [ + helper.make_node("MatMul", ["ln_out", "k_weight"], ["k_matmul_out"], "k_matmul"), + helper.make_node("Add", ["k_matmul_out", "k_bias"], ["k_add_out"], "k_add"), + helper.make_node("Reshape", ["k_add_out", "qkv_reshape_shape"], ["k_4d_bsnh"], "k_reshape"), + helper.make_node("Transpose", ["k_4d_bsnh"], ["k_transposed"], "k_transpose", perm=[0, 2, 3, 1]), + ] + ) + + # V path: MatMul -> Add(matmul_out, bias) -> Reshape -> Transpose + # Matches: ["Transpose", "Reshape", "Add", "MatMul"] with [1, 0, 0, 0] + nodes.extend( + [ + helper.make_node("MatMul", ["ln_out", "v_weight"], ["v_matmul_out"], "v_matmul"), + helper.make_node("Add", ["v_matmul_out", "v_bias"], ["v_add_out"], "v_add"), + helper.make_node("Reshape", ["v_add_out", "qkv_reshape_shape"], ["v_4d_bsnh"], "v_reshape"), + helper.make_node("Transpose", ["v_4d_bsnh"], ["v_4d_bnsh"], "v_transpose", perm=[0, 2, 1, 3]), + ] + ) + + # QK computation with Div masking: + # MatMul(QK) -> Add(qk_bias) -> Div(scale) -> inner_Where -> Softmax -> outer_Where -> MatMul(V) + # + # Matches: ["Where", "Softmax", "Where", "Div", "Add", "MatMul"] with [0, 2, 0, 2, 0, 0] + # where_qk = inner_Where + nodes.extend( + [ + helper.make_node("MatMul", ["q_scaled", "k_transposed"], ["qk_out"], "matmul_qk"), + helper.make_node("Add", ["qk_out", "qk_bias"], ["qk_add_out"], "add_qk"), + helper.make_node("Div", ["qk_add_out", "qk_div_scale"], ["qk_div_out"], "div_qk"), + # inner_Where: condition ? qk_div_out : mask_value → input[0]=cond, [1]=mask, [2]=qk_div_out + helper.make_node( + "Where", + ["mask_condition", "mask_value", "qk_div_out"], + ["inner_where_out"], + "inner_where", + ), + helper.make_node("Softmax", ["inner_where_out"], ["softmax_out"], "softmax_qk", axis=3), + # outer_Where: condition ? zeros : softmax_out → input[0]=cond, [1]=zeros, [2]=softmax_out + helper.make_node( + "Where", + ["mask_condition", "zeros_val", "softmax_out"], + ["outer_where_out"], + "outer_where", + ), + helper.make_node("MatMul", ["outer_where_out", "v_4d_bnsh"], ["qkv_bnsh"], "matmul_qkv"), + ] + ) + + # Output: Transpose -> Reshape -> MatMul -> Add -> SkipLayerNorm + nodes.extend( + [ + helper.make_node("Transpose", ["qkv_bnsh"], ["qkv_bsnh"], "qkv_transpose", perm=[0, 2, 1, 3]), + helper.make_node("Reshape", ["qkv_bsnh", "out_reshape_shape"], ["attn_out"], "out_reshape"), + helper.make_node("MatMul", ["attn_out", "out_weight"], ["out_matmul"], "out_matmul"), + helper.make_node("Add", ["out_bias", "out_matmul"], ["out_add"], "out_add"), + helper.make_node( + "SkipLayerNormalization", + ["ln_skip_out", "out_add", "ln_weight", "ln_bias"], + ["output_0", "", "", "output_1"], + "next_skiplayernorm", + domain="com.microsoft", + epsilon=epsilon, + ), + ] + ) + + q_weight, _ = get_tensor_and_weight("q_weight", [hidden_size, hidden_size]) + k_weight, _ = get_tensor_and_weight("k_weight", [hidden_size, hidden_size]) + v_weight, _ = get_tensor_and_weight("v_weight", [hidden_size, hidden_size]) + + initializers = [ + float_tensor("ln_weight", [hidden_size]), + float_tensor("ln_bias", [hidden_size]), + float_tensor("out_weight", [hidden_size, hidden_size]), + float_tensor("out_bias", [hidden_size]), + q_weight, + k_weight, + v_weight, + numpy_helper.from_array(np.array([1.0] * hidden_size, dtype="float32"), name="q_bias"), + numpy_helper.from_array(np.array([1.0] * hidden_size, dtype="float32"), name="k_bias"), + numpy_helper.from_array(np.array([1.0] * hidden_size, dtype="float32"), name="v_bias"), + # Non-zero qk_bias so ORT's constant folding (which removes Add(x, 0)) doesn't eliminate this node. + numpy_helper.from_array(np.array([_NON_ZERO_QK_BIAS], dtype="float32"), name="qk_bias"), + numpy_helper.from_array(np.array(1.0 / np.sqrt(head_size), dtype="float32"), name="q_scale"), + numpy_helper.from_array(np.array(float(head_size), dtype="float32"), name="qk_div_scale"), + # Boolean mask condition (all True = no masking, for test purposes) + helper.make_tensor("mask_condition", TensorProto.BOOL, [1, 1, 1, 1], [True]), + numpy_helper.from_array(np.array([-1e9], dtype="float32"), name="mask_value"), + numpy_helper.from_array(np.array([0.0], dtype="float32"), name="zeros_val"), + numpy_helper.from_array(np.array([0, 0, num_heads, head_size], dtype="int64"), name="qkv_reshape_shape"), + numpy_helper.from_array(np.array([0, 0, hidden_size], dtype="int64"), name="out_reshape_shape"), + ] + + graph = helper.make_graph( + nodes, "conformer_qk_div_masking_graph", inputs, outputs, initializers, doc_string="conformer" + ) + opsetid = helper.make_opsetid("ai.onnx", min(onnx.defs.onnx_opset_version(), 16)) + return helper.make_model(graph, opset_imports=(opsetid,)) + + if __name__ == "__main__": np.random.seed(2) num_heads = 8 diff --git a/onnxruntime/test/python/transformers/test_conformer.py b/onnxruntime/test/python/transformers/test_conformer.py index 471ba9756bcf8..e3e52cc456d42 100644 --- a/onnxruntime/test/python/transformers/test_conformer.py +++ b/onnxruntime/test/python/transformers/test_conformer.py @@ -5,10 +5,16 @@ # -------------------------------------------------------------------------- import os +import tempfile import unittest import onnx -from conformer_model_generator import create_conformer_attention +from conformer_model_generator import ( + create_conformer_attention, + create_conformer_attention_no_add_kv, + create_conformer_attention_qk_div_masking, + create_conformer_attention_simple_bias, +) from parity_utilities import find_transformers_source if find_transformers_source(): @@ -46,6 +52,32 @@ def verify_fusion(self, optimized_model, expected_model_filename): ) ) + def count_fused_attention_nodes(self, optimized_model): + """Return the number of Attention and MultiHeadAttention nodes in the optimized graph.""" + return sum( + 1 + for node in optimized_model.model.graph.node + if node.op_type in ("Attention", "MultiHeadAttention") and node.domain == "com.microsoft" + ) + + def _run_conformer_optimization(self, model, num_heads, hidden_size): + """Save the model to a temp file, run the conformer optimizer, and return the result.""" + with tempfile.NamedTemporaryFile(suffix=".onnx", delete=False) as f: + model_path = f.name + try: + onnx.save(model, model_path) + options = FusionOptions("conformer") + optimized = optimize_model( + model_path, + model_type="conformer", + num_heads=num_heads, + hidden_size=hidden_size, + optimization_options=options, + ) + finally: + os.remove(model_path) + return optimized + def test_ct_mha_fusion(self): num_heads = 8 hidden_size = 512 @@ -64,6 +96,60 @@ def test_ct_mha_fusion(self): os.remove(model_path) self.verify_fusion(optimized_model, "conformer_self_mha_fused.onnx") + def test_conformer_no_extra_q_nodes(self): + """Regression test: standard conformer without positional embedding extra-Q path. + + Before the fix, the extra_q_nodes block required one of the two branch patterns to match. + When neither matched (simple QK bias, no CT or Nemotron positional embed), fusion would + incorrectly return early. This test verifies that fusion still produces a fused attention + node when extra_q_nodes is None throughout. + """ + num_heads = 4 + hidden_size = 64 + model = create_conformer_attention_simple_bias(num_heads=num_heads, hidden_size=hidden_size) + optimized = self._run_conformer_optimization(model, num_heads, hidden_size) + fused_count = self.count_fused_attention_nodes(optimized) + self.assertEqual(fused_count, 1, f"Expected 1 fused attention node, got {fused_count}") + + def test_nemotron_conformer_no_bias_kv(self): + """Nemotron-like model with no Add-bias in K/V paths and no leading Add in QKV output. + + Exercises the new fallback matchers introduced for Nemotron graph shapes: + - QKV output path without leading Add: ["MatMul", "Reshape", "Transpose", "MatMul"] + - Q path (Transpose→Add→Reshape→MatMul, no leading Div/Mul): + ["Transpose", "Add", "Reshape", "MatMul"] with [0, 0, 0, 0] + - K/V paths without bias Add: + ["Transpose", "Reshape", "MatMul"] with [1, 0, 0] + Because add_k and add_v are None, the fused node must be MultiHeadAttention. + """ + num_heads = 4 + hidden_size = 64 + model = create_conformer_attention_no_add_kv(num_heads=num_heads, hidden_size=hidden_size) + optimized = self._run_conformer_optimization(model, num_heads, hidden_size) + fused_count = self.count_fused_attention_nodes(optimized) + self.assertEqual(fused_count, 1, f"Expected 1 fused attention node, got {fused_count}") + # add_k / add_v are None → use_packed_attention_op is False → MultiHeadAttention + mha_count = sum( + 1 + for node in optimized.model.graph.node + if node.op_type == "MultiHeadAttention" and node.domain == "com.microsoft" + ) + self.assertEqual(mha_count, 1, f"Expected MultiHeadAttention node, got {mha_count}") + + def test_conformer_qk_div_masking(self): + """Conformer with a Where→Softmax→Where→Div→Add→MatMul QK masking path. + + Exercises the new QK fallback: + ["Where", "Softmax", "Where", "Div", "Add", "MatMul"] with [0, 2, 0, 2, 0, 0] + which handles graphs where the QK logits are scaled by Div before the Where mask is applied. + """ + num_heads = 4 + hidden_size = 64 + model = create_conformer_attention_qk_div_masking(num_heads=num_heads, hidden_size=hidden_size) + optimized = self._run_conformer_optimization(model, num_heads, hidden_size) + fused_count = self.count_fused_attention_nodes(optimized) + self.assertEqual(fused_count, 1, f"Expected 1 fused attention node, got {fused_count}") + if __name__ == "__main__": unittest.main() From c884195afb02fcb1231ea2d70301cb743871c4cc Mon Sep 17 00:00:00 2001 From: adrastogi Date: Sun, 29 Mar 2026 17:29:51 -0700 Subject: [PATCH 09/14] Fix new-delete mismatch in DML EP's QuantizeLinear operator (#27823) ### Description DmlOperatorQuantization21 was missing the tensor reshaping logic that the older DmlOperatorElementwiseQLinear already had. Scalar scale tensors get padded to 4D, but a 5D input stays 5D. DML rejects the dimension mismatch with E_INVALIDARG, and the resulting exception unwind triggers a sized-delete bug in WRL's MakeAllocator which address sanitizer detects. The fix is to port the same logic from the DmlOperatorElementwiseQLinear into this path, so that the dimensions match. ### Motivation and Context This is required to ensure the DML EP correctly handles this scenario. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/AbiCustomRegistry.cpp | 2 +- .../src/BucketizedBufferAllocator.cpp | 2 +- .../src/DmlCommittedResourceAllocator.cpp | 2 +- .../src/DmlExternalBufferAllocator.h | 4 +- .../src/ExecutionProvider.cpp | 8 +- .../src/GraphDescBuilder.cpp | 2 +- .../src/MLOperatorAuthorImpl.cpp | 32 ++-- .../src/Operators/DmlDFT.h | 6 +- .../src/Operators/DmlGridSample.h | 6 +- .../src/Operators/DmlOperator.cpp | 67 +++++++++ .../src/Operators/DmlOperator.h | 9 ++ .../src/Operators/DmlOperatorElementWise.cpp | 61 +------- .../src/Operators/DmlOperatorNonZero.cpp | 6 +- .../src/Operators/DmlSTFT.h | 8 +- .../src/Operators/OperatorRegistration.cpp | 6 +- .../src/SafeMakeOrThrow.h | 37 +++++ .../dml/DmlExecutionProvider/src/precomp.h | 1 + .../MLOperatorAuthorHelper.h | 3 +- .../SchemaInferenceOverrider.h | 3 +- .../providers/dml/dml_provider_factory.cc | 6 +- .../cpu/tensor/quantize_linear_test.cc | 84 +++++++++++ .../providers/dml_safe_make_or_throw_test.cc | 139 ++++++++++++++++++ 22 files changed, 390 insertions(+), 104 deletions(-) create mode 100644 onnxruntime/core/providers/dml/DmlExecutionProvider/src/SafeMakeOrThrow.h create mode 100644 onnxruntime/test/providers/dml_safe_make_or_throw_test.cc diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/AbiCustomRegistry.cpp b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/AbiCustomRegistry.cpp index 353f698bb6f2c..076027dd3672f 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/AbiCustomRegistry.cpp +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/AbiCustomRegistry.cpp @@ -504,7 +504,7 @@ HRESULT STDMETHODCALLTYPE AbiCustomRegistry::RegisterOperatorKernel( InferAndVerifyOutputSizes(node, &defaultAttributesCapture, shapeInferrerCapture.Get(), constantCpuInputCapture, constantInputGetter, inputShapesOverrides, *outputShapes); // Create the kernel while allowing input shape and output shape queries according to options - ComPtr kernelInfoWrapper = wil::MakeOrThrow( + ComPtr kernelInfoWrapper = Dml::SafeMakeOrThrow( &protoHelper, executionHandle, true, diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/BucketizedBufferAllocator.cpp b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/BucketizedBufferAllocator.cpp index 18b4b4593f537..ed99ac0fc7fc2 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/BucketizedBufferAllocator.cpp +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/BucketizedBufferAllocator.cpp @@ -132,7 +132,7 @@ namespace Dml assert(resourceWrapper->GetD3D12Resource()->GetDesc().Width == bucketSize); assert(resourceWrapper != nullptr); - ComPtr allocInfo = wil::MakeOrThrow( + ComPtr allocInfo = Dml::SafeMakeOrThrow( this, ++m_currentAllocationId, resourceId, diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlCommittedResourceAllocator.cpp b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlCommittedResourceAllocator.cpp index 54393e9bf1539..2934fd0c11516 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlCommittedResourceAllocator.cpp +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlCommittedResourceAllocator.cpp @@ -22,7 +22,7 @@ namespace Dml )); ComPtr resourceWrapper; - wil::MakeOrThrow(std::move(resource)).As(&resourceWrapper); + Dml::SafeMakeOrThrow(std::move(resource)).As(&resourceWrapper); return resourceWrapper; } } diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlExternalBufferAllocator.h b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlExternalBufferAllocator.h index c99d686349e94..158c102d69ee7 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlExternalBufferAllocator.h +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlExternalBufferAllocator.h @@ -48,9 +48,9 @@ namespace Dml constexpr uint64_t pooledResourceId = 0; // Not a pooled resource Microsoft::WRL::ComPtr resourceWrapper; - wil::MakeOrThrow(std::move(resource)).As(&resourceWrapper); + Dml::SafeMakeOrThrow(std::move(resource)).As(&resourceWrapper); - Microsoft::WRL::ComPtr allocInfo = wil::MakeOrThrow( + Microsoft::WRL::ComPtr allocInfo = Dml::SafeMakeOrThrow( nullptr, 0, pooledResourceId, diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/ExecutionProvider.cpp b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/ExecutionProvider.cpp index 6d8d5453b9fc0..cd7dfd46485af 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/ExecutionProvider.cpp +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/ExecutionProvider.cpp @@ -55,7 +55,7 @@ namespace Dml _Out_ std::shared_ptr* registry, _Out_ std::shared_ptr* internalRegInfoMap) { - ComPtr abiRegistry = wil::MakeOrThrow(); + ComPtr abiRegistry = Dml::SafeMakeOrThrow(); Dml::RegisterDmlOperators(abiRegistry.Get()); assert(abiRegistry->GetRegistries().size() == 1); @@ -88,7 +88,7 @@ namespace Dml ComPtr device; GRAPHICS_THROW_IF_FAILED(dmlDevice->GetParentDevice(IID_GRAPHICS_PPV_ARGS(device.GetAddressOf()))); - m_impl = wil::MakeOrThrow(dmlDevice, device.Get(), executionContext, enableMetacommands, + m_impl = Dml::SafeMakeOrThrow(dmlDevice, device.Get(), executionContext, enableMetacommands, enableGraphCapture, enableSyncSpinning, disableMemoryArena); } @@ -1298,9 +1298,9 @@ namespace Dml uint64_t pooledResourceId = 0; // Not a pooled resource ComPtr resourceWrapper; - wil::MakeOrThrow(pResource).As(&resourceWrapper); + Dml::SafeMakeOrThrow(pResource).As(&resourceWrapper); - ComPtr allocInfo = wil::MakeOrThrow(nullptr, 0, pooledResourceId, resourceWrapper.Get(), (size_t)pResource->GetDesc().Width); + ComPtr allocInfo = Dml::SafeMakeOrThrow(nullptr, 0, pooledResourceId, resourceWrapper.Get(), (size_t)pResource->GetDesc().Width); return allocInfo.Detach(); } void FreeGPUAllocation(void* ptr) diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/GraphDescBuilder.cpp b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/GraphDescBuilder.cpp index 22de743f6e718..51c25d6d40c5b 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/GraphDescBuilder.cpp +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/GraphDescBuilder.cpp @@ -291,7 +291,7 @@ namespace Dml::GraphDescBuilder if (iter != isInitializerTransferable.end()) { // Using const_cast here is simpler than making surrounding code const correct. - tensorWrapper = wil::MakeOrThrow(const_cast(iter->second.first), modelPath); + tensorWrapper = Dml::SafeMakeOrThrow(const_cast(iter->second.first), modelPath); } return tensorWrapper; }; diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/MLOperatorAuthorImpl.cpp b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/MLOperatorAuthorImpl.cpp index fe52f27b35bb8..13ce9afa99b1e 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/MLOperatorAuthorImpl.cpp +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/MLOperatorAuthorImpl.cpp @@ -868,7 +868,7 @@ namespace Windows::AI::MachineLearning::Adapter const onnx::TensorProto* tensorProto = &attributeProto->t(); // An empty path is used as external weights are not currently supported in this case - Microsoft::WRL::ComPtr tensorWrapper = wil::MakeOrThrow(const_cast(tensorProto), std::filesystem::path()); + Microsoft::WRL::ComPtr tensorWrapper = Dml::SafeMakeOrThrow(const_cast(tensorProto), std::filesystem::path()); *tensor = tensorWrapper.Detach(); return S_OK; } @@ -1977,7 +1977,7 @@ namespace Windows::AI::MachineLearning::Adapter auto inputTensor = m_impl->Input(gsl::narrow_cast(inputIndex)); if (inputTensor != nullptr) { - ComPtr tensorWrapper = wil::MakeOrThrow( + ComPtr tensorWrapper = Dml::SafeMakeOrThrow( const_cast(inputTensor), IsAllocationInterface(inputTensor->Location()), m_winmlProvider.Get(), @@ -2019,7 +2019,7 @@ namespace Windows::AI::MachineLearning::Adapter auto elemTensor = const_cast(&inputTensorSeq->Get(sequenceIndex)); if (elemTensor != nullptr) { - ComPtr tensorWrapper = wil::MakeOrThrow( + ComPtr tensorWrapper = Dml::SafeMakeOrThrow( elemTensor, IsAllocationInterface(elemTensor->Location()), m_winmlProvider.Get(), @@ -2119,7 +2119,7 @@ namespace Windows::AI::MachineLearning::Adapter auto elemTensor = const_cast(&outputTensorSeq->Get(sequenceIndex)); if (elemTensor != nullptr) { - ComPtr tensorWrapper = wil::MakeOrThrow( + ComPtr tensorWrapper = Dml::SafeMakeOrThrow( elemTensor, IsAllocationInterface(elemTensor->Location()), m_winmlProvider.Get(), @@ -2212,7 +2212,7 @@ namespace Windows::AI::MachineLearning::Adapter auto outputTensor = m_impl->Output(outputIndex, shape); if (outputTensor) { - ComPtr tensorWrapper = wil::MakeOrThrow( + ComPtr tensorWrapper = Dml::SafeMakeOrThrow( const_cast(outputTensor), IsAllocationInterface(outputTensor->Location()), m_winmlProvider.Get(), @@ -2377,7 +2377,7 @@ namespace Windows::AI::MachineLearning::Adapter const onnxruntime::Tensor* tensor = nullptr; if (kerneInfo.TryGetConstantInput(index, &tensor)) { - tensorWrapper = wil::MakeOrThrow( + tensorWrapper = Dml::SafeMakeOrThrow( const_cast(tensor), IsAllocationInterface(tensor->Location()), winmlProviderCapture.Get(), @@ -2396,7 +2396,7 @@ namespace Windows::AI::MachineLearning::Adapter } // Create the kernel while allowing input shape and output shape queries according to options - ComPtr kernelInfoWrapper = wil::MakeOrThrow( + ComPtr kernelInfoWrapper = Dml::SafeMakeOrThrow( &kerneInfo, m_abiExecutionObject.Get(), nullptr, @@ -2443,7 +2443,7 @@ namespace Windows::AI::MachineLearning::Adapter const auto* tensor = context->Input(gsl::narrow_cast(index)); if (tensor != nullptr) { - tensorWrapper = wil::MakeOrThrow( + tensorWrapper = Dml::SafeMakeOrThrow( const_cast(tensor), IsAllocationInterface(tensor->Location()), winmlProviderCapture.Get(), @@ -2464,7 +2464,7 @@ namespace Windows::AI::MachineLearning::Adapter for (uint32_t sequenceIndex = 0; sequenceIndex < tensorSequence->Size(); ++sequenceIndex) { auto& tensor = tensorSequence->Get(sequenceIndex); - auto tensorWrapper = wil::MakeOrThrow( + auto tensorWrapper = Dml::SafeMakeOrThrow( const_cast(&tensor), IsAllocationInterface(tensor.Location()), winmlProviderCapture.Get(), @@ -2491,7 +2491,7 @@ namespace Windows::AI::MachineLearning::Adapter } // Create the kernel while allowing input shape and output shape queries according to options - ComPtr kernelInfoWrapper = wil::MakeOrThrow( + ComPtr kernelInfoWrapper = Dml::SafeMakeOrThrow( &Info(), m_abiExecutionObject.Get(), &inputShapes, @@ -2569,7 +2569,7 @@ namespace Windows::AI::MachineLearning::Adapter EdgeShapes localInferredOutputShapes; ComPtr localKernel = inferShapesAndCreateKernel(local_input_shapes, localInferredOutputShapes); - ComPtr kernelContextWrapper = wil::MakeOrThrow( + ComPtr kernelContextWrapper = Dml::SafeMakeOrThrow( context, Info().GetExecutionProvider(), m_internalOperator, @@ -2588,7 +2588,7 @@ namespace Windows::AI::MachineLearning::Adapter } } - ComPtr kernelContextWrapper = wil::MakeOrThrow( + ComPtr kernelContextWrapper = Dml::SafeMakeOrThrow( context, Info().GetExecutionProvider(), m_internalOperator, @@ -2811,7 +2811,7 @@ namespace Windows::AI::MachineLearning::Adapter onnxruntime::ProtoHelperNodeContext protoContext(node); onnxruntime::OpNodeProtoHelper info(&protoContext); - ComPtr inferenceContext = wil::MakeOrThrow(&info, inputShapes, outputShapes, defaultAttributes, requiredConstantCpuInputs, constantInputGetter); + ComPtr inferenceContext = Dml::SafeMakeOrThrow(&info, inputShapes, outputShapes, defaultAttributes, requiredConstantCpuInputs, constantInputGetter); outputShapes.Reset(info.GetOutputCount()); @@ -2865,13 +2865,13 @@ namespace Windows::AI::MachineLearning::Adapter [ctx](uint32_t index) { // An empty path is used as external weights are not currently supported in this case - Microsoft::WRL::ComPtr tensorWrapper = wil::MakeOrThrow( + Microsoft::WRL::ComPtr tensorWrapper = Dml::SafeMakeOrThrow( const_cast(ctx->getInputData(index)), std::filesystem::path()); return tensorWrapper; } ); - return wil::MakeOrThrow(info, ctx, requiredConstantCpuInputs, mlOperatorTensorGetter); + return Dml::SafeMakeOrThrow(info, ctx, requiredConstantCpuInputs, mlOperatorTensorGetter); } MLSchemaInferenceContext::MLSchemaInferenceContext( @@ -2952,7 +2952,7 @@ namespace Windows::AI::MachineLearning::Adapter const AttributeMap* defaultAttributes) { MLOperatorTensorGetter mLOperatorTensorGetter = MLOperatorTensorGetter(); - return wil::MakeOrThrow(info, defaultAttributes, mLOperatorTensorGetter); + return Dml::SafeMakeOrThrow(info, defaultAttributes, mLOperatorTensorGetter); } MLSupportQueryContext::MLSupportQueryContext( diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlDFT.h b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlDFT.h index 1de88a61a0d77..25210c146a6b6 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlDFT.h +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlDFT.h @@ -1097,7 +1097,7 @@ class GpuDFTOperatorFactory : public WRL::Base version = 20; } - auto dftOperator = wil::MakeOrThrow(context, version); + auto dftOperator = Dml::SafeMakeOrThrow(context, version); dftOperator.CopyTo(kernel); return S_OK; } @@ -1177,8 +1177,8 @@ class GpuDFTOperatorFactory : public WRL::Base kernelDescription.options = MLOperatorKernelOptions::None; kernelDescription.executionOptions = 0; - auto shareInferrer = wil::MakeOrThrow(); - auto factory = wil::MakeOrThrow(); + auto shareInferrer = Dml::SafeMakeOrThrow(); + auto factory = Dml::SafeMakeOrThrow(); std::array requiredConstantCpuInputs = { 1, 2 }; diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlGridSample.h b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlGridSample.h index 5ba936ddf3976..6d7a089103c9b 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlGridSample.h +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlGridSample.h @@ -747,7 +747,7 @@ class DmlGridSampleOperatorFactory : public WRL::Base { try { - auto dftOperator = wil::MakeOrThrow(context); + auto dftOperator = Dml::SafeMakeOrThrow(context); dftOperator.CopyTo(kernel); return S_OK; } @@ -832,8 +832,8 @@ class DmlGridSampleOperatorFactory : public WRL::Base kernelDescription.options = MLOperatorKernelOptions::None; kernelDescription.executionOptions = 0; - auto shareInferrer = wil::MakeOrThrow(); - auto factory = wil::MakeOrThrow(); + auto shareInferrer = Dml::SafeMakeOrThrow(); + auto factory = Dml::SafeMakeOrThrow(); ComPtr registryPrivate; ORT_THROW_IF_FAILED(registry->QueryInterface(IID_PPV_ARGS(®istryPrivate))); diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperator.cpp b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperator.cpp index 287f1e5b6dfe7..2ee85b01a9a2e 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperator.cpp +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperator.cpp @@ -907,4 +907,71 @@ namespace Dml bufferTensorDesc->TotalTensorSizeInBytes = (elementSize + 3) & ~3; } + void DmlOperator::BroadcastQuantizationParameters( + const MLOperatorKernelCreationContext& kernelInfo, + gsl::span outputShape + ) + { + const uint32_t outputShapeDimCount = gsl::narrow_cast(outputShape.size()); + + uint32_t axis = 0; + + // If an axis was explicitly passed (or the default value 1 is set from the schema), + // then other inputs are broadcasting to the shape of the input data tensor. + if (kernelInfo.HasAttribute(AttrName::Axis, MLOperatorAttributeType::Int)) + { + // Avoid validating the axis until later because the axis parameter is ignorable unless + // broadcasting is actually needed. ONNX opset 13 returns a default value of 1 for the + // "axis" attribute even when the attribute doesn't actually exist in the model, which + // would cause a validation failure here. + const int32_t signedAxis = gsl::narrow_cast(kernelInfo.GetAttribute(AttrName::Axis)); + axis = Dml::HandleNegativeAxis(signedAxis, outputShapeDimCount, /*validateAxis*/ false); + } + + // Explicitly reshape each of the inputs after the first input (scale tensor and optional zero point tensor). + for (uint32_t index = 1, inputCount = gsl::narrow_cast(m_inputTensorDescs.size()); index < inputCount; ++index) + { + if (!kernelInfo.IsInputValid(index)) + { + continue; + } + + auto edgeDesc = kernelInfo.GetInputEdgeDescription(index); + assert(edgeDesc.edgeType == MLOperatorEdgeType::Tensor); + + // Fix up the tensor shape by filling with trailing ones. So input[2,3] with axis=0 and scale[2] + // becomes scale[2,1], so that broadcasting works correctly. + std::vector inputTensorShape = kernelInfo.GetTensorShapeDescription().GetInputTensorShape(index); + + // If the input tensor is a 1D vector, then extra massaging is needed to project their + // 1D vectors back to the full shape for broadcasting along the given axis. + // The 1D vector should have a length equal to the output tensor's dimension on that axis. + if (inputTensorShape.size() == 1 && inputTensorShape != std::vector(outputShape.begin(), outputShape.end())) + { + ML_CHECK_VALID_ARGUMENT(axis < outputShapeDimCount); + uint32_t broadcastAxisLength = outputShape[axis]; + ML_CHECK_VALID_ARGUMENT( + (inputTensorShape[0] == broadcastAxisLength) || + // Treat as broadcast dimension to match CPU behavior. + (inputTensorShape[0] == 1) + ); + inputTensorShape.insert(inputTensorShape.begin(), axis, 1); + inputTensorShape.insert(inputTensorShape.end(), outputShapeDimCount - 1 - axis, 1); + } + // For any other shape (scalar/ND), leave it alone, and the TensorDesc constructor + // will apply broadcasting with standard elementwise alignment. + + m_inputTensorDescs[index] = TensorDesc( + edgeDesc.tensorDataType, + outputShape, + gsl::make_span(inputTensorShape), + TensorAxis::DoNotCoerce, + TensorAxis::W, + TensorAxis::RightAligned, + NchwDimensionCount, // minDimensionCount + 0 // guaranteedBaseOffsetAlignment + ); + } + } + } // namespace Dml diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperator.h b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperator.h index fa54d4b041b5f..002541e23c47c 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperator.h +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperator.h @@ -149,6 +149,15 @@ namespace Dml uint32_t minDimensionCount = NchwDimensionCount ) const; + // Reshapes scale and zero_point tensor descriptors (inputs after index 0) so that their + // dimension count matches the output shape, enabling correct broadcasting in DML. + // For 1D per-axis tensors, the shape is projected along the given axis (e.g. scale[6] + // with axis=0 on a 5D output becomes [6,1,1,1,1]). + void BroadcastQuantizationParameters( + const MLOperatorKernelCreationContext& kernelInfo, + gsl::span outputShape + ); + static void TryConvertTensorToBroadcastScalar( const MLOperatorKernelCreationContext& kernelInfo, const DML_TENSOR_DESC* tensor, diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperatorElementWise.cpp b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperatorElementWise.cpp index d4d7ee1311874..b64a5265f56e3 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperatorElementWise.cpp +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperatorElementWise.cpp @@ -542,64 +542,7 @@ class DmlOperatorElementwiseQLinear : public DmlOperator const DML_TENSOR_DATA_TYPE outputDataType = m_outputTensorDescs[0].GetDmlDataType(); bool hasZeroPointTensor = kernelInfo.IsInputValid(2); - uint32_t axis = 0; - - // If an axis was given explicitly passed (or the default value 1 is set from the schema), - // then other inputs are broadcasting to the shape of the input data tensor. - if (kernelInfo.HasAttribute(AttrName::Axis, MLOperatorAttributeType::Int)) - { - // Avoid validating the axis until later because the axis parameter is ignorable unless - // broadcasting is actually needed. ONNX opset 13 returns a default value of 1 for the - // "axis" attribute even when the attribute doesn't actually exist in the model, which - // would cause a validation failure here. - const int32_t signedAxis = gsl::narrow_cast(kernelInfo.GetAttribute(AttrName::Axis)); - axis = Dml::HandleNegativeAxis(signedAxis, outputShapeDimCount, /*validateAxis*/ false); - } - - // Explicitly reshape each of the inputs after the first input (scale tensor and optional zero point tensor). - for (uint32_t index = 1, inputCount = gsl::narrow_cast(m_inputTensorDescs.size()); index < inputCount; ++index) - { - if (!kernelInfo.IsInputValid(index)) - { - continue; - } - - auto edgeDesc = kernelInfo.GetInputEdgeDescription(index); - assert(edgeDesc.edgeType == MLOperatorEdgeType::Tensor); - - // Fix up the the tensor shape by filling with trailing ones. So input[2,3] with axis=0 and scale[2] - // becomes scale[2,1], so that broadcasting works correctly. - std::vector inputTensorShape = kernelInfo.GetTensorShapeDescription().GetInputTensorShape(index); - - // If the input tensor is a 1D vector, then extra massaging is needed to project their - // 1D vectors back to the full shape for broadcasting along the given axis. - // The 1D vector should have a length equal to the output tensor's dimension on that axis. - if (inputTensorShape.size() == 1 && inputTensorShape != outputShape) - { - ML_CHECK_VALID_ARGUMENT(axis < outputShapeDimCount); - uint32_t broadcastAxisLength = outputShape[axis]; - ML_CHECK_VALID_ARGUMENT( - (inputTensorShape[0] == broadcastAxisLength) || - // Treat as broadcast dimension to match CPU behavior. - (inputTensorShape[0] == 1) - ); - inputTensorShape.insert(inputTensorShape.begin(), axis, 1); - inputTensorShape.insert(inputTensorShape.end(), outputShapeDimCount - 1 - axis, 1); - } - // For any other shape (scalar/ND), leave it alone, and the TensorDesc constructor - // will apply broadcasting with standard elementwise alignment. - - m_inputTensorDescs[index] = TensorDesc( - edgeDesc.tensorDataType, - gsl::make_span(outputShape), - gsl::make_span(inputTensorShape), - TensorAxis::DoNotCoerce, - TensorAxis::W, - TensorAxis::RightAligned, - NchwDimensionCount, // minDimensionCount - 0 // guaranteedBaseOffsetAlignment - ); - } + BroadcastQuantizationParameters(kernelInfo, gsl::make_span(outputShape)); std::vector inputDescs = GetDmlInputDescs(); std::vector outputDescs = GetDmlOutputDescs(); @@ -630,6 +573,8 @@ class DmlOperatorQuantization21 : public DmlOperator const DML_TENSOR_DATA_TYPE outputDataType = m_outputTensorDescs[0].GetDmlDataType(); bool hasZeroPointTensor = kernelInfo.IsInputValid(2); + BroadcastQuantizationParameters(kernelInfo, gsl::make_span(outputShape)); + std::vector inputDescs = GetDmlInputDescs(); std::vector outputDescs = GetDmlOutputDescs(); diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperatorNonZero.cpp b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperatorNonZero.cpp index bc29256dd2e28..83e35ae89282d 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperatorNonZero.cpp +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlOperatorNonZero.cpp @@ -76,7 +76,7 @@ class DmlOperatorNonZero: public DmlOperator // Create the DML output tensor for the number of nonzero elements onnxruntime::Tensor outputCountDml(onnxruntime::DataTypeImpl::GetType(), m_outputCountShape, executionProvider->GetGpuAllocator()); - Microsoft::WRL::ComPtr outputCountDmlWrapper = wil::MakeOrThrow( + Microsoft::WRL::ComPtr outputCountDmlWrapper = Dml::SafeMakeOrThrow( &outputCountDml, true, executionProvider, @@ -84,7 +84,7 @@ class DmlOperatorNonZero: public DmlOperator // Create the DML output tensor for the coordinates (not cropped) onnxruntime::Tensor intermediateCoordinatesDml(onnxruntime::DataTypeImpl::GetType(), m_outputCoordinatesShape, executionProvider->GetGpuAllocator()); - Microsoft::WRL::ComPtr intermediateCoordinatesDmlWrapper = wil::MakeOrThrow( + Microsoft::WRL::ComPtr intermediateCoordinatesDmlWrapper = Dml::SafeMakeOrThrow( &intermediateCoordinatesDml, true, executionProvider, @@ -105,7 +105,7 @@ class DmlOperatorNonZero: public DmlOperator // Copy the number of nonzero elements back to the CPU onnxruntime::Tensor outputCountCpu(onnxruntime::DataTypeImpl::GetType(), {1}, executionProvider->GetCpuInputAllocator()); - Microsoft::WRL::ComPtr outputCountCpuWrapper = wil::MakeOrThrow( + Microsoft::WRL::ComPtr outputCountCpuWrapper = Dml::SafeMakeOrThrow( &outputCountCpu, false, executionProvider, diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlSTFT.h b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlSTFT.h index e2f38231f7295..091a82daefbdc 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlSTFT.h +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/DmlSTFT.h @@ -238,7 +238,7 @@ class DmlSTFTOperator : public WRL::Base constexpr uint32_t dftAxis = 1; constexpr bool dftIsInverse = false; - m_dftOperator.op = wil::MakeOrThrow( + m_dftOperator.op = Dml::SafeMakeOrThrow( m_d3dDevice.Get(), dftAxis, params.isOnesided, @@ -516,7 +516,7 @@ class DmlSTFTOperatorFactory : public WRL::Base { try { - auto dftOperator = wil::MakeOrThrow(context); + auto dftOperator = Dml::SafeMakeOrThrow(context); dftOperator.CopyTo(kernel); return S_OK; } @@ -574,8 +574,8 @@ class DmlSTFTOperatorFactory : public WRL::Base kernelDescription.options = MLOperatorKernelOptions::None; kernelDescription.executionOptions = 0; - auto shareInferrer = wil::MakeOrThrow(); - auto factory = wil::MakeOrThrow(); + auto shareInferrer = Dml::SafeMakeOrThrow(); + auto factory = Dml::SafeMakeOrThrow(); std::array requiredConstantCpuInputs = { /*frame_step*/1, /*frame_length*/3 }; diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/OperatorRegistration.cpp b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/OperatorRegistration.cpp index b0b37d01370bc..26f998c7521a2 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/OperatorRegistration.cpp +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/Operators/OperatorRegistration.cpp @@ -1314,18 +1314,18 @@ void RegisterDmlOperators(IMLOperatorRegistry* registry) totalTypeCount += typeConstraints[i].allowedTypeCount; } - ComPtr factory = wil::MakeOrThrow(information.creationFunction); + ComPtr factory = Dml::SafeMakeOrThrow(information.creationFunction); ComPtr shapeInferrer; if (information.shapeInferenceFunction) { - shapeInferrer = wil::MakeOrThrow(information.shapeInferenceFunction); + shapeInferrer = Dml::SafeMakeOrThrow(information.shapeInferenceFunction); } ComPtr supportQuery; if (information.supportQueryFunction) { - supportQuery = wil::MakeOrThrow(information.supportQueryFunction); + supportQuery = Dml::SafeMakeOrThrow(information.supportQueryFunction); } ORT_THROW_IF_FAILED(registryPrivate->RegisterOperatorKernel( diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/SafeMakeOrThrow.h b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/SafeMakeOrThrow.h new file mode 100644 index 0000000000000..c2740470cbc0a --- /dev/null +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/SafeMakeOrThrow.h @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include + +// Drop-in replacement for wil::MakeOrThrow that avoids an ASan false positive. +// WRL's MakeAllocator stores its buffer as char*, so if the constructor throws, +// ~MakeAllocator calls delete on a char* — passing sizeof(char)=1 to sized +// operator delete instead of sizeof(T). With the default MSVC allocator, this is +// benign (sized delete ignores the size), but ASan flags it as +// new-delete-type-mismatch. This helper uses placement new with correctly-sized +// cleanup to avoid the issue. +namespace Dml +{ + template + Microsoft::WRL::ComPtr SafeMakeOrThrow(TArgs&&... args) + { + void* buffer = ::operator new(sizeof(T)); + T* raw = nullptr; + try + { + raw = new (buffer) T(std::forward(args)...); + } + catch (...) + { + ::operator delete(buffer, sizeof(T)); + throw; + } + Microsoft::WRL::ComPtr result; + result.Attach(raw); + return result; + } +} // namespace Dml diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/precomp.h b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/precomp.h index e9df3fd20aff9..b9febb8171e0d 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/precomp.h +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/precomp.h @@ -25,6 +25,7 @@ #include #include +#include "SafeMakeOrThrow.h" #include diff --git a/onnxruntime/core/providers/dml/OperatorAuthorHelper/MLOperatorAuthorHelper.h b/onnxruntime/core/providers/dml/OperatorAuthorHelper/MLOperatorAuthorHelper.h index ac77616cb96f0..dec84d9945569 100644 --- a/onnxruntime/core/providers/dml/OperatorAuthorHelper/MLOperatorAuthorHelper.h +++ b/onnxruntime/core/providers/dml/OperatorAuthorHelper/MLOperatorAuthorHelper.h @@ -5,6 +5,7 @@ #include "core/providers/dml/DmlExecutionProvider/inc/MLOperatorAuthor.h" #include "MLOperatorAuthorPrivate.h" +#include "core/providers/dml/DmlExecutionProvider/src/SafeMakeOrThrow.h" #include "core/framework/int4.h" #include #include @@ -972,7 +973,7 @@ class MLOperatorKernel : public Microsoft::WRL::RuntimeClass< { ORT_TRY { - Microsoft::WRL::ComPtr kernel = wil::MakeOrThrow(MLOperatorKernelCreationContext(&info)); + Microsoft::WRL::ComPtr kernel = Dml::SafeMakeOrThrow(MLOperatorKernelCreationContext(&info)); *opKernel = kernel.Detach(); return S_OK; diff --git a/onnxruntime/core/providers/dml/OperatorAuthorHelper/SchemaInferenceOverrider.h b/onnxruntime/core/providers/dml/OperatorAuthorHelper/SchemaInferenceOverrider.h index fa04bcf6edf41..597780a9f448b 100644 --- a/onnxruntime/core/providers/dml/OperatorAuthorHelper/SchemaInferenceOverrider.h +++ b/onnxruntime/core/providers/dml/OperatorAuthorHelper/SchemaInferenceOverrider.h @@ -5,6 +5,7 @@ #include "OperatorHelper.h" #include "OperatorVersions.h" +#include "core/providers/dml/DmlExecutionProvider/src/SafeMakeOrThrow.h" namespace SchemaInferenceOverrider { @@ -21,7 +22,7 @@ namespace SchemaInferenceOverrider ) { Microsoft::WRL::ComPtr shapeInferrer = - wil::MakeOrThrow(OperatorHelper::ShapeInferenceFunction); + Dml::SafeMakeOrThrow(OperatorHelper::ShapeInferenceFunction); auto schema = const_cast(onnx::OpSchemaRegistry::Schema(name, version)); diff --git a/onnxruntime/core/providers/dml/dml_provider_factory.cc b/onnxruntime/core/providers/dml/dml_provider_factory.cc index c72ce205e5fbb..c0ddc44d0ca57 100644 --- a/onnxruntime/core/providers/dml/dml_provider_factory.cc +++ b/onnxruntime/core/providers/dml/dml_provider_factory.cc @@ -21,6 +21,8 @@ using Microsoft::WRL::ComPtr; #include #include +#include "core/providers/dml/DmlExecutionProvider/src/SafeMakeOrThrow.h" + #include "core/providers/dml/dml_provider_factory.h" #include "core/providers/dml/dml_provider_factory_creator.h" #include "core/session/abi_session_options_impl.h" @@ -89,11 +91,11 @@ std::unique_ptr DMLProviderFactory::CreateProvider() { // First, check if an I/O binding API that was used before this session or another session has already created a queue if (FAILED(d3d12_device->GetPrivateData(dml_execution_context_guid, &execution_context_ptr_size, execution_context.GetAddressOf()))) { - execution_context = wil::MakeOrThrow(d3d12_device.Get(), dml_device_.Get(), cmd_queue_.Get(), true, true); + execution_context = Dml::SafeMakeOrThrow(d3d12_device.Get(), dml_device_.Get(), cmd_queue_.Get(), true, true); ORT_THROW_IF_FAILED(d3d12_device->SetPrivateDataInterface(dml_execution_context_guid, execution_context.Get())); } } else { - execution_context = wil::MakeOrThrow(d3d12_device.Get(), dml_device_.Get(), cmd_queue_.Get(), cpu_sync_spinning_enabled_, false); + execution_context = Dml::SafeMakeOrThrow(d3d12_device.Get(), dml_device_.Get(), cmd_queue_.Get(), cpu_sync_spinning_enabled_, false); } auto provider = Dml::CreateExecutionProvider(dml_device_.Get(), execution_context.Get(), metacommands_enabled_, graph_capture_enabled_, cpu_sync_spinning_enabled_, disable_memory_arena_); diff --git a/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc b/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc index d1d1dd1c321af..82e58dcd71fdc 100644 --- a/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc @@ -516,6 +516,90 @@ TEST(QuantizeLinearOpTest, Int8) { test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kTensorrtExecutionProvider}); } +// Repro for new-delete-type-mismatch in DML EP during graph fusion. +// QuantizeLinear float32→int8 with 5D input triggers a type-size +// mismatch (192 bytes allocated, 1 byte deallocated) visible under ASan. +TEST(QuantizeLinearOpTest, Int8_5D_DML_TypeMismatch) { + auto dml_ep = DefaultDmlExecutionProvider(); + if (!dml_ep) { + GTEST_SKIP() << "Skipping because DML EP is not available."; + } + + OpTester test("QuantizeLinear", 13); + std::vector dims{6, 1, 1, 1, 1}; + test.AddInput("x", dims, {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}); + test.AddInput("y_scale", {}, {1.0f}); + test.AddInput("y_zero_point", {}, {0}); + test.AddOutput("y", dims, {1, 2, 3, 4, 5, 6}); + + std::vector> execution_providers; + execution_providers.emplace_back(std::move(dml_ep)); + test.ConfigEps(std::move(execution_providers)) + .RunWithConfig(); +} + +// Same as above but with per-axis quantization along axis 0 to exercise +// the DML graph fusion path with per-channel int8 quantization. +TEST(QuantizeLinearOpTest, Int8_5D_PerAxis_DML_TypeMismatch) { + auto dml_ep = DefaultDmlExecutionProvider(); + if (!dml_ep) { + GTEST_SKIP() << "Skipping because DML EP is not available."; + } + + OpTester test("QuantizeLinear", 13); + std::vector dims{6, 1, 1, 1, 1}; + test.AddAttribute("axis", 0); + test.AddInput("x", dims, {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}); + test.AddInput("y_scale", {6}, {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}); + test.AddInput("y_zero_point", {6}, {0, 0, 0, 0, 0, 0}); + test.AddOutput("y", dims, {1, 2, 3, 4, 5, 6}); + + std::vector> execution_providers; + execution_providers.emplace_back(std::move(dml_ep)); + test.ConfigEps(std::move(execution_providers)) + .RunWithConfig(); +} + +// Opset 21 QuantizeLinear float32→uint8 WITHOUT zero_point. +// Without zero_point, the output type defaults to uint8. +TEST(QuantizeLinearOpTest, Uint8_5D_NoZeroPoint_Opset21_DML) { + auto dml_ep = DefaultDmlExecutionProvider(); + if (!dml_ep) { + GTEST_SKIP() << "Skipping because DML EP is not available."; + } + + OpTester test("QuantizeLinear", 21); + std::vector dims{6, 1, 1, 1, 1}; + test.AddInput("x", dims, {0.0f, 51.0f, 102.0f, 153.0f, 204.0f, 255.0f}); + test.AddInput("y_scale", {}, {1.0f}); + test.AddOutput("y", dims, {0, 51, 102, 153, 204, 255}); + + std::vector> execution_providers; + execution_providers.emplace_back(std::move(dml_ep)); + test.ConfigEps(std::move(execution_providers)) + .RunWithConfig(); +} + +// Opset 21 QuantizeLinear float32→int8 with zero_point (the customer's exact scenario). +TEST(QuantizeLinearOpTest, Int8_5D_WithZeroPoint_Opset21_DML) { + auto dml_ep = DefaultDmlExecutionProvider(); + if (!dml_ep) { + GTEST_SKIP() << "Skipping because DML EP is not available."; + } + + OpTester test("QuantizeLinear", 21); + std::vector dims{6, 1, 1, 1, 1}; + test.AddInput("x", dims, {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}); + test.AddInput("y_scale", {}, {1.0f}); + test.AddInput("y_zero_point", {}, {0}); + test.AddOutput("y", dims, {1, 2, 3, 4, 5, 6}); + + std::vector> execution_providers; + execution_providers.emplace_back(std::move(dml_ep)); + test.ConfigEps(std::move(execution_providers)) + .RunWithConfig(); +} + // Test uint16 QuantizeLinear (per tensor) TEST(QuantizeLinearOpTest, Uint16) { OpTester test("QuantizeLinear", 21); diff --git a/onnxruntime/test/providers/dml_safe_make_or_throw_test.cc b/onnxruntime/test/providers/dml_safe_make_or_throw_test.cc new file mode 100644 index 0000000000000..8041f0dae8c28 --- /dev/null +++ b/onnxruntime/test/providers/dml_safe_make_or_throw_test.cc @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifdef USE_DML + +#include "gtest/gtest.h" + +#include +#include +#include "core/providers/dml/DmlExecutionProvider/src/SafeMakeOrThrow.h" + +#include + +namespace onnxruntime { +namespace test { + +// A trivial COM interface for testing. +MIDL_INTERFACE("A1B2C3D4-E5F6-7890-ABCD-EF1234567890") +ITestInterface : public IUnknown { + virtual int STDMETHODCALLTYPE GetValue() = 0; +}; + +// A RuntimeClass whose constructor succeeds and stores a value. +class SucceedingClass : public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, ITestInterface> { + public: + int value; + + SucceedingClass(int v) : value(v) {} + + int STDMETHODCALLTYPE GetValue() override { return value; } +}; + +// A RuntimeClass that tracks whether its destructor ran. +class TrackedClass : public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, ITestInterface> { + public: + bool& destroyed; + + TrackedClass(bool& flag) : destroyed(flag) { destroyed = false; } + ~TrackedClass() { destroyed = true; } + + int STDMETHODCALLTYPE GetValue() override { return 42; } +}; + +// A RuntimeClass whose constructor always throws. +// Uses a ref-counted witness to verify cleanup: the witness is destroyed +// (via Release) during stack unwinding if memory is freed correctly. +class ThrowingClass : public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, ITestInterface> { + public: + Microsoft::WRL::ComPtr witness; + + ThrowingClass(bool& witness_destroyed) { + // Create a witness that will be destroyed when this object's members + // are cleaned up during stack unwinding. + witness = Dml::SafeMakeOrThrow(witness_destroyed); + throw std::runtime_error("intentional throw"); + } + + int STDMETHODCALLTYPE GetValue() override { return -1; } +}; + +// Verify that SafeMakeOrThrow creates an object with ref count 1, +// and that the object is properly released when the ComPtr goes out of scope. +TEST(SafeMakeOrThrowTest, SuccessPath_RefCountIsOne) { + Microsoft::WRL::ComPtr obj = Dml::SafeMakeOrThrow(123); + + ASSERT_NE(obj.Get(), nullptr); + EXPECT_EQ(obj->GetValue(), 123); + + // AddRef/Release to observe ref count: AddRef returns new count. + unsigned long refAfterAdd = obj->AddRef(); + EXPECT_EQ(refAfterAdd, 2u); + + unsigned long refAfterRelease = obj->Release(); + EXPECT_EQ(refAfterRelease, 1u); +} + +// Verify that the object is destroyed when the last ComPtr releases it. +TEST(SafeMakeOrThrowTest, SuccessPath_DestructorRunsOnRelease) { + bool destroyed = false; + { + auto obj = Dml::SafeMakeOrThrow(destroyed); + EXPECT_FALSE(destroyed); + } + // ComPtr went out of scope — destructor should have run. + EXPECT_TRUE(destroyed); +} + +// Verify that copying the ComPtr increments the ref count and +// the object survives until the last reference is released. +TEST(SafeMakeOrThrowTest, SuccessPath_MultipleReferences) { + bool destroyed = false; + Microsoft::WRL::ComPtr copy; + { + auto obj = Dml::SafeMakeOrThrow(destroyed); + copy = obj; + EXPECT_FALSE(destroyed); + } + // Original ComPtr gone, but copy still holds a reference. + EXPECT_FALSE(destroyed); + + copy.Reset(); + EXPECT_TRUE(destroyed); +} + +// Verify that when the constructor throws, the exception propagates +// and sub-objects are properly cleaned up (no leak). +TEST(SafeMakeOrThrowTest, FailurePath_ConstructorThrows) { + bool witness_destroyed = false; + EXPECT_THROW( + Dml::SafeMakeOrThrow(witness_destroyed), + std::runtime_error); + // The witness ComPtr member was constructed before the throw. + // If cleanup worked correctly, the witness should have been destroyed + // when the ThrowingClass sub-objects were unwound. + EXPECT_TRUE(witness_destroyed); +} + +// Verify that QI works correctly on a SafeMakeOrThrow-created object. +TEST(SafeMakeOrThrowTest, SuccessPath_QueryInterface) { + auto obj = Dml::SafeMakeOrThrow(42); + + Microsoft::WRL::ComPtr unk; + HRESULT hr = obj.As(&unk); + EXPECT_EQ(hr, S_OK); + EXPECT_NE(unk.Get(), nullptr); + + Microsoft::WRL::ComPtr iface; + hr = unk.As(&iface); + EXPECT_EQ(hr, S_OK); + EXPECT_EQ(iface->GetValue(), 42); +} + +} // namespace test +} // namespace onnxruntime + +#endif // USE_DML From d352abeab32e152ebc0cf1e413a6c3febcb1f371 Mon Sep 17 00:00:00 2001 From: adrastogi Date: Sun, 29 Mar 2026 20:02:57 -0700 Subject: [PATCH 10/14] Fix overflow in DmlGraphFusionHelper::ProcessInputData (#27815) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description This change tries to address a problem in the DML EP where AlignToPow2 rounded up tensorByteSize to a 4-byte boundary before the data was read from the source buffer. This caused CreateCpuResource, CreateResource, WriteToFile, and the inputRawData vector construction to read 1–3 bytes past the end of the original tensor data. CreateResource and CreateCpuResource already independently align the D3D12 resource descriptor size, so they work correctly with the original (unaligned) byte count. The fix is to move the alignment to the location where it's needed. ### Motivation and Context This is required because it addresses a crash / incorrect behavior in the DML EP. --- .../src/DmlGraphFusionHelper.cpp | 7 ++--- .../cpu/tensor/quantize_linear_test.cc | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlGraphFusionHelper.cpp b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlGraphFusionHelper.cpp index 6bd7de0fba5cb..4ddf8b8640376 100644 --- a/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlGraphFusionHelper.cpp +++ b/onnxruntime/core/providers/dml/DmlExecutionProvider/src/DmlGraphFusionHelper.cpp @@ -232,8 +232,6 @@ namespace DmlGraphFusionHelper } } - // Tensor sizes in DML must be a multiple of 4 bytes large. - tensorByteSize = AlignToPow2(tensorByteSize, 4); if(graphSerializationEnabled) { WriteToFile(modelName, ConvertToWString(iter->first) + L".bin", reinterpret_cast(tensorPtr), tensorByteSize); @@ -264,9 +262,10 @@ namespace DmlGraphFusionHelper initializeInputBuffer = CreateCpuResource(providerImpl, tensorPtr, tensorByteSize); } - // Set the binding for operator initialization to the buffer + // Set the binding for operator initialization to the buffer. + // DML requires buffer binding sizes to be a multiple of 4 bytes. initInputBindings[i].Buffer = initializeInputBuffer.Get(); - initInputBindings[i].SizeInBytes = tensorByteSize; + initInputBindings[i].SizeInBytes = AlignToPow2(tensorByteSize, 4); initializeResourceRefs.push_back(std::move(initializeInputBuffer)); } diff --git a/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc b/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc index 82e58dcd71fdc..1672433aaac3a 100644 --- a/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc @@ -74,6 +74,34 @@ TEST(DequantizeLinearOpTest, Int4_LargeInitializerInput) { test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kTensorrtExecutionProvider}); } +// Regression test: int8 tensor whose byte size is not a multiple of 4. +// DML graph fusion rounds tensor sizes to a multiple of 4 via AlignToPow2. +// If the original buffer is not padded, the subsequent memcpy reads past the +// allocation boundary (heap-buffer-overflow detectable with ASan). +// Mirrors the WebNN PoC: dequantizeLinear with int8[135] (135 % 4 != 0). +TEST(DequantizeLinearOpTest, Int8_NonAlignedSize_Initializer) { + OpTester test("DequantizeLinear", 10); + constexpr int64_t kNumElements = 135; // 135 bytes, AlignToPow2(135,4)=136 + + std::vector x_data(kNumElements); + std::vector y_expected(kNumElements); + const float scale = 0.5f; + const int8_t zero_point = 0; + for (int64_t i = 0; i < kNumElements; ++i) { + x_data[i] = static_cast(i % 127); + y_expected[i] = (x_data[i] - zero_point) * scale; + } + + // Mark all inputs as initializers so they go through DML's ProcessInputData + // → UnpackInitializer → AlignToPow2 code path during graph fusion. + test.AddInput("x", {kNumElements}, x_data, /*is_initializer=*/true); + test.AddInput("x_scale", {1}, {scale}, /*is_initializer=*/true); + test.AddInput("x_zero_point", {1}, {zero_point}, /*is_initializer=*/true); + test.AddOutput("y", {kNumElements}, y_expected); + + test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kTensorrtExecutionProvider}); +} + // scalar zero & scale with int4 TEST(DequantizeLinearOpTest, Int4) { OpTester test("DequantizeLinear", 21); From f4bdbb8d18580f532841cccef7d3557bd5175521 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Mon, 30 Mar 2026 01:19:24 -0700 Subject: [PATCH 11/14] Annotation based partitioning along with resource accounting (#27595) This pull request introduces support for node "layering annotations" and improves resource accounting and memory management during graph partitioning in ONNX Runtime. The changes add new mechanisms for annotating nodes, filtering nodes by annotation during partitioning, and efficiently accounting for resources in fused nodes. Several APIs are extended to support these features, and new configuration options are introduced to guide layer assignment. **Layering annotations & partitioning:** * Added `layering_annotation_` member and associated getter/setter/clear methods to the `Node` class, allowing nodes to be annotated for layer assignment. Also added a method to clear these annotations after partitioning to save memory. (`include/onnxruntime/core/graph/graph.h`) [[1]](diffhunk://#diff-aaea1507ec81a94c72a1fa72ce320df712156b665f7798573be3f7e439bb4c37R177-R184) [[2]](diffhunk://#diff-aaea1507ec81a94c72a1fa72ce320df712156b665f7798573be3f7e439bb4c37R266-R272) [[3]](diffhunk://#diff-aaea1507ec81a94c72a1fa72ce320df712156b665f7798573be3f7e439bb4c37R702-R703) * Extended the graph partitioning logic to support filtering nodes by their layering annotation using a `LayeringIndex`, ensuring only nodes matching the current execution provider's assignment are considered during partitioning. (`onnxruntime/core/framework/graph_partitioner.cc`) [[1]](diffhunk://#diff-e2d3910ae7593ee7ba4fd74e53f738fa973ae2fc32c069f1088ba458b91f8d4bR155) [[2]](diffhunk://#diff-e2d3910ae7593ee7ba4fd74e53f738fa973ae2fc32c069f1088ba458b91f8d4bR199-R286) [[3]](diffhunk://#diff-e2d3910ae7593ee7ba4fd74e53f738fa973ae2fc32c069f1088ba458b91f8d4bL244-R357) [[4]](diffhunk://#diff-e2d3910ae7593ee7ba4fd74e53f738fa973ae2fc32c069f1088ba458b91f8d4bL433-R545) [[5]](diffhunk://#diff-e2d3910ae7593ee7ba4fd74e53f738fa973ae2fc32c069f1088ba458b91f8d4bL451-R564) [[6]](diffhunk://#diff-e2d3910ae7593ee7ba4fd74e53f738fa973ae2fc32c069f1088ba458b91f8d4bL477-R591) * Added a new session option `kOrtSessionOptionsLayerAssignmentSettings` to configure layer assignment using annotation prefixes per device. (`include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h`) **Resource accounting improvements:** * Improved the `IResourceAccountant` interface to allow resetting and committing pending weights per node, and updated resource accounting logic to correctly sum and commit costs for all constituent nodes in fused nodes, preventing double-counting or undercounting. (`include/onnxruntime/core/framework/resource_accountant.h`, `include/onnxruntime/core/graph/indexed_sub_graph.h`, `onnxruntime/core/framework/graph_partitioner.cc`) [[1]](diffhunk://#diff-7b1c9ef14536f9a66ed370cb729b6609d12c5907b460d8f145a7ad5a401e0fb6L48-R72) [[2]](diffhunk://#diff-3f09a80586759ee33e272477c3eb96f28d9b37f1e8251d13f1211c0450945135L89-R114) [[3]](diffhunk://#diff-e2d3910ae7593ee7ba4fd74e53f738fa973ae2fc32c069f1088ba458b91f8d4bL391-L397) **API and code organization:** * Updated the `Graph` class and related APIs to propagate layering annotations during function inlining and to provide a method for removing all layering annotations after partitioning. (`include/onnxruntime/core/graph/graph.h`) [[1]](diffhunk://#diff-aaea1507ec81a94c72a1fa72ce320df712156b665f7798573be3f7e439bb4c37R1341-R1346) [[2]](diffhunk://#diff-aaea1507ec81a94c72a1fa72ce320df712156b665f7798573be3f7e439bb4c37R1590-R1594) * Moved the `CreateAccountants` function out of the `NodeStatsRecorder` class to the namespace level for clarity. (`include/onnxruntime/core/framework/resource_accountant.h`) These changes enable more flexible and memory-efficient graph partitioning, particularly for scenarios involving hardware-specific layer assignments and dynamic resource constraints. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/Optimizer_Layering_Annotations.md | 130 ++ .../core/framework/resource_accountant.h | 25 +- include/onnxruntime/core/graph/graph.h | 105 +- .../core/graph/indexed_sub_graph.h | 24 +- .../onnxruntime_session_options_config_keys.h | 22 +- .../core/framework/graph_partitioner.cc | 219 +- .../core/framework/graph_partitioner.h | 2 + .../core/framework/layering_annotations.cc | 584 ++++++ .../core/framework/layering_annotations.h | 213 ++ .../core/framework/resource_accountant.cc | 157 +- .../core/framework/tensorprotoutils.cc | 13 + onnxruntime/core/framework/tensorprotoutils.h | 10 + onnxruntime/core/graph/graph.cc | 115 +- onnxruntime/core/graph/graph_utils.cc | 149 +- onnxruntime/core/graph/graph_utils.h | 16 + .../core/optimizer/dq_matmulnbits_fusion.cc | 4 +- .../core/optimizer/embed_layer_norm_fusion.cc | 12 +- onnxruntime/core/optimizer/gelu_fusion.cc | 2 +- onnxruntime/core/optimizer/gemm_sum_fusion.cc | 3 +- .../core/optimizer/gemm_transpose_fusion.cc | 3 +- .../core/optimizer/layer_norm_fusion.cc | 4 +- .../core/optimizer/matmul_add_fusion.cc | 7 +- .../core/optimizer/matmul_bn_fusion.cc | 1 + .../ensure_unique_dq_for_node_unit.cc | 2 + .../qdq_transformer/qdq_propagation.cc | 6 + .../weight_bias_quantization.cc | 14 +- .../qdq_transformer/where_dummy_dq.cc | 1 + onnxruntime/core/optimizer/reshape_fusion.cc | 3 +- .../slice_concat_to_space_to_depth_fusion.cc | 2 + .../core/optimizer/stft_decomposition.cc | 68 +- .../onnx_transpose_optimization.cc | 15 + .../transpose_optimization/optimizer_api.h | 12 + .../ort_optimizer_api_impl.cc | 11 + onnxruntime/core/optimizer/utils.cc | 7 + onnxruntime/core/optimizer/utils.h | 2 + .../providers/cuda/cuda_execution_provider.cc | 8 +- onnxruntime/core/session/inference_session.cc | 26 +- onnxruntime/core/session/onnxruntime_c_api.cc | 226 +-- .../python/tools/layering/layer_annotate.py | 165 ++ onnxruntime/test/framework/function_test.cc | 156 ++ .../framework/layering_annotations_test.cc | 1763 +++++++++++++++++ .../framework/resource_accountant_test.cc | 327 +++ .../test/framework/session_state_test.cc | 151 +- .../test/framework/tensorutils_test.cc | 58 + .../tiny_gpt2_beamsearch_layering.onnx | Bin 0 -> 580131 bytes .../tiny_gpt2_beamsearch_layering.txt | 55 + 46 files changed, 4586 insertions(+), 312 deletions(-) create mode 100644 docs/Optimizer_Layering_Annotations.md create mode 100644 onnxruntime/core/framework/layering_annotations.cc create mode 100644 onnxruntime/core/framework/layering_annotations.h create mode 100644 onnxruntime/python/tools/layering/layer_annotate.py create mode 100644 onnxruntime/test/framework/layering_annotations_test.cc create mode 100644 onnxruntime/test/framework/resource_accountant_test.cc create mode 100644 onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.onnx create mode 100644 onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.txt diff --git a/docs/Optimizer_Layering_Annotations.md b/docs/Optimizer_Layering_Annotations.md new file mode 100644 index 0000000000000..a268bd8fbe84f --- /dev/null +++ b/docs/Optimizer_Layering_Annotations.md @@ -0,0 +1,130 @@ +# Optimizer Layering Annotations + +## Overview + +Layering annotations are per-node metadata strings that guide graph partitioning by indicating which execution provider (EP) layer a node belongs to. They are loaded from the ONNX model's `NodeProto` metadata (key `"layer_ann"`) and consumed during the partitioning phase to influence EP assignment. + +## Execution Pipeline + +Graph optimizers run in ordered levels: + +``` +Level 0 (Basic) ─► Level 1 (Extended) ─► Partitioning ─► Level 2+ (Layout, etc.) +``` + +1. **Level 0 and Level 1** optimizers run **before** partitioning. At this point, layering annotations are present on nodes and must be preserved through any graph transformations. +2. **Partitioning** reads the annotations to assign nodes to execution providers. +3. After partitioning, `Graph::RemoveAllLayeringAnnotations()` clears all annotations. +4. **Level 2, 3, and 4** optimizers run **after** annotations have been cleared. They do not need to handle annotations. + +**Key rule: Only Level 1 (and Level 0) optimizers need to propagate layering annotations.** + +## Why Propagation Matters + +When an optimizer replaces, fuses, or decomposes nodes, the original annotated node is removed and new nodes are created. If the new nodes do not carry the original annotation, partitioning loses the assignment hint for that subgraph, potentially causing incorrect EP placement. + +## How to Propagate Annotations + +### Preferred: Use the `AddNode` Overload with `annotation_source` + +`Graph::AddNode` provides overloads that accept a `const Node& annotation_source` parameter. The new node automatically inherits the layering annotation from the source node. + +```cpp +// Instead of: +Node& new_node = graph.AddNode(name, op_type, description, inputs, outputs); +// Missing annotation propagation! + +// Use: +Node& new_node = graph.AddNode(name, op_type, description, inputs, outputs, + original_node); // annotation_source +``` + +All standard `AddNode` signatures have a corresponding `annotation_source` variant: + +```cpp +// With const NodeAttributes* +Node& AddNode(name, op_type, description, + gsl::span inputs, + gsl::span outputs, + const Node& annotation_source, + const NodeAttributes* attributes = nullptr, + const std::string& domain = kOnnxDomain); + +// With NodeAttributes&& +Node& AddNode(name, op_type, description, + gsl::span inputs, + gsl::span outputs, + const Node& annotation_source, + NodeAttributes&& attributes, + const std::string& domain = kOnnxDomain); + +// initializer_list variants also available +``` + +### Legacy: `DuplicateNodeAnnotation` + +The utility function `optimizer_utils::DuplicateNodeAnnotation(src, dst)` copies annotations between existing nodes. This is still used when the annotation source is conditional (e.g., when the source node pointer may be null). Prefer the `AddNode` overload for unconditional propagation. + +### Automatic Propagation + +`Graph::AddNode(const Node& other)` — the copy overload used for duplicating nodes — automatically copies annotations. No additional action is needed when duplicating a node via this overload. + +## Post-Partitioning: Propagating EP Assignments + +Although Level 2+ optimizers do not deal with layering annotations directly (they have been cleared), they must still propagate **execution provider (EP) assignments**. EP assignments are the downstream result of the annotation-driven partitioning step. After partitioning, each node carries an EP assignment (e.g., `CUDAExecutionProvider`, `CPUExecutionProvider`) that determines where the node's kernel runs. + +When a Level 2+ optimizer creates new nodes that replace or derive from existing ones, it must copy the EP assignment from the source node: + +```cpp +Node& new_node = graph.AddNode(name, op_type, description, inputs, outputs); +new_node.SetExecutionProviderType(original_node.GetExecutionProviderType()); +``` + +Failing to propagate the EP assignment causes the new node to fall back to the default provider (typically CPU), silently breaking the intended placement and potentially degrading performance or correctness. This requirement predates the layering annotation feature and applies to all optimizers that run after partitioning. + +> **Note:** The `AddNode` overload with `annotation_source` propagates both the layering annotation *and* nothing else — EP assignment is still set separately. Layering annotations and EP assignments serve different stages of the pipeline and are managed independently. + +## When You Do NOT Need to Propagate Annotations + +- **Level 2+ optimizers** — annotations have already been consumed and cleared (but EP assignments must still be propagated, see above). +- **Training optimizers** — training runs after partitioning. +- **Optimizers that only remove nodes** (e.g., identity elimination) — no new nodes are created. +- **Optimizers that modify nodes in-place** — the annotation remains on the existing node. + +## Examples + +### Fusion (replacing multiple nodes with one) + +```cpp +// GeluFusion: fusing Div + Erf + Add + Mul + Mul into a single Gelu +Node& gelu_node = graph.AddNode( + graph.GenerateNodeName("Gelu"), + "Gelu", "fused Gelu subgraphs", + {gelu_input}, {gelu_output}, + div_node); // propagate annotation from the root matched node +``` + +### Decomposition (replacing one node with many) + +```cpp +// STFT decomposition: each new node inherits from the original STFT node +auto [reshape_node, reshape_out] = AddNode(graph, "Reshape", ep, inputs, &stft); +auto [conv_node, conv_out] = AddNode(graph, "Conv", ep, conv_inputs, &stft); +auto [concat_node, concat_out] = AddNode(graph, "Concat", ep, concat_inputs, &stft); +``` + +### Conditional source (use DuplicateNodeAnnotation) + +```cpp +Node& q_node = graph.AddNode(...); +if (src_node) { + optimizer_utils::DuplicateNodeAnnotation(*src_node, q_node); +} +``` + +## Checklist for New Level 1 Optimizers + +1. Identify the "source" node whose annotation should propagate (typically the root of the matched pattern). +2. For every `graph.AddNode(...)` call that creates a replacement node, use the `annotation_source` overload. +3. If the source is conditional (may be null), use `optimizer_utils::DuplicateNodeAnnotation` after the `AddNode` call. +4. Test with an annotated model to verify annotations survive the transformation. diff --git a/include/onnxruntime/core/framework/resource_accountant.h b/include/onnxruntime/core/framework/resource_accountant.h index b072e27816463..7bb5a993d140b 100644 --- a/include/onnxruntime/core/framework/resource_accountant.h +++ b/include/onnxruntime/core/framework/resource_accountant.h @@ -45,18 +45,31 @@ class IResourceAccountant { virtual ResourceCount GetConsumedAmount() const = 0; virtual void AddConsumedAmount(const ResourceCount& amount) = 0; virtual void RemoveConsumedAmount(const ResourceCount& amount) = 0; - virtual ResourceCount ComputeResourceCount(const Node& node) const = 0; + virtual ResourceCount ComputeResourceCount(const Node& node) = 0; std::optional GetThreshold() const { return threshold_; } + void SetThreshold(const ResourceCount& threshold) { + threshold_ = threshold; + } + void SetStopAssignment() noexcept { stop_assignment_ = true; } bool IsStopIssued() const noexcept { return stop_assignment_; } + // Called before each GetCapability pass to discard pending weight tracking + // from a previous (discarded) pass. Default no-op for stats-based accountants. + virtual void ResetPendingWeights() {} + + // Called when a node's cost is committed (AccountForNode/AccountForAllNodes). + // Moves the node's pending weights into the committed set so they persist + // across GetCapability passes. Default no-op for stats-based accountants. + virtual void CommitWeightsForNode(size_t /*node_index*/) {} + static std::string MakeUniqueNodeName(const Node& node); private: @@ -114,11 +127,6 @@ class NodeStatsRecorder { void DumpStats(const std::filesystem::path& model_path) const; - [[nodiscard]] static Status CreateAccountants( - const ConfigOptions& config_options, - const std::filesystem::path& model_path, - std::optional& acc_map); - private: void DumpStats(std::ostream& os) const; @@ -126,4 +134,9 @@ class NodeStatsRecorder { std::unique_ptr impl_; }; +Status CreateAccountants( + const ConfigOptions& config_options, + const std::filesystem::path& model_path, + std::optional& acc_map); + } // namespace onnxruntime diff --git a/include/onnxruntime/core/graph/graph.h b/include/onnxruntime/core/graph/graph.h index 58473a79ddaa6..c5351bc5dfef7 100644 --- a/include/onnxruntime/core/graph/graph.h +++ b/include/onnxruntime/core/graph/graph.h @@ -174,7 +174,14 @@ class Node { */ void SetSinceVersion(int since_version) noexcept { since_version_ = since_version; } + void SetLayeringAnnotation(std::string annotation) { layering_annotation_ = std::move(annotation); } + + const std::string& GetLayeringAnnotation() const noexcept { return layering_annotation_; } + + const Graph* GetContainingGraph() const noexcept { return graph_; } + #if !defined(ORT_MINIMAL_BUILD) + /** Gets the Node's OpSchema. @remarks The graph containing this node must be resolved, otherwise nullptr will be returned. */ const ONNX_NAMESPACE::OpSchema* Op() const noexcept { return op_; } @@ -256,6 +263,13 @@ class Node { #endif // !defined(ORT_MINIMAL_BUILD) #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + + // Make sure that the annotation does not occupy memory after partitioning is done. + void ClearLayeringAnnotation() { + std::string t; + layering_annotation_.swap(t); + } + /** Gets a modifiable count of arguments for each of the Node's explicit inputs. @todo This should be removed in favor of a method that updates the input args and the count. Currently these operations are separate which is not a good setup. */ @@ -685,6 +699,8 @@ class Node { // Graph instances for subgraphs that are owned by this Node std::vector> subgraphs_; + std::string layering_annotation_; + // Can be saved? The node cannot be saved anymore if removable attributes have been cleared. bool can_be_saved_; }; @@ -1044,6 +1060,41 @@ class Graph { // NOLINT(clang-analyzer-optin.performance.Padding): preserve exi gsl::span output_args, NodeAttributes&& attributes, const std::string& domain = kOnnxDomain); + + /** Add a Node to this Graph, propagating the layering annotation from an existing node. + This is the preferred way to create new nodes in Level 1 (pre-partitioning) graph optimizers. + The new node automatically inherits the layering annotation from @p annotation_source, which + ensures correct layer-based partitioning when annotations are present. + @param name The Node name. Must be unique in this Graph. + @param op_type The operator type. e.g. ONNX operator name. + @param description Arbitrary description of the Node. + @param input_args The explicit inputs to this Node. + @param output_args The outputs from this Node. + @param annotation_source The node from which to inherit the layering annotation. + @param attributes Optional NodeAttributes to add. + @param domain The domain for the op_type. + @returns Reference to the new Node. + @remarks Use this overload in Level 1 optimizers that create nodes replacing or derived from + existing annotated nodes. See docs/Optimizer_Layering_Annotations.md for details. + */ + Node& AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + gsl::span input_args, + gsl::span output_args, + const Node& annotation_source, + const NodeAttributes* attributes = nullptr, + const std::string& domain = kOnnxDomain); + + Node& AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + gsl::span input_args, + gsl::span output_args, + const Node& annotation_source, + NodeAttributes&& attributes, + const std::string& domain = kOnnxDomain); + Node& AddNode(const std::string& name, const std::string& op_type, const std::string& description, @@ -1057,6 +1108,21 @@ class Graph { // NOLINT(clang-analyzer-optin.performance.Padding): preserve exi attributes, domain); } + Node& AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + std::initializer_list input_args, + std::initializer_list output_args, + const Node& annotation_source, + const NodeAttributes* attributes = nullptr, + const std::string& domain = kOnnxDomain) { + return AddNode(name, op_type, description, + AsSpan(input_args), + AsSpan(output_args), + annotation_source, + attributes, domain); + } + Node& AddNode(const std::string& name, const std::string& op_type, const std::string& description, @@ -1070,16 +1136,46 @@ class Graph { // NOLINT(clang-analyzer-optin.performance.Padding): preserve exi attributes, domain); } + Node& AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + gsl::span input_args, + std::initializer_list output_args, + const Node& annotation_source, + const NodeAttributes* attributes = nullptr, + const std::string& domain = kOnnxDomain) { + return AddNode(name, op_type, description, + input_args, + AsSpan(output_args), + annotation_source, + attributes, domain); + } + + Node& AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + std::initializer_list input_args, + gsl::span output_args, + const NodeAttributes* attributes = nullptr, + const std::string& domain = kOnnxDomain) { + return AddNode(name, op_type, description, + AsSpan(input_args), + output_args, + attributes, domain); + } + Node& AddNode(const std::string& name, const std::string& op_type, const std::string& description, std::initializer_list input_args, gsl::span output_args, + const Node& annotation_source, const NodeAttributes* attributes = nullptr, const std::string& domain = kOnnxDomain) { return AddNode(name, op_type, description, AsSpan(input_args), output_args, + annotation_source, attributes, domain); } @@ -1322,10 +1418,12 @@ class Graph { // NOLINT(clang-analyzer-optin.performance.Padding): preserve exi The Graph needs to be Resolve()d after this call. @param func_to_inline + @param parent_annotation. Annotation inherited from the parent node that is being inlined. @returns Status indicating success or providing an error message. */ - Status InlineFunctionProto(const ONNX_NAMESPACE::FunctionProto& func_to_inline); + Status InlineFunctionProto(const ONNX_NAMESPACE::FunctionProto& func_to_inline, + const std::string& parent_annotation); /** Mark a NodeArg name as coming from the outer scope when programmatically constructing a Graph that will be used as a GraphProto attribute in another Node. @@ -1569,6 +1667,11 @@ class Graph { // NOLINT(clang-analyzer-optin.performance.Padding): preserve exi // compiled model during partitioning, leaving them unused in the ORT Graph. To allow the memory to be freed // we need to manually run the cleanup that would usually happen as part of Graph::Resolve. Status RemovedUnusedInitializersOrtFormat(); + + // This examines all the nodes and removes any annotations that are only used for layering. + // This potentially saves memory. + Status RemoveAllLayeringAnnotations(); + #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) // This friendship relationship should only be used to call Graph::Graph and diff --git a/include/onnxruntime/core/graph/indexed_sub_graph.h b/include/onnxruntime/core/graph/indexed_sub_graph.h index 8ef4fdb66e1e6..54e878761ba87 100644 --- a/include/onnxruntime/core/graph/indexed_sub_graph.h +++ b/include/onnxruntime/core/graph/indexed_sub_graph.h @@ -86,18 +86,32 @@ struct IndexedSubGraph { // Should call IsAccountingEnabled() first // Takes the previously computed ResourceCount for the node - // (usually during GetCapabiilty()) + // (usually during GetCapability()) // if present and adds it to the consumed amount void AccountForNode(size_t cost_index) const { assert(cost_index < nodes_costs.size()); resource_accountant->AddConsumedAmount(nodes_costs[cost_index]); + resource_accountant->CommitWeightsForNode(nodes[cost_index]); } - // This computes and accounts for the resource cost for the node that just - // been fused from other nodes, and the EP did not had a chance to compute the costs. - void ComputeAndAccountForNode(const Node& node) const { + // Accounts for all constituent nodes by summing their pre-stored costs. + // Use this when fusing nodes into a single node so the total cost + // reflects what was computed during GetCapability() (with correct + // cross-node weight deduplication already applied). + void AccountForAllNodes() const { assert(resource_accountant != nullptr); - resource_accountant->AddConsumedAmount(resource_accountant->ComputeResourceCount(node)); + for (size_t i = 0; i < nodes_costs.size(); ++i) { + resource_accountant->AddConsumedAmount(nodes_costs[i]); + resource_accountant->CommitWeightsForNode(nodes[i]); + } + } + + // Accounts for a node given its index and a pre-computed resource cost. + // Use this when the cost was computed externally (e.g. for a fused node). + void AccountForNode(NodeIndex node_index, const ResourceCount& resource_count) const { + assert(resource_accountant != nullptr); + resource_accountant->AddConsumedAmount(resource_count); + resource_accountant->CommitWeightsForNode(node_index); } void SetAccountant(IResourceAccountant* res_accountant) { diff --git a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h index a9d9ac8323b16..9941224258506 100644 --- a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h +++ b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h @@ -325,13 +325,33 @@ static const char* const kOrtSessionOptionsCollectNodeMemoryStatsToFile = "sessi /// This is a composite CSV setting formatted as "memory limit in kb,file name for collected stats" /// "limit > 0": enables Capacity Aware Partitioning for Cuda EP. `limit` is optional and when absent /// the provider may attempt to figure out the memory available automatically. +/// The setting with no pre-recorded stats is expected to look like: "limit > 0,". +/// In this case, the EP will calculate memory using the initializers referenced by the node. +/// This enables an ad-hoc and flexible scenarios with no pre-recorded stats, but may be less accurate. /// The setting with no limit is expected to look like: ",file name for collected stats" -/// The EP will place nodes on device "file name" : +/// Finally a setting with both limit and pre-recorded stats absent can contain a single comma: ",". +/// The EP will attempt to place nodes on device (currently only CUDA is supported) : /// this file is expected to be found at the same folder with the model. The file contains /// pre-recorded stats collected when running with kOrtSessionOptionsCollectNodeMemoryStatsToFile enforce (see above) static const char* const kOrtSessionOptionsResourceCudaPartitioningSettings = "session.resource_cuda_partitioning_settings"; +/// +/// This is a setting that contains string annotations or annotation prefixes to be matched +/// against individual nodes metadata entry 'layer_ann' to guide layer assignment during partitioning. +/// The value is a semicolon separated list of strings or string prefixes per device. +/// Format: device1(annotation1, annotation2, ...); device2(annotation1, =annotation3, ...);... +/// Where: +/// - device1, device2, ... are the recognized device names to be matched against EPs configured in +/// the given session. +/// - annotation1, annotation2, ... are annotation prefixes to be matched against node annotations. Any +/// node annotation that starts with one of these prefixes will be matched. +/// - =annotation3 indicates an exact match for annotation3. Only node annotations that are exactly +/// equal to 'annotation3' will be matched. +/// TODO: add a list of recognized devices here. +/// +static const char* const kOrtSessionOptionsLayerAssignmentSettings = "session.layer_assignment_settings"; + // Enable EP context feature to dump the partitioned graph which includes the EP context into Onnx file. // The dumped Onnx model with EP context can be used for future inference to avoid the EP graph partitioning/compile overhead. // "0": disable. (default) diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index 9cb2111670ba6..cc65142318d02 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -16,6 +16,7 @@ #include "core/framework/kernel_lookup.h" #include "core/framework/kernel_registry_manager.h" #include "core/framework/kernel_registry.h" +#include "core/framework/layering_annotations.h" #include "core/framework/resource_accountant.h" #include "core/graph/function.h" #include "core/graph/function_utils.h" @@ -69,6 +70,7 @@ struct PartitionParams { std::reference_wrapper debug_graph_fn; #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) std::reference_wrapper on_partition_assignment_fn; + LayeringIndex* layering_index; }; } // namespace @@ -150,6 +152,7 @@ struct GetCapabilityForEPParams { IResourceAccountant* resource_accountant; std::reference_wrapper graph_optimizer_registry; std::reference_wrapper check_load_cancellation_fn; + LayeringIndex* layering_index; // Added member }; auto get_capabilities = [](const IExecutionProvider& ep, @@ -193,10 +196,94 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l auto& capabilities = params.capabilities.get(); const auto& graph_optimizer_registry = params.graph_optimizer_registry.get(); +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + InlinedVector assigned_filtered_in_nodes; + InlinedVector filtered_in_nodes; +#endif + // Helper to create a GraphViewer that filters nodes based on layering_index if present. + auto create_graph_viewer = [&](std::unique_ptr& out_sub_graph, + std::unique_ptr& out_viewer) -> Status { +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + if (params.layering_index) { + assigned_filtered_in_nodes.clear(); + filtered_in_nodes.clear(); + filtered_in_nodes.reserve(graph.NumberOfNodes()); + + auto rules_opt = params.layering_index->GetLayeringRulesForThisEp(ep_type); + if (rules_opt) { + assigned_filtered_in_nodes.reserve(rules_opt->get().size()); + } + + for (auto& node : graph.Nodes()) { + auto rule_idx_opt = params.layering_index->GetNodeAssignment(graph, node.Index()); + bool include = true; + if (rule_idx_opt) { + // If node has an assignment, include it only if it is assigned to this EP + if (!rules_opt || rules_opt->get().count(*rule_idx_opt) == 0) { + include = false; + } else { + assigned_filtered_in_nodes.push_back(node.Index()); + } + } + // If node has no assignment, it is included (available to any EP) + + if (include) { + filtered_in_nodes.push_back(&node); + } + } + ORT_RETURN_IF_ERROR(graph_utils::CreateFilteredIndexedGraph(filtered_in_nodes, graph, out_sub_graph)); + out_viewer = std::make_unique(graph, *out_sub_graph); + return Status::OK(); + } +#else + ORT_UNUSED_PARAMETER(out_sub_graph); +#endif + out_viewer = std::make_unique(graph); + return Status::OK(); + }; + // Helper to un-assign nodes that were assigned to this EP but not claimed by updated capabilities. + auto reset_assignment_unclaimed_nodes = [&]() { +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + if (params.layering_index) { + auto rules_opt = params.layering_index->GetLayeringRulesForThisEp(ep_type); + if (rules_opt) { + const auto& ep_rules = rules_opt->get(); + InlinedHashSet claimed; + for (const auto& cap : capabilities) { + if (cap && cap->sub_graph) { + for (auto idx : cap->sub_graph->nodes) claimed.insert(idx); + } + } + + // Check if all assigned filtered-in nodes are claimed + // and if not make them available for subsequent EPs + for (auto& node_index : assigned_filtered_in_nodes) { + if (claimed.count(node_index) == 0) { + auto rule_idx_opt = params.layering_index->GetNodeAssignment(graph, node_index); + if (rule_idx_opt && ep_rules.count(*rule_idx_opt) > 0) { + params.layering_index->MakeNodeUnassigned(graph, node_index); + } + } + } + assigned_filtered_in_nodes.clear(); + } + } +#endif + }; + { - const GraphViewer graph_viewer(graph); - capabilities = get_capabilities(current_ep, graph_viewer, kernel_lookup, params.resource_accountant, + std::unique_ptr sub_graph_holder; + std::unique_ptr graph_viewer; + ORT_RETURN_IF_ERROR(create_graph_viewer(sub_graph_holder, graph_viewer)); + + if (params.resource_accountant) { + params.resource_accountant->ResetPendingWeights(); + } + capabilities = get_capabilities(current_ep, *graph_viewer, kernel_lookup, params.resource_accountant, graph_optimizer_registry); + + reset_assignment_unclaimed_nodes(); + if (params.check_load_cancellation_fn()) { return ORT_MAKE_STATUS(ONNXRUNTIME, MODEL_LOAD_CANCELED, "Graph partitioning was canceled by user request"); @@ -241,9 +328,33 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l capabilities.clear(); - const GraphViewer graph_viewer(graph); - capabilities = get_capabilities(current_ep, graph_viewer, kernel_lookup, params.resource_accountant, +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + if (params.layering_index && end_node > first_new_node) { + // We need to update the LayeringIndex with newly created nodes + // as the layout transformation may have created new nodes + // with inherited annotations + InlinedVector new_node_indices; + for (NodeIndex idx = first_new_node; idx < end_node; ++idx) { + if (graph.GetNode(idx) != nullptr) { + new_node_indices.push_back(idx); + } + } + params.layering_index->Update(graph, new_node_indices); + } +#endif + + std::unique_ptr sub_graph_holder; + std::unique_ptr graph_viewer; + ORT_RETURN_IF_ERROR(create_graph_viewer(sub_graph_holder, graph_viewer)); + + if (params.resource_accountant) { + params.resource_accountant->ResetPendingWeights(); + } + capabilities = get_capabilities(current_ep, *graph_viewer, kernel_lookup, params.resource_accountant, graph_optimizer_registry); + + reset_assignment_unclaimed_nodes(); + if (params.check_load_cancellation_fn()) { return ORT_MAKE_STATUS(ONNXRUNTIME, MODEL_LOAD_CANCELED, "GetCapabilities was canceled by user request"); @@ -388,13 +499,13 @@ static Node* PlaceNode(Graph& graph, const IndexedSubGraph& capability, fused_node->SetExecutionProviderType(provider_type); if (acc_enabled) { - // We account for the fused node. We operate under assumption - // that the fused node would use no more memory when the nodes we are fusing. - // and potentially less than that, and therefore, no threshold check is needed here. - // All threshold checks are done within the EP. - capability.ComputeAndAccountForNode(*fused_node); + // Account for all constituent nodes using the per-node costs computed + // during GetCapability() (which already includes within-pass weight dedup). + // Computing the cost for the newly created fused node would undercount + // because the fused node often doesn't expose all original initializers, + // and would commit weights for the wrong node index. + capability.AccountForAllNodes(); } - result = fused_node; } else { // assign the nodes in the indexed subgraph to the current EP so that level 2+ optimizers will not change them. @@ -430,7 +541,8 @@ static Status PartitionOnnxFormatModelImpl(Graph& graph, FuncManager& func_mgr, const OnPartitionAssignmentFunction& on_partition_assignment_fn, const logging::Logger& logger, IResourceAccountant* resource_accountant, const GraphOptimizerRegistry& graph_optimizer_registry, - bool disable_model_compile) { + bool disable_model_compile, + LayeringIndex* layering_index) { // Added arg // handle testing edge case where optimizers or constant lifting results in graph with no nodes. // doing it here saves all providers checking for this in GetCapability if (graph.NumberOfNodes() == 0) { @@ -448,7 +560,8 @@ static Status PartitionOnnxFormatModelImpl(Graph& graph, FuncManager& func_mgr, check_load_cancellation_fn, on_partition_assignment_fn, logger, resource_accountant, - graph_optimizer_registry, disable_model_compile)); + graph_optimizer_registry, disable_model_compile, + layering_index)); // Pass through } } @@ -474,7 +587,8 @@ static Status PartitionOnnxFormatModelImpl(Graph& graph, FuncManager& func_mgr, std::cref(debug_graph_fn), resource_accountant, std::ref(graph_optimizer_registry), - std::cref(check_load_cancellation_fn)}; + std::cref(check_load_cancellation_fn), + layering_index}; // Pass param ORT_RETURN_IF_ERROR(GetCapabilityForEP(get_capability_params, logger)); if (capabilities.empty()) { @@ -654,17 +768,17 @@ static Status PartitionOnnxFormatModelImpl(Graph& graph, FuncManager& func_mgr, } // expand any nodes that have an ONNX function definition but no matching ORT kernel -static Status InlineNodes(Graph& graph, bool& modified_graph) { +static Status InlineNodes(Graph& graph, bool& modified_graph, LayeringIndex* layering_index) { // recurse into nested graphs first so we process from bottom up for (auto& node : graph.Nodes()) { for (auto& entry : node.GetAttributeNameToMutableSubgraphMap()) { Graph* subgraph = entry.second; - ORT_RETURN_IF_ERROR(InlineNodes(*subgraph, modified_graph)); + ORT_RETURN_IF_ERROR(InlineNodes(*subgraph, modified_graph, layering_index)); } } - // See if the node with no provider can be inlined. If one such nodes can be - // successfully inlined, we re-run the partitioner on the modified graph. + // See if the node with no provider can be inlined. If one such nodes can be successfully inlined, + // we re-run the partitioner on the modified graph. // NOTE: Inlining the function will change the nodes in the Graph instance, so we can't do that while iterating // using graph.Nodes(). InlinedVector nodes_to_inline; @@ -674,9 +788,50 @@ static Status InlineNodes(Graph& graph, bool& modified_graph) { } } + // Collect new node indices for nodes inlined from annotated parents so we can + // update the LayeringIndex in one batch. + InlinedVector new_node_indices; + for (auto* node : nodes_to_inline) { + // Check for an effective layering assignment: either from an explicit annotation + // on the node, or from an inherited assignment via the LayeringIndex (e.g., a function + // call node inside an annotated If/Loop subgraph that inherited its parent's rule). + const bool has_explicit_annotation = !node->GetLayeringAnnotation().empty(); + bool has_effective_assignment = has_explicit_annotation; + + if (layering_index != nullptr && !has_explicit_annotation) { + // The node may have an inherited-only assignment with no stored annotation string. + // Materialize the annotation on the node so Graph::InlineFunction propagates it + // to the newly created inlined nodes. + auto rule_idx = layering_index->GetNodeAssignment(graph, node->Index()); + if (rule_idx) { + has_effective_assignment = true; + const auto& rules = layering_index->GetRules(); + if (*rule_idx < rules.rules.size()) { + node->SetLayeringAnnotation(rules.rules[*rule_idx].annotation); + } + } + } + + const int max_before = has_effective_assignment ? graph.MaxNodeIndex() : 0; + ORT_RETURN_IF_ERROR(graph.InlineFunction(*node)); modified_graph = true; + + if (has_effective_assignment) { + const int max_after = graph.MaxNodeIndex(); + for (int i = max_before; i < max_after; ++i) { + if (graph.GetNode(static_cast(i)) != nullptr) { + new_node_indices.push_back(static_cast(i)); + } + } + } + } + + // Update the LayeringIndex so the next partitioning round filters correctly + // for the newly inlined nodes that inherited their parent's annotation. + if (layering_index != nullptr && !new_node_indices.empty()) { + layering_index->Update(graph, new_node_indices); } return Status::OK(); @@ -1018,7 +1173,7 @@ static Status PartitionOnnxFormatModel(const PartitionParams& partition_params, KernelRegistryManager& kernel_registry_manager, const std::optional& acc_map, const GraphOptimizerRegistry& graph_optimizer_registry, - const logging::Logger& logger, bool disable_model_compile) { + const logging::Logger& logger, bool disable_model_compile) { // Added arg bool modified_graph = false; auto& graph = partition_params.graph.get(); @@ -1046,12 +1201,13 @@ static Status PartitionOnnxFormatModel(const PartitionParams& partition_params, check_load_cancellation_fn, on_partition_assignment_fn, logger, resource_accountant, graph_optimizer_registry, - disable_model_compile)); + disable_model_compile, + partition_params.layering_index)); // Pass param } // expand any nodes that have an ONNX function definition but no matching ORT kernel. modified_graph = false; - ORT_RETURN_IF_ERROR(InlineNodes(graph, modified_graph)); + ORT_RETURN_IF_ERROR(InlineNodes(graph, modified_graph, partition_params.layering_index)); // Resolve and rerun graph partitioning and inlining if there was a change if (modified_graph) { @@ -1101,7 +1257,8 @@ static Status PartitionOrtFormatModelImpl(const PartitionParams& partition_param #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) nullptr, std::ref(graph_optimizer_registry), - partition_params.check_load_cancellation_fn + partition_params.check_load_cancellation_fn, + partition_params.layering_index }; // clang-format on @@ -1135,7 +1292,7 @@ static Status PartitionOrtFormatModelImpl(const PartitionParams& partition_param Node& fused_node = graph.BeginFuseSubGraph(indexed_sub_graph, node_name); fused_node.SetExecutionProviderType(type); if (indexed_sub_graph.IsAccountingEnabled()) { - indexed_sub_graph.ComputeAndAccountForNode(fused_node); + indexed_sub_graph.AccountForAllNodes(); } // create filtered graph viewer for this set of nodes @@ -1143,6 +1300,7 @@ static Status PartitionOrtFormatModelImpl(const PartitionParams& partition_param // TODO: Could avoid the topological sort in the GraphViewer ctor by constructing from an existing // GraphViewer instance instead of the Graph (copying the topological order instead of recalculating). auto viewer = std::make_unique(graph, indexed_sub_graph); + compilation_entries.push_back(CompilationEntry{std::move(viewer), fused_node, *capability}); #else // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "Compiling capabilities is not supported in this build."); @@ -1153,7 +1311,6 @@ static Status PartitionOrtFormatModelImpl(const PartitionParams& partition_param #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) // We will compile the fused nodes one by one, and fuse the subgraph if successful. for (const auto& compilation_entry : compilation_entries) { - const bool acc_enabled = compilation_entry.capability.get().sub_graph->IsAccountingEnabled(); Node& node = compilation_entry.fused_node; std::vector single_node_compute_func; ORT_RETURN_IF_ERROR(current_ep.Compile({IExecutionProvider::FusedNodeAndGraph{node, *compilation_entry.viewer}}, @@ -1184,9 +1341,7 @@ static Status PartitionOrtFormatModelImpl(const PartitionParams& partition_param // now that we're done compiling we can remove the original nodes from the Graph and wire in the new one graph.FinalizeFuseSubGraph(indexed_sub_graph, node); - if (acc_enabled) { - compilation_entry.capability.get().sub_graph->ComputeAndAccountForNode(node); - } + // accounting was already done via AccountForAllNodes() when the fused node was created above. } #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) @@ -1259,9 +1414,10 @@ Status GraphPartitioner::Partition(Graph& graph, FuncManager& func_mgr, const layout_transformation::TransformLayoutFunction& transform_layout_function, const ConfigOptions& config_options, const logging::Logger& logger, + LayeringIndex* layering_index, Mode mode, const epctx::ModelGenOptions& ep_context_gen_options, - const layout_transformation::DebugGraphFn& debug_graph_fn) const { + const layout_transformation::DebugGraphFn& debug_graph_fn) const { // Added arg // It is a greedy partitioning algorithm per provider preferences user provided when calling ONNX RUNTIME right now. // 1. Execution providers' capabilities are checked one by one. // 2. All sub-graphs that an execution provider returns will be assigned to it if it's not assigned yet. @@ -1292,7 +1448,8 @@ Status GraphPartitioner::Partition(Graph& graph, FuncManager& func_mgr, std::ref(fused_node_unique_id), std::cref(transform_layout_function), std::cref(debug_graph_fn), - std::cref(on_partition_assignment_fn_)}; + std::cref(on_partition_assignment_fn_), + layering_index}; #else // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) @@ -1303,7 +1460,7 @@ Status GraphPartitioner::Partition(Graph& graph, FuncManager& func_mgr, std::ref(graph), std::cref(check_load_cancellation_fn), std::cref(on_partition_assignment_fn_), - }; + layering_index}; #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) @@ -1323,12 +1480,12 @@ Status GraphPartitioner::Partition(Graph& graph, FuncManager& func_mgr, // We use this only if Resource Aware Partitioning is enabled for any of the EPs // The map is empty if not created if not enabled std::optional ep_acc_map; - ORT_RETURN_IF_ERROR(NodeStatsRecorder::CreateAccountants(config_options, graph.ModelPath(), ep_acc_map)); + ORT_RETURN_IF_ERROR(CreateAccountants(config_options, graph.ModelPath(), ep_acc_map)); bool disable_model_compile = config_options.GetConfigOrDefault(kOrtSessionOptionsDisableModelCompile, "0") == "1"; ORT_RETURN_IF_ERROR(PartitionOnnxFormatModel(partition_params, mode, providers_, kernel_registry_mgr_, ep_acc_map, *graph_optimizer_registry_, logger, - disable_model_compile)); + disable_model_compile)); // Pass param if (ep_context_gen_options.enable) { ORT_RETURN_IF_ERROR(CreateEpContextModel(providers_, graph, ep_context_gen_options, logger)); diff --git a/onnxruntime/core/framework/graph_partitioner.h b/onnxruntime/core/framework/graph_partitioner.h index eb70b9f89933d..4de9d94781b18 100644 --- a/onnxruntime/core/framework/graph_partitioner.h +++ b/onnxruntime/core/framework/graph_partitioner.h @@ -13,6 +13,7 @@ namespace onnxruntime { class ExecutionProviders; class KernelRegistryManager; +class LayeringIndex; class Model; struct ConfigOptions; @@ -60,6 +61,7 @@ class GraphPartitioner { const layout_transformation::TransformLayoutFunction& transform_layout_function, const ConfigOptions& config_options, const logging::Logger& logger, + LayeringIndex* layering_index, Mode mode = Mode::kNormal, const epctx::ModelGenOptions& ep_context_gen_options = {}, const layout_transformation::DebugGraphFn& debug_graph_fn = {}) const; diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc new file mode 100644 index 0000000000000..91df102abef17 --- /dev/null +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -0,0 +1,584 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + +#include "core/graph/constants.h" +#include "core/common/narrow.h" +#include "core/common/parse_string.h" +#include "core/common/string_utils.h" +#include "core/framework/layering_annotations.h" +#include "core/framework/ortmemoryinfo.h" +#include "core/session/abi_devices.h" +#include "core/framework/execution_providers.h" +#include "core/graph/graph.h" + +#include + +namespace onnxruntime { + +common::Status LayeringRules::FromConfigString(const std::string& config_value, LayeringRules& rules) { + rules.rules.clear(); + if (config_value.empty()) { + return common::Status::OK(); + } + + // Track seen annotations to reject duplicates. + // Separate sets for exact and prefix match annotations. + InlinedHashSet seen_exact_annotations; + InlinedHashSet seen_prefix_annotations; + + auto entries = utils::SplitString(config_value, ";"); + for (const auto& e : entries) { + auto entry = utils::TrimString(e); + if (entry.empty()) { + continue; + } + + const size_t open_paren = entry.find('('); + const size_t close_paren = entry.find(')'); + + if (open_paren == std::string::npos) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid layering config: Missing '(' in entry: ", entry); + } + if (close_paren == std::string::npos) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid layering config: Missing ')' in entry: ", entry); + } + if (close_paren < open_paren) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid layering config: ')' comes before '(' in entry: ", entry); + } + + std::string device = entry.substr(0, open_paren); + device = utils::TrimString(device); + + if (device.empty()) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid layering config: Empty device name in entry: ", entry); + } + + std::string annotations_list = entry.substr(open_paren + 1, close_paren - open_paren - 1); + auto annotations = utils::SplitString(annotations_list, ","); + for (auto& a : annotations) { + auto ann = utils::TrimString(a); + if (ann.empty()) { + continue; + } + + bool prefix_match = true; + if (ann[0] == '=') { + prefix_match = false; + ann = ann.substr(1); + ann = utils::TrimString(ann); + } + + if (ann.empty()) { + continue; + } + + // Check for duplicate annotation (same annotation string and match type) + auto& seen_set = prefix_match ? seen_prefix_annotations : seen_exact_annotations; + auto [it, inserted] = seen_set.insert(ann); + if (!inserted) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Invalid layering config: Duplicate ", (prefix_match ? "prefix" : "exact"), + " match annotation '", ann, "' found in entry: ", entry); + } + + rules.rules.push_back({device, std::move(ann), prefix_match}); + } + } + + return common::Status::OK(); +} + +LayeringRuleMatcher::LayeringRuleMatcher(const LayeringRules& rules) { + for (size_t i = 0; i < rules.rules.size(); ++i) { + const auto& rule = rules.rules[i]; + ORT_ENFORCE(!rule.annotation.empty(), "Layering rule annotation cannot be empty"); + if (rule.prefix_match) { + AddPrefixRule(rule.annotation, i); + } else { + AddExactRule(rule.annotation, i); + } + } +} + +std::optional LayeringRuleMatcher::Match(const std::string& node_annotation) const { + std::optional best_match = std::nullopt; + + // 1. Check Prefix Matches via Trie. Prefix have priority over exact matches. + const TrieNode* current = &root_; + + // No empty annotations + // so we omit checking the root. + + for (char c : node_annotation) { + if (best_match && *best_match == 0) { + // Optimization: If we already found index 0, we can't do better. + return best_match; + } + + auto child_it = current->children.find(c); + if (child_it == current->children.end()) { + break; + } + current = child_it->second.get(); + if (current->rule_index) { + UpdateBestMatch(best_match, *current->rule_index); + } + } + + if (best_match) { + return best_match; + } + + // 2. Check Exact Matches (fallback) + auto it = exact_match_rules_.find(node_annotation); + if (it != exact_match_rules_.end()) { + best_match = it->second; + } + + return best_match; +} + +namespace { +bool CaseInsensitiveCompare(std::string_view a, std::string_view b) { + return std::equal(a.begin(), a.end(), b.begin(), b.end(), + [](char c1, char c2) { + return std::tolower(static_cast(c1)) == + std::tolower(static_cast(c2)); + }); +} + +bool TryParseIndex(const std::string& str, uint32_t& index) { + if (str.empty()) return false; + return TryParseStringWithClassicLocale(str, index); +} + +// Sentinel value representing an unknown/unavailable device type. +// Used when an OrtEpDevice has neither hardware info nor memory info, +// so we cannot determine the actual device type. +constexpr OrtDevice::DeviceType kDeviceTypeUnknown = static_cast(-1); + +// Normalized view of an EP's device properties used by the matching logic. +// All fields are non-owning references or value types. +struct EpDeviceView { + std::string_view ep_name; + OrtDevice::DeviceType device_type; // OrtDevice::CPU, GPU, NPU, FPGA, or kDeviceTypeUnknown + uint32_t vendor_id; + OrtDevice::DeviceId device_id; + std::string_view vendor_string; // from OrtHardwareDevice::vendor (empty if unavailable) +}; + +bool MatchEpDevice(const EpDeviceView& ep, + std::string_view target_type_str, + std::string_view target_specifier, + std::string_view target_full) { + // "cpu" + if (CaseInsensitiveCompare(target_type_str, "cpu")) { + return ep.ep_name == kCpuExecutionProvider || + ep.device_type == OrtDevice::CPU; + } + // "gpu" + if (CaseInsensitiveCompare(target_type_str, "gpu")) { + if (target_specifier.empty()) { + if (ep.device_type == OrtDevice::GPU) return true; + // Heuristic fallback for common GPU EPs if hardware info is missing + return ep.ep_name == kCudaExecutionProvider || ep.ep_name == kDmlExecutionProvider; + } + // "gpu:" or "gpu:" + if (ep.device_type == OrtDevice::GPU) { + uint32_t index = std::numeric_limits::max(); + if (TryParseIndex(std::string(target_specifier), index)) { + return ep.device_id == static_cast(index); + } + // gpu: + if (!ep.vendor_string.empty() && CaseInsensitiveCompare(ep.vendor_string, target_specifier)) { + return true; + } + if (CaseInsensitiveCompare(target_specifier, "nvidia") && + ep.vendor_id == OrtDevice::VendorIds::NVIDIA) return true; + if (CaseInsensitiveCompare(target_specifier, "amd") && + ep.vendor_id == OrtDevice::VendorIds::AMD) return true; + if (CaseInsensitiveCompare(target_specifier, "intel") && + ep.vendor_id == OrtDevice::VendorIds::INTEL) return true; + // Heuristic: gpu:nvidia -> CUDA + if (CaseInsensitiveCompare(target_specifier, "nvidia") && + ep.ep_name == kCudaExecutionProvider) return true; + } + return false; + } + // "accelerator" (not cpu) + if (CaseInsensitiveCompare(target_type_str, "accelerator")) { + // Match if the EP is not a known CPU provider and its device type + // is not definitively CPU. Unknown device type (no HW/mem info) + // is treated as a potential accelerator. + return ep.ep_name != kCpuExecutionProvider && ep.device_type != OrtDevice::CPU; + } + // "npu" + if (CaseInsensitiveCompare(target_type_str, "npu")) { + if (ep.device_type == OrtDevice::NPU) return true; + return ep.ep_name == kQnnExecutionProvider || ep.ep_name == kVitisAIExecutionProvider; + } + // "fpga" + if (CaseInsensitiveCompare(target_type_str, "fpga")) { + return ep.device_type == OrtDevice::FPGA; + } + // "cuda" + if (CaseInsensitiveCompare(target_type_str, "cuda")) { + return ep.ep_name == kCudaExecutionProvider; + } + // "dml" + if (CaseInsensitiveCompare(target_type_str, "dml")) { + return ep.ep_name == kDmlExecutionProvider; + } + // Fallback: exact EP name match + return ep.ep_name == target_full; +} + +void ParseDeviceTarget(const std::string& target_full, + std::string& target_type_str, + std::string& target_specifier) { + const auto colon_pos = target_full.find(':'); + target_type_str = (colon_pos == std::string::npos) ? target_full : target_full.substr(0, colon_pos); + target_specifier = (colon_pos != std::string::npos) ? target_full.substr(colon_pos + 1) : std::string(); +} + +} // namespace + +std::optional EpLayeringMatcher::Match(gsl::span ep_devices, + const LayerAnnotation& rule) { + std::string target_type_str, target_specifier; + ParseDeviceTarget(rule.device, target_type_str, target_specifier); + + for (const auto* ep_device_ptr : ep_devices) { + if (!ep_device_ptr) continue; + const OrtEpDevice& ep_device = *ep_device_ptr; + + // Build normalized view from OrtEpDevice. + // Device type comes from either the hardware device or the memory info, + // with hardware device taking priority. If neither is available, + // device_type is set to kDeviceTypeUnknown. + OrtDevice::DeviceType device_type = kDeviceTypeUnknown; + bool has_hw = ep_device.device != nullptr; + if (has_hw) { + // Map OrtHardwareDeviceType to OrtDevice::DeviceType + switch (ep_device.device->type) { + case OrtHardwareDeviceType_GPU: + device_type = OrtDevice::GPU; + break; + case OrtHardwareDeviceType_NPU: + device_type = OrtDevice::NPU; + break; + case OrtHardwareDeviceType_CPU: + device_type = OrtDevice::CPU; + break; + default: + device_type = kDeviceTypeUnknown; + break; + } + } else if (ep_device.device_memory_info) { + device_type = ep_device.device_memory_info->device.Type(); + } + + EpDeviceView view{ + ep_device.ep_name, + device_type, + has_hw ? ep_device.device->vendor_id : 0u, + has_hw ? static_cast(ep_device.device->device_id) : OrtDevice::DeviceId{}, + has_hw ? std::string_view(ep_device.device->vendor) : std::string_view{}}; + + if (MatchEpDevice(view, target_type_str, target_specifier, rule.device)) { + return std::string(ep_device.ep_name); + } + } + return std::nullopt; +} + +std::optional EpLayeringMatcher::Match(const ExecutionProviders& providers, + const LayerAnnotation& rule) { + std::string target_type_str, target_specifier; + ParseDeviceTarget(rule.device, target_type_str, target_specifier); + + for (const auto& ep_shared_ptr : providers) { + if (!ep_shared_ptr) continue; + const IExecutionProvider& ep = *ep_shared_ptr; + const OrtDevice& device = ep.GetDevice(); + + EpDeviceView view{ + ep.Type(), + device.Type(), + device.Vendor(), + device.Id(), + {}}; // no vendor string available from IExecutionProvider + + if (MatchEpDevice(view, target_type_str, target_specifier, rule.device)) { + return std::string(ep.Type()); + } + } + return std::nullopt; +} + +LayeringIndex LayeringIndex::Create(const Graph& graph, + EpNameToLayeringIndices ep_map, + LayeringIndexToEpName rule_map, + LayeringRules layering_rules) { + // 1. Create LayeringIndex instance with pre-computed maps + LayeringIndex index(std::move(layering_rules), std::move(ep_map), std::move(rule_map)); + + // 2. Traverse the graph and index nodes + index.ProcessGraph(graph, std::nullopt); + + return index; +} + +Status LayeringIndex::Create(const Graph& graph, + const std::string& config_string, + gsl::span ep_devices, + const ExecutionProviders& ep_providers, + const logging::Logger& logger, + std::optional& layering_index) { + LayeringRules rules; + ORT_RETURN_IF_ERROR(LayeringRules::FromConfigString(config_string, rules)); + + LOGS(logger, INFO) << "Parsed " << rules.rules.size() << " layering rules from config."; + + if (rules.rules.empty()) { + // Return no index indicating no layering + layering_index.reset(); + return Status::OK(); + } + + // Identify which EPs satisfy which rules + EpNameToLayeringIndices ep_map; + LayeringIndexToEpName rule_map; + + size_t matched_rule_count = 0; + + for (size_t i = 0, lim = rules.rules.size(); i < lim; ++i) { + const auto& rule = rules.rules[i]; + + // 1. Try matching against ep_devices (from session options) + std::optional matched_ep; + if (!ep_devices.empty()) { + matched_ep = EpLayeringMatcher::Match(ep_devices, rule); + } + + // 2. If not matched, try matching against Registered EPs + if (!matched_ep) { + matched_ep = EpLayeringMatcher::Match(ep_providers, rule); + } + + if (matched_ep) { + const std::string& ep_type = *matched_ep; + ep_map[ep_type].insert(i); + // Ensure 1:1 mapping from rule index to EP type + // Note: A rule index refers to a unique entry in LayeringRules::rules vector. + // So 'i' is unique. + rule_map[i] = ep_type; + matched_rule_count++; + LOGS(logger, VERBOSE) << "Layering Rule " << i << " (" << rule.device << " -> " << rule.annotation + << ") mapped to EP: " << ep_type; + } else { + LOGS(logger, WARNING) << "Layering Rule " << i << " (" << rule.device << " -> " << rule.annotation + << ") could not be mapped to any available Execution Provider."; + } + } + + LOGS(logger, INFO) << "LayeringIndex created. Matched " << matched_rule_count + << " out of " << rules.rules.size() << " rules to available Execution Providers."; + + layering_index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + return Status::OK(); +} + +void LayeringIndex::ProcessGraph(const Graph& graph, std::optional parent_layer_id) { + // 3. Create entry for this graph instance + bool was_updated = false; + std::optional new_index; + GraphLayeringIndex* current_graph_index_ptr = nullptr; + auto found = graph_index_.find(&graph); + if (found != graph_index_.end()) { + current_graph_index_ptr = &found->second; + } else { + new_index.emplace(); + current_graph_index_ptr = &(*new_index); + } + GraphLayeringIndex& current_graph_index = *current_graph_index_ptr; + + for (auto& node : graph.Nodes()) { + std::optional matched_rule_idx = std::nullopt; + + // 4. For every node query its annotation + const std::string& annotation = node.GetLayeringAnnotation(); + if (!annotation.empty()) { + // If it has an annotation try to match it + matched_rule_idx = matcher_.Match(annotation); + } + + // 5. If node has no annotation, inherit from subgraph parent node + if (!matched_rule_idx && parent_layer_id) { + matched_rule_idx = parent_layer_id; + } + + // Record assignment if we have a match + if (matched_rule_idx) { + const size_t rule_idx = *matched_rule_idx; + + // Only assign if this rule maps to a valid EP in our configuration + if (layering_index_to_ep_name_.count(rule_idx)) { + ORT_IGNORE_RETURN_VALUE(current_graph_index.node_to_layering_index_.insert_or_assign(node.Index(), rule_idx)); + ORT_IGNORE_RETURN_VALUE(current_graph_index.layer_to_node_ids_[rule_idx].insert(node.Index())); + was_updated = true; + } else { + // reset since no valid EP mapping + matched_rule_idx = std::nullopt; + } + } + + // Recurse for subgraphs + if (node.ContainsSubgraph()) { + const std::optional subgraph_parent_assignment = matched_rule_idx; + for (auto& [attr_name, subgraph] : node.GetAttributeNameToSubgraphMap()) { + ProcessGraph(*subgraph, subgraph_parent_assignment); + } + } + } + if (was_updated && new_index) { + graph_index_.emplace(&graph, std::move(*new_index)); + } +} + +void LayeringIndex::Update(const Graph& graph, gsl::span nodes) { + // Ensure we have an entry for this graph (creating it if it doesn't exist, though typically it should) + bool was_updated = false; + std::optional new_index; + GraphLayeringIndex* current_graph_index_ptr = nullptr; + auto found = graph_index_.find(&graph); + if (found != graph_index_.end()) { + current_graph_index_ptr = &found->second; + } else { + new_index.emplace(); + current_graph_index_ptr = &(*new_index); + } + + auto& current_graph_index = *current_graph_index_ptr; + + for (NodeIndex node_index : nodes) { + // GetMutableNode because we want to ClearLayeringAnnotation if we use it + const Node* node = graph.GetNode(node_index); + if (!node) { + continue; + } + + const std::string& annotation = node->GetLayeringAnnotation(); + if (!annotation.empty()) { + auto matched_rule_idx = matcher_.Match(annotation); + + if (matched_rule_idx) { + const size_t rule_idx = *matched_rule_idx; + + // Only assign if this rule maps to a valid EP in our configuration + if (layering_index_to_ep_name_.count(rule_idx)) { + // Check if already assigned to a DIFFERENT rule, if so clean up old mapping + auto prev_assign = current_graph_index.node_to_layering_index_.find(node_index); + if (prev_assign != current_graph_index.node_to_layering_index_.end()) { + size_t old_rule = prev_assign->second; + if (old_rule != rule_idx) { + current_graph_index.layer_to_node_ids_[old_rule].erase(node_index); + } + } + + ORT_IGNORE_RETURN_VALUE(current_graph_index.node_to_layering_index_.insert_or_assign(node_index, rule_idx)); + ORT_IGNORE_RETURN_VALUE(current_graph_index.layer_to_node_ids_[rule_idx].insert(node_index)); + was_updated = true; + } + } + } + } + if (was_updated && new_index) { + graph_index_.emplace(&graph, std::move(*new_index)); + } +} + +void LayeringRuleMatcher::AddExactRule(const std::string& annotation, size_t index) { + // Only store the first occurrence (lowest index) + exact_match_rules_.insert({annotation, index}); +} + +void LayeringRuleMatcher::AddPrefixRule(const std::string& annotation, size_t index) { + TrieNode* current = &root_; + for (char c : annotation) { + auto p = current->children.insert({c, nullptr}); + if (p.second) { + p.first->second = std::make_unique(); + } + current = p.first->second.get(); + } + + // Only store if strictly better (lower index) or not set + // Since we iterate rules 0..N, if a rule index is already set for this node, + // it corresponds to a higher priority rule, so we skip overwriting it. + if (!current->rule_index) { + current->rule_index = index; + } +} + +void LayeringRuleMatcher::UpdateBestMatch(std::optional& current_best, size_t candidate) const { + if (!current_best || candidate < *current_best) { + current_best = candidate; + } +} + +std::optional>> +LayeringIndex::GetLayeringRulesForThisEp(const std::string& ep_type) const { + auto hit = ep_name_to_layering_indices_.find(ep_type); + if (hit == ep_name_to_layering_indices_.end()) { + return {}; + } + return hit->second; +} + +std::optional LayeringIndex::GetNodeAssignment(const Graph& graph, NodeIndex node_id) const { + auto hit = graph_index_.find(&graph); + if (hit == graph_index_.end()) { + return {}; + } + + // Nodes in subgraph that were not annotated has already inherited their + // annotation if any from the parent node of the subgraph + const auto& graph_layering_index = hit->second; + auto layer_hit = graph_layering_index.node_to_layering_index_.find(node_id); + if (layer_hit != graph_layering_index.node_to_layering_index_.end()) { + return layer_hit->second; + } + return {}; +} + +void LayeringIndex::MakeNodeUnassigned(const Graph& graph, NodeIndex node_id) { + auto hit = graph_index_.find(&graph); + if (hit == graph_index_.end()) { + return; + } + auto& graph_layering_index = hit->second; + auto node_to_layer_hit = graph_layering_index.node_to_layering_index_.find(node_id); + std::optional layer_idx; + if (node_to_layer_hit != graph_layering_index.node_to_layering_index_.end()) { + // Get the layer index + layer_idx = node_to_layer_hit->second; + graph_layering_index.node_to_layering_index_.erase(node_to_layer_hit); + } + // Remove node from layer collection + if (layer_idx) { + auto layer_to_nodes_hit = graph_layering_index.layer_to_node_ids_.find(*layer_idx); + if (layer_to_nodes_hit != graph_layering_index.layer_to_node_ids_.end()) { + layer_to_nodes_hit->second.erase(node_id); + if (layer_to_nodes_hit->second.empty()) { + graph_layering_index.layer_to_node_ids_.erase(layer_to_nodes_hit); + } + } + } +} + +} // namespace onnxruntime + +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) diff --git a/onnxruntime/core/framework/layering_annotations.h b/onnxruntime/core/framework/layering_annotations.h new file mode 100644 index 0000000000000..5d58e9ace2471 --- /dev/null +++ b/onnxruntime/core/framework/layering_annotations.h @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + +#include "core/common/inlined_containers.h" +#include "core/common/status.h" +#include "core/graph/basic_types.h" +#include "core/common/logging/logging.h" +#include "gsl/gsl" +#include +#include +#include +#include + +struct OrtEpDevice; + +namespace onnxruntime { +class ExecutionProviders; +class Graph; + +/// +/// Annotation extracted from kOrtSessionOptionsLayerAssignmentSettings session configuration option. +/// +struct LayerAnnotation { + std::string device; + std::string annotation; + bool prefix_match; +}; + +/// +/// This struct is a container for layering rules extracted from the kOrtSessionOptionsLayerAssignmentSettings +/// session configuration option. +/// +struct LayeringRules { + std::vector rules; + /// + /// Parses the layering rules from the given configuration string. + /// The configuration string is in the following format.: + /// 'cpu(L1,L2); gpu(L3,=L4)' where cpu or gpu denote the target EP. + /// L1, L2, L3 are annotations that can be matched to node annotations in the graph. The '=' prefix denotes + /// exact match. The position of the annotation (L1, L2, L3) in the list denotes its priority in matching (left to right). + /// However, the prefix annotations will always have higher priority than the exact match annotations regardless + /// of their position in the list. In the above example, L1 has the highest priority, followed by L2, + /// then L3 and finally L4. The rules are separated by ';' and there can be multiple rules for different EPs. + /// + /// The configuration string to parse. + /// Output parameter where the parsed rules will be stored. + /// Status indicating success or failure (e.g. due to format errors). + static common::Status FromConfigString(const std::string& config_value, LayeringRules& rules); +}; + +/// +/// This class matches node annotations against layering rules. +/// +class LayeringRuleMatcher { + public: + explicit LayeringRuleMatcher(const LayeringRules& rules); + + /// + /// The method returns the index of the best matching rule for the given annotation + /// if it exists + /// + /// annotation retrieved from protobuf node metadata + /// index of the matching LayeringRule if it exists + std::optional Match(const std::string& node_annotation) const; + + private: + struct TrieNode { + InlinedHashMap> children; + std::optional rule_index; + }; + + TrieNode root_; + InlinedHashMap exact_match_rules_; + + void AddExactRule(const std::string& annotation, size_t index); + + void AddPrefixRule(const std::string& annotation, size_t index); + + void UpdateBestMatch(std::optional& current_best, size_t candidate) const; +}; + +namespace EpLayeringMatcher { +/// +/// Matches a list of available OrtEpDevices against the device string specified in the LayerAnnotation. +/// Returns the EP Type string of the first device that matches the rule. +/// +/// The list of available EP devices. +/// The rule containing the device designator. +/// Optional containing the matched EP type, nullopt otherwise. +std::optional Match(gsl::span ep_devices, + const LayerAnnotation& rule); + +/// +/// Matches a collection of ExecutionProviders against the device string specified in the LayerAnnotation. +/// Returns the EP Type string of the first provider that matches the rule. +/// +/// The collection of available Execution Providers. +/// The rule containing the device designator. +/// Optional containing the matched EP type, nullopt otherwise. +std::optional Match(const ExecutionProviders& providers, const LayerAnnotation& rule); +} // namespace EpLayeringMatcher + +// This class contains indexing information about the entire graph +// per sub-graph info is stored in graph_index_ +class LayeringIndex { + public: + // mapping of EP name/type to a set of LayeringRule indices mapped to that EP. + using EpNameToLayeringIndices = InlinedHashMap>; + // mapping of LayeringRule index to EP name/type, reverse of the above + using LayeringIndexToEpName = InlinedHashMap; + + /// + /// Creates a fully initialized LayeringIndex. + /// + /// The graph to traverse and index. + /// Pre-populated mapping of EP names to their applicable rule indices. + /// Pre-populated mapping of rule indices to EP names. + /// Matcher to resolve node annotations to rule indices. + static LayeringIndex Create(const Graph& graph, + EpNameToLayeringIndices ep_map, + LayeringIndexToEpName rule_map, + LayeringRules layering_rules); + + /// + /// Factory method that creates a LayeringIndex by parsing configuration, matching rules against + /// available devices/providers, and indexing the graph. + /// + /// The graph to index. + /// The configuration string containing layering rules. + /// Available OrtEpDevices to match rules against. + /// Available ExecutionProviders to match rules against (fallback). + /// Logger for reporting information/errors. + /// Output parameter for the created LayeringIndex. Returns no index if + /// no valid layering rules discovered. + /// Status indicating success or failure. + static Status Create(const Graph& graph, + const std::string& config_string, + gsl::span ep_devices, + const ExecutionProviders& ep_providers, + const logging::Logger& logger, + std::optional& layering_index); + + // Returns the Layering Rule indices mapped to the EP if any + std::optional>> + GetLayeringRulesForThisEp(const std::string& ep_type) const; + + // Returns the parsed layering rules + const LayeringRules& GetRules() const noexcept { return rules_; } + + // This function returns an index for the Layering rule the node is assigned to if any + std::optional GetNodeAssignment(const Graph& graph, NodeIndex node_id) const; + + // This is used when an EP fails to claim a node during partitioning so we make it + // available for other EPs + void MakeNodeUnassigned(const Graph& graph, NodeIndex node_id); + /// + /// Updates the layering index for a specific set of nodes in a graph. + /// This checks if the nodes have annotations, and if so, matches them against the rules + /// and updates the assignment. + /// + /// The graph containing the nodes. + /// Indices of nodes to check and update. + void Update(const Graph& graph, gsl::span nodes); + + private: + LayeringRules rules_; + LayeringRuleMatcher matcher_; + // These stay constant + EpNameToLayeringIndices ep_name_to_layering_indices_; + LayeringIndexToEpName layering_index_to_ep_name_; + + using SetOfNodes = InlinedHashSet; + using LayerIndexToNodes = InlinedHashMap; + using NodeIndexToLayeringIndex = InlinedHashMap; + + /// + /// This struct contains the result of layering assignment for a graph. + /// The struct first reflects pre-assignment according to the configuration. + /// However, as we partition the graph, some nodes may be moved to unassigned sections + /// to make them available to subsequent partitioning passes. + /// + struct GraphLayeringIndex { + // Node to layering idx assignment map 1:1 + // If the node is not in this map, it is unassigned + NodeIndexToLayeringIndex node_to_layering_index_; + // This map contains mapping of LayeringRule index to the list of node ids + // Reverse from the above 1:M + LayerIndexToNodes layer_to_node_ids_; + }; + + LayeringIndex(LayeringRules layering_rules, EpNameToLayeringIndices ep_name_to_layering_indices, LayeringIndexToEpName layering_index_to_ep_name) + : rules_(std::move(layering_rules)), + matcher_(rules_), + ep_name_to_layering_indices_(std::move(ep_name_to_layering_indices)), + layering_index_to_ep_name_(std::move(layering_index_to_ep_name)) {} + + // Graph and sub-graphs mapping to their indices + InlinedHashMap graph_index_; + + void ProcessGraph(const Graph& graph, std::optional parent_layer_id); +}; + +} // namespace onnxruntime + +#else +namespace onnxruntime { +class LayeringIndex; +} +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) diff --git a/onnxruntime/core/framework/resource_accountant.cc b/onnxruntime/core/framework/resource_accountant.cc index 0665cc1951e60..68610ebb4be17 100644 --- a/onnxruntime/core/framework/resource_accountant.cc +++ b/onnxruntime/core/framework/resource_accountant.cc @@ -11,24 +11,31 @@ #include "core/framework/config_options.h" #include "core/framework/murmurhash3.h" +#include "core/framework/tensorprotoutils.h" #include "core/graph/constants.h" #include "core/graph/graph.h" #include "core/session/onnxruntime_session_options_config_keys.h" #include +#include namespace onnxruntime { // Use this accountant if your resource can be counted with size_t type -class SizeTAccountant : public IResourceAccountant { +// This accountant uses NodeAllocationStats to compute resource consumption per node +// which can be collected and saved to a file OR loaded from a file and used for partitioning. +// This is currently used for CUDA EP. +class SizeBasedStatsAccountant : public IResourceAccountant { public: - SizeTAccountant() = default; - ~SizeTAccountant() = default; + SizeBasedStatsAccountant() = default; + ~SizeBasedStatsAccountant() = default; - SizeTAccountant(size_t threshold, InlinedHashMap&& node_stats) + SizeBasedStatsAccountant(size_t threshold, InlinedHashMap&& node_stats) : IResourceAccountant(threshold), node_stats_(std::move(node_stats)) {} - explicit SizeTAccountant(InlinedHashMap&& node_stats) + explicit SizeBasedStatsAccountant(size_t threshold) : IResourceAccountant(threshold) {} + + explicit SizeBasedStatsAccountant(InlinedHashMap&& node_stats) : IResourceAccountant(), node_stats_(std::move(node_stats)) {} ResourceCount GetConsumedAmount() const noexcept override { @@ -46,20 +53,99 @@ class SizeTAccountant : public IResourceAccountant { } } - ResourceCount ComputeResourceCount(const Node& node) const override { - const auto node_name = MakeUniqueNodeName(node); - auto hit = node_stats_.find(node_name); - if (hit != node_stats_.end()) { - const auto& stats = hit->second; - return stats.input_sizes + stats.initializers_sizes + - stats.total_dynamic_sizes + stats.total_temp_allocations; + ResourceCount ComputeResourceCount(const Node& node) override { + if (node_stats_) { + const auto node_name = MakeUniqueNodeName(node); + auto hit = node_stats_->find(node_name); + if (hit != node_stats_->end()) { + const auto& stats = hit->second; + return stats.input_sizes + stats.initializers_sizes + + stats.total_dynamic_sizes + stats.total_temp_allocations; + } + return static_cast(0U); + } else { + const auto* graph = node.GetContainingGraph(); + if (!graph) return static_cast(0); + + SafeInt total_size = 0; + for (const auto* input_def : node.InputDefs()) { + if (!input_def->Exists()) continue; + + const auto& name = input_def->Name(); + constexpr bool check_outer_scope = true; + const auto* tensor_proto = graph->GetInitializer(name, check_outer_scope); + + if (tensor_proto) { + // Skip if already committed from a previous partitioning iteration + if (committed_weights_.count(name) > 0) { + continue; + } + + // Skip if already pending from another node in this GetCapability pass + if (pending_weights_.count(name) > 0) { + continue; + } + + size_t size = 0; + auto status = utils::GetSizeInBytesFromTensorProto<0>(*tensor_proto, &size); + + if (status.IsOK()) { + total_size += size; + pending_weights_.insert(name); + pending_weights_by_node_[node.Index()].insert(name); + } + } + } + + // Account for intermediate output tensors when shape info is available. + // GetSizeInBytesFromTensorTypeProto will only succeed when all dims are known + // (static shape) and a valid element type is present, so dynamic outputs are + // naturally skipped. + SafeInt output_size = 0; + for (const auto* output_def : node.OutputDefs()) { + if (!output_def->Exists() || !output_def->HasTensorOrScalarShape()) continue; + const auto* type_proto = output_def->TypeAsProto(); + if (!type_proto || !utils::HasTensorType(*type_proto)) continue; + + size_t size = 0; + if (utils::GetSizeInBytesFromTensorTypeProto<0>(type_proto->tensor_type(), &size).IsOK()) { + output_size += size; + } + } + + // Apply a safety multiplier for workspace/temp allocations we can't see + constexpr size_t kAdHocSafetyMultiplierPercent = 150; // 1.5x + SafeInt estimated = total_size + output_size; + return static_cast(estimated * kAdHocSafetyMultiplierPercent / 100); + } + } + + void ResetPendingWeights() override { + pending_weights_.clear(); + pending_weights_by_node_.clear(); + } + + void CommitWeightsForNode(NodeIndex node_index) override { + auto it = pending_weights_by_node_.find(node_index); + if (it != pending_weights_by_node_.end()) { + for (const auto& name : it->second) { + pending_weights_.erase(name); + } + committed_weights_.insert(it->second.begin(), it->second.end()); + pending_weights_by_node_.erase(it); } - return static_cast(0U); } private: size_t consumed_amount_ = 0; - InlinedHashMap node_stats_; + std::optional> node_stats_; + // Weights committed from previous partitioning iterations. + // These persist across GetCapability passes. + InlinedHashSet committed_weights_; + // Flat set of all pending weight names for O(1) membership checks. + InlinedHashSet pending_weights_; + // Same pending weights keyed by node index, used by CommitWeightsForNode. + InlinedHashMap> pending_weights_by_node_; }; struct NodeStatsRecorder::Impl { @@ -155,10 +241,11 @@ static Status LoadNodeAllocationStats( return Status::OK(); } -Status NodeStatsRecorder::CreateAccountants( +Status CreateAccountants( const ConfigOptions& config_options, const std::filesystem::path& model_path, std::optional& acc_map) { + std::optional result; // Check if CUDA partitioning settings are provided const std::string resource_partitioning_settings = config_options.GetConfigOrDefault( kOrtSessionOptionsResourceCudaPartitioningSettings, ""); @@ -166,29 +253,34 @@ Status NodeStatsRecorder::CreateAccountants( if (!resource_partitioning_settings.empty()) { auto splits = utils::SplitString(resource_partitioning_settings, ",", true); if (splits.size() == 2) { - if (splits[1].empty()) { - return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid resource partitioning settings"); - } - - InlinedHashMap loaded_stats; - ORT_RETURN_IF_ERROR(LoadNodeAllocationStats(model_path, splits[1], loaded_stats)); - - std::optional result; auto& map = result.emplace(); + std::optional cuda_memory_limit; if (!splits[0].empty()) { - size_t cuda_memory_limit = 0; - ORT_RETURN_IF_ERROR(ParseStringWithClassicLocale(std::string{splits[0]}, cuda_memory_limit)); - cuda_memory_limit = SafeInt(cuda_memory_limit) * 1024; // to bytes + cuda_memory_limit.emplace(0U); + ORT_RETURN_IF_ERROR(ParseStringWithClassicLocale(std::string{splits[0]}, *cuda_memory_limit)); + cuda_memory_limit = SafeInt(*cuda_memory_limit) * 1024; // to bytes + } + + std::optional> loaded_stats; + if (!splits[1].empty()) { + loaded_stats.emplace(); + ORT_RETURN_IF_ERROR(LoadNodeAllocationStats(model_path, splits[1], *loaded_stats)); + } + + if (cuda_memory_limit && loaded_stats) { map.insert_or_assign(kCudaExecutionProvider, - std::make_unique(cuda_memory_limit, - std::move(loaded_stats))); - } else { + std::make_unique(*cuda_memory_limit, + std::move(*loaded_stats))); + } else if (cuda_memory_limit) { map.insert_or_assign(kCudaExecutionProvider, - std::make_unique(std::move(loaded_stats))); + std::make_unique(*cuda_memory_limit)); + } else if (loaded_stats) { + map.insert_or_assign(kCudaExecutionProvider, + std::make_unique(std::move(*loaded_stats))); + } else { + map.insert_or_assign(kCudaExecutionProvider, std::make_unique()); } - - acc_map = std::move(result); } else { return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid format for: ", kOrtSessionOptionsResourceCudaPartitioningSettings, @@ -196,6 +288,7 @@ Status NodeStatsRecorder::CreateAccountants( } } + acc_map = std::move(result); return Status::OK(); } diff --git a/onnxruntime/core/framework/tensorprotoutils.cc b/onnxruntime/core/framework/tensorprotoutils.cc index bee7f048b7c6e..74fbe4d24de96 100644 --- a/onnxruntime/core/framework/tensorprotoutils.cc +++ b/onnxruntime/core/framework/tensorprotoutils.cc @@ -2531,5 +2531,18 @@ Status UnpackInitializerData(const ONNX_NAMESPACE::TensorProto& initializer, std return UnpackInitializerData(initializer, std::filesystem::path(), unpacked_tensor); } +std::optional GetNodeProtoLayeringAnnotation(const ONNX_NAMESPACE::NodeProto& node_proto) { + std::optional result; + for (const auto& prop : node_proto.metadata_props()) { + if (prop.key() == kNodeProtoLayerAnnotation) { + if (!prop.value().empty()) { + result = prop.value(); + break; + } + } + } + return result; +} + } // namespace utils } // namespace onnxruntime diff --git a/onnxruntime/core/framework/tensorprotoutils.h b/onnxruntime/core/framework/tensorprotoutils.h index e7649c072416c..8b22e8d6d1c89 100644 --- a/onnxruntime/core/framework/tensorprotoutils.h +++ b/onnxruntime/core/framework/tensorprotoutils.h @@ -671,5 +671,15 @@ common::Status UnpackInitializerData(const ONNX_NAMESPACE::TensorProto& initiali */ common::Status UnpackInitializerData(const ONNX_NAMESPACE::TensorProto& initializer, std::vector& unpacked_tensor); + +constexpr const char* kNodeProtoLayerAnnotation = "layer_ann"; + +/** + * This function examines the given node proto and looks into its metadata_props. + * It returns the first non-empty value found for the key kNodeProtoLayerAnnotation. + * A node is expected to have only one such annotation. + * If no non-empty annotation is found, std::nullopt is returned. + */ +std::optional GetNodeProtoLayeringAnnotation(const ONNX_NAMESPACE::NodeProto& node_proto); } // namespace utils } // namespace onnxruntime diff --git a/onnxruntime/core/graph/graph.cc b/onnxruntime/core/graph/graph.cc index 3599edbfcd357..e7da5a16930c6 100644 --- a/onnxruntime/core/graph/graph.cc +++ b/onnxruntime/core/graph/graph.cc @@ -3935,6 +3935,20 @@ Status Graph::RemovedUnusedInitializersOrtFormat() { auto result = ForThisAndAllSubgraphs(all_subgraphs, cleanup_func); return result; } + +Status Graph::RemoveAllLayeringAnnotations() { + std::vector all_subgraphs; + FindAllSubgraphs(all_subgraphs); + auto cleanup_func = [](Graph& graph) { + for (auto& node : graph.Nodes()) { + node.ClearLayeringAnnotation(); + } + return Status::OK(); + }; + + return ForThisAndAllSubgraphs(all_subgraphs, cleanup_func); +} + #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) const std::string& Graph::Name() const noexcept { @@ -4371,6 +4385,13 @@ Node& Graph::AddNode(const Node& other) { &other.GetAttributes(), other.Domain()); + // Preserve layering annotation from the source node so that graph transformers + // that reconstruct nodes (or function inlining) retain the EP assignment hint. + const auto& annotation = other.GetLayeringAnnotation(); + if (!annotation.empty()) { + new_node.SetLayeringAnnotation(annotation); + } + return new_node; } @@ -4396,6 +4417,13 @@ Node& Graph::AddNode(const NodeProto& node_proto, &attributes, node_proto.domain()); +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + auto maybe_annotation = utils::GetNodeProtoLayeringAnnotation(node_proto); + if (maybe_annotation) { + new_node.SetLayeringAnnotation(std::move(*maybe_annotation)); + } +#endif // + // Perf optimization: temporarily set NodeProto in Node so we don't need to call Node::ToProto prior to // calling onnx::check_node // NOTE: We don't handle a node with kOnnxDomainAlias. The entry in schema_registry_ uses kOnnxDomain, @@ -4630,6 +4658,38 @@ Node& Graph::AddNode(const std::string& name, return *node; } +Node& Graph::AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + gsl::span input_args, + gsl::span output_args, + const Node& annotation_source, + const NodeAttributes* attributes, + const std::string& domain) { + auto& new_node = AddNode(name, op_type, description, input_args, output_args, attributes, domain); + const auto& annotation = annotation_source.GetLayeringAnnotation(); + if (!annotation.empty()) { + new_node.SetLayeringAnnotation(annotation); + } + return new_node; +} + +Node& Graph::AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + gsl::span input_args, + gsl::span output_args, + const Node& annotation_source, + NodeAttributes&& attributes, + const std::string& domain) { + auto& new_node = AddNode(name, op_type, description, input_args, output_args, std::move(attributes), domain); + const auto& annotation = annotation_source.GetLayeringAnnotation(); + if (!annotation.empty()) { + new_node.SetLayeringAnnotation(annotation); + } + return new_node; +} + bool Graph::RemoveNode(NodeIndex p_index) { auto node = GetNode(p_index); if (nullptr == node) { @@ -6074,7 +6134,8 @@ Status Graph::InlineIfSubgraph(bool condition_value, Node& if_node, const loggin return Status::OK(); } -Status Graph::InlineFunctionProto(const ONNX_NAMESPACE::FunctionProto& func_to_inline) { +Status Graph::InlineFunctionProto(const ONNX_NAMESPACE::FunctionProto& func_to_inline, + const std::string& parent_annotation) { auto to_node_arg = [this](const std::string& name) { return &this->GetOrCreateNodeArg(name, nullptr); }; @@ -6109,28 +6170,31 @@ Status Graph::InlineFunctionProto(const ONNX_NAMESPACE::FunctionProto& func_to_i for (const auto& node_attr : inlined_node->attribute()) { new_attr_map.insert_or_assign(node_attr.name(), node_attr); } - ORT_IGNORE_RETURN_VALUE(AddNode(inlined_node->name(), inlined_node->op_type(), - inlined_node->doc_string(), inputs, outputs, - &new_attr_map, inlined_node->domain())); + auto& new_node = AddNode(inlined_node->name(), inlined_node->op_type(), + inlined_node->doc_string(), inputs, outputs, + &new_attr_map, inlined_node->domain()); + + // Nodes that come from function_proto currently can not have any annotations. + // So we set it to parent. + if (!parent_annotation.empty()) { + new_node.SetLayeringAnnotation(parent_annotation); + } } return Status::OK(); } Status Graph::InlineFunction(Node& callnode) { - // Remove output edges. Requirement for RemoveNode() below. - auto output_edges = callnode.GetRelationships().output_edges; // copy so RemoveEdge doesn't invalidate iterator - for (const auto& output_edge : output_edges) { - RemoveEdge(callnode.Index(), output_edge.GetNode().Index(), output_edge.GetSrcArgIndex(), - output_edge.GetDstArgIndex()); - } - // create a uniq_identifier to append to every node name and intermediate input\outputs // to make sure there are no unintended duplicates std::string base_uniq_identifier{"_inlfunc_"}; base_uniq_identifier.append(callnode.OpType()); const auto uniq_identifier = GenerateNodeName(base_uniq_identifier); + // Capture the parent function node's layering annotation before inlining. + // Inlined nodes that don't already have their own annotation will inherit this. + const std::string parent_annotation = callnode.GetLayeringAnnotation(); + // Replace a (function-call) node by an inlined graph. if (!callnode.GetFunctionBody()) { // This is the normal use-case: inlining a FunctionProto (representing @@ -6142,7 +6206,7 @@ Status Graph::InlineFunction(Node& callnode) { function_utils::Specialize(inlined_fp, callnode, uniq_identifier); // In this case, global Resolve() will take care of everything. - ORT_RETURN_IF_ERROR(InlineFunctionProto(inlined_fp)); + ORT_RETURN_IF_ERROR(InlineFunctionProto(inlined_fp, parent_annotation)); } else { // Uncommon scenario. Inlining a node representing a fused sub-graph. // TODO: Unclear that this feature is needed. Can this be removed? @@ -6161,11 +6225,18 @@ Status Graph::InlineFunction(Node& callnode) { outputs.push_back(&n_output); } - AddNode(subgraph_node.Name() + uniq_identifier, subgraph_node.OpType(), subgraph_node.Description(), - inputs, - outputs, - &subgraph_node.GetAttributes(), - subgraph_node.Domain()); + auto& new_node = AddNode(subgraph_node.Name() + uniq_identifier, subgraph_node.OpType(), + subgraph_node.Description(), + inputs, + outputs, + &subgraph_node.GetAttributes(), + subgraph_node.Domain()); + if (!subgraph_node.GetLayeringAnnotation().empty()) { + new_node.SetLayeringAnnotation(subgraph_node.GetLayeringAnnotation()); + } else if (!parent_annotation.empty()) { + // If the subgraph node doesn't have its own annotation, use the parent function node's annotation. + new_node.SetLayeringAnnotation(parent_annotation); + } } } @@ -6192,9 +6263,15 @@ Status Graph::InlineFunction(Node& callnode) { } } - RemoveNode(callnode.Index()); + // Requirement for RemoveNode() below. + // copy so RemoveEdge doesn't invalidate iterator + auto output_edges = callnode.GetRelationships().output_edges; + for (const auto& output_edge : output_edges) { + RemoveEdge(callnode.Index(), output_edge.GetNode().Index(), output_edge.GetSrcArgIndex(), + output_edge.GetDstArgIndex()); + } - // std::cout << "Graph after inlining\n\n" << *this << std::endl << std::flush; + RemoveNode(callnode.Index()); return Status::OK(); } diff --git a/onnxruntime/core/graph/graph_utils.cc b/onnxruntime/core/graph/graph_utils.cc index 0480263befdd1..85de654581161 100644 --- a/onnxruntime/core/graph/graph_utils.cc +++ b/onnxruntime/core/graph/graph_utils.cc @@ -32,6 +32,154 @@ static int GetIndexFromName(const Node& node, const std::string& name, bool is_i return static_cast(index); } +Status CreateFilteredIndexedGraph(gsl::span nodes, const Graph& graph, + std::unique_ptr& result) { + // Following data structures help determine the final inputs/outputs of the subgraph. + // Note: The 'subgraph' here refers to a graph that contains a subset of nodes in the 'src_graph'. + + // Pre-pass: Identify all outputs produced by nodes within the subgraph. + // This allows O(1) checks to determine if an input is internal or from the boundary. + InlinedHashSet node_set; + InlinedHashSet internal_outputs; + for (size_t i = 0, lim = nodes.size(); i < lim; i++) { + const auto& node = *nodes[i]; + node_set.insert(node.Index()); + for (const auto& output : node.OutputDefs()) { + internal_outputs.insert(output); + } + } + + // Source graph output names + InlinedHashSet graph_output_names; + for (const auto* output_arg : graph.GetOutputs()) { + graph_output_names.insert(output_arg->Name()); + } + + // These maps store the inputs and outputs of the subgraph. + // Value is order index to maintain deterministic order. + InlinedHashMap subgraph_inputs, subgraph_outputs; + + int input_order = 0; + int output_order = 0; + + std::unique_ptr indexed_sub_graph = std::make_unique(); + InlinedVector initializers; + + // Add nodes and identify boundary inputs/outputs + for (size_t i = 0, lim = nodes.size(); i < lim; i++) { + const auto& node = *nodes[i]; + indexed_sub_graph->nodes.push_back(node.Index()); + + // Process Inputs: If an input is not produced internally, it's a subgraph input. + auto process_inputs = [&](gsl::span inputs) { + for (const auto& input : inputs) { + if (!input->Exists()) continue; + + const auto* tensor_proto = graph.GetConstantInitializer(input->Name(), true); + if (tensor_proto != nullptr) { + initializers.push_back(input->Name()); + continue; + } + + // If not produced by this subgraph, it's a boundary input + if (internal_outputs.count(input) == 0) { + // Use insert to keep the first occurrence's order + auto emplace_result = subgraph_inputs.emplace(input, input_order); + if (emplace_result.second) { + ++input_order; + } + } + } + }; + + process_inputs(gsl::make_span(node.InputDefs().data(), node.InputDefs().size())); + process_inputs(gsl::make_span(node.ImplicitInputDefs().data(), node.ImplicitInputDefs().size())); + + // Process Outputs: If an output is graph output OR consumed externally, it's a subgraph output. + for (const auto& output : node.OutputDefs()) { + if (!output->Exists()) continue; + + bool is_boundary_output = false; + + // 1. Is it a graph output? + if (graph_output_names.count(output->Name()) > 0) { + is_boundary_output = true; + } else { + // 2. Is it consumed by any node outside the subgraph? + for (auto it = node.OutputEdgesBegin(), end = node.OutputEdgesEnd(); it != end; ++it) { + // Check if the edge uses this specific output + if (it->GetSrcArgIndex() < static_cast(node.OutputDefs().size()) && + node.OutputDefs()[it->GetSrcArgIndex()] == output) { + if (node_set.count(it->GetNode().Index()) == 0) { + is_boundary_output = true; + break; + } + } + } + } + + if (is_boundary_output) { + subgraph_outputs.insert({output, output_order++}); + } + } + } + + std::multimap inputs, outputs; + + // Get the input order of the original graph + InlinedHashMap original_inputs; + int order = 0; + for (const auto* input : graph.GetInputs()) { + original_inputs[input] = order++; + } + + // input order needs to be consistent with original graph's input order + for (const auto& [node_arg, subgraph_input_order] : subgraph_inputs) { + const auto original_input_it = original_inputs.find(node_arg); + + if (original_input_it != original_inputs.end()) { + inputs.emplace( + original_input_it->second, // input order from original graph + node_arg); + } else { + inputs.emplace( + subgraph_input_order, // input order from subgraph + node_arg); + } + } + + // Sort outputs by the order they were added + for (const auto& [node_arg, subgraph_output_order] : subgraph_outputs) { + outputs.emplace(subgraph_output_order, node_arg); + } + + std::unique_ptr meta_def = std::make_unique(); + meta_def->name = "sub_graph"; + meta_def->since_version = 1; + + // Assign inputs and outputs to subgraph's meta_def + for (const auto& input : inputs) { + if (input.second->Exists()) { + meta_def->inputs.push_back(input.second->Name()); + } + } + + for (const auto& initializer : initializers) { + meta_def->constant_initializers.push_back(initializer); + } + + for (const auto& output : outputs) { + if (output.second->Exists()) { + meta_def->outputs.push_back(output.second->Name()); + } + } + + indexed_sub_graph->SetMetaDef(std::move(meta_def)); + result = std::move(indexed_sub_graph); + + return Status::OK(); +} + #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) #if !defined(ORT_MINIMAL_BUILD) @@ -1010,6 +1158,5 @@ NodeArg& CreateNodeArg(Graph& graph, const NodeArg& base_arg) { } #endif // !defined(ORT_MINIMAL_BUILD) - } // namespace graph_utils } // namespace onnxruntime diff --git a/onnxruntime/core/graph/graph_utils.h b/onnxruntime/core/graph/graph_utils.h index 256a6fc81495d..2106da1a96327 100644 --- a/onnxruntime/core/graph/graph_utils.h +++ b/onnxruntime/core/graph/graph_utils.h @@ -475,5 +475,21 @@ NodeArg& CreateNodeArg(Graph& graph, const NodeArg& base_arg); #endif // !defined(ORT_MINIMAL_BUILD) +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + +/// +/// This function creates an indexed subgraph from a collection of nodes +/// using the graph instance. The IndexedSubgraph can then be used to create +/// a filtered GraphViewer instance that only contains the nodes in the collection. +/// +/// +/// +/// +/// +Status CreateFilteredIndexedGraph(gsl::span nodes, const Graph& graph, + std::unique_ptr& indexed_subgraph); + +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + } // namespace graph_utils } // namespace onnxruntime diff --git a/onnxruntime/core/optimizer/dq_matmulnbits_fusion.cc b/onnxruntime/core/optimizer/dq_matmulnbits_fusion.cc index f9ae13808cf2c..f3956d5e9e0f3 100644 --- a/onnxruntime/core/optimizer/dq_matmulnbits_fusion.cc +++ b/onnxruntime/core/optimizer/dq_matmulnbits_fusion.cc @@ -605,7 +605,7 @@ void ApplyReshapeTransposeFusions( graph.GenerateNodeName("DQFusedMatMulNBits"), "MatMulNBits", "Fused from DQ+Reshape+Transpose+MatMul", - mnb_inputs, mnb_outputs, &mnb_attrs, kMSDomain); + mnb_inputs, mnb_outputs, *mm_node, &mnb_attrs, kMSDomain); mnb_node.SetExecutionProviderType(mm_node->GetExecutionProviderType()); graph_utils::RemoveNodeOutputEdges(graph, *graph.GetNode(match.matmul_idx)); @@ -784,7 +784,7 @@ void ApplyDirectDQFusions( graph.GenerateNodeName("DirectDQFusedMatMulNBits"), "MatMulNBits", "Fused from direct DQ(axis=0)+MatMul", - mnb_inputs, mnb_outputs, &mnb_attrs, kMSDomain); + mnb_inputs, mnb_outputs, *mm_node, &mnb_attrs, kMSDomain); mnb_node.SetExecutionProviderType(mm_node->GetExecutionProviderType()); graph_utils::RemoveNodeOutputEdges(graph, *graph.GetNode(match.matmul_idx)); diff --git a/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc b/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc index 9e35550e2f845..606e91ce91bbb 100644 --- a/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc +++ b/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc @@ -17,7 +17,7 @@ using namespace ONNX_NAMESPACE; using namespace onnxruntime::common; namespace onnxruntime { // Add a Cast to convert Input from int64 to int32. -static NodeArg* CastToInt32(Graph& graph, NodeArg* input, ProviderType provider_type) { +static NodeArg* CastToInt32(Graph& graph, NodeArg* input, const Node& source_node) { auto data_type = input->TypeAsProto()->tensor_type().elem_type(); if (data_type == ONNX_NAMESPACE::TensorProto_DataType_INT32) { return input; @@ -36,13 +36,13 @@ static NodeArg* CastToInt32(Graph& graph, NodeArg* input, ProviderType provider_ "Cast Input from int64 to int32", std::array{input}, std::array{&cast32}, + source_node, nullptr, kOnnxDomain); // Add attribute: "to" = 6 node.AddAttribute("to", int64_t{ONNX_NAMESPACE::TensorProto_DataType_INT32}); - - node.SetExecutionProviderType(provider_type); + node.SetExecutionProviderType(source_node.GetExecutionProviderType()); return &cast32; } @@ -487,9 +487,9 @@ static void CreateEmbedLayernormNode(Graph& graph, NodeArg* segment_embedding, Node& layer_norm_node) { // Cast input_ids and segment_ids to int32 if needed. - input_ids = CastToInt32(graph, input_ids, layer_norm_node.GetExecutionProviderType()); + input_ids = CastToInt32(graph, input_ids, layer_norm_node); if (segment_ids != nullptr && segment_embedding != nullptr) { - segment_ids = CastToInt32(graph, segment_ids, layer_norm_node.GetExecutionProviderType()); + segment_ids = CastToInt32(graph, segment_ids, layer_norm_node); } NodeArg place_holder("", nullptr); @@ -514,7 +514,7 @@ static void CreateEmbedLayernormNode(Graph& graph, "fused EmbedLayerNorm subgraphs ", embed_layer_norm_input_defs, std::array{layer_norm_node.MutableOutputDefs()[0], &mask_index}, - {}, kMSDomain); + layer_norm_node, nullptr, kMSDomain); // Get attribute "epsilon" from "LayerNormalization" node if available. Else, default value // will be used. diff --git a/onnxruntime/core/optimizer/gelu_fusion.cc b/onnxruntime/core/optimizer/gelu_fusion.cc index 641bfbf388623..e2f448bf70734 100644 --- a/onnxruntime/core/optimizer/gelu_fusion.cc +++ b/onnxruntime/core/optimizer/gelu_fusion.cc @@ -178,7 +178,7 @@ Status GeluFusion::ApplyImpl(Graph& graph, bool& modified, int graph_level, cons "Gelu", "fused Gelu subgraphs ", gelu_input_defs, - {}, {}, op_domain); + {}, div, nullptr, op_domain); // Assign provider to this new node. Provider should be same as the provider for old node. gelu_node.SetExecutionProviderType(div.GetExecutionProviderType()); diff --git a/onnxruntime/core/optimizer/gemm_sum_fusion.cc b/onnxruntime/core/optimizer/gemm_sum_fusion.cc index be3c90a822fe2..c84e34a6d0dbe 100644 --- a/onnxruntime/core/optimizer/gemm_sum_fusion.cc +++ b/onnxruntime/core/optimizer/gemm_sum_fusion.cc @@ -41,7 +41,8 @@ Status GemmSumFusion::Apply(Graph& graph, Node& gemm_node, RewriteRuleEffect& mo "Fused Gemm with Sum", new_gemm_input_defs, new_gemm_output_defs, - {}, + gemm_node, + nullptr, gemm_node.Domain()); new_gemm_node.AddAttribute("transA", static_cast(transA)); new_gemm_node.AddAttribute("transB", static_cast(transB)); diff --git a/onnxruntime/core/optimizer/gemm_transpose_fusion.cc b/onnxruntime/core/optimizer/gemm_transpose_fusion.cc index da454b67aecf4..a66ad987cfaef 100644 --- a/onnxruntime/core/optimizer/gemm_transpose_fusion.cc +++ b/onnxruntime/core/optimizer/gemm_transpose_fusion.cc @@ -80,7 +80,8 @@ Status GemmTransposeFusion::Apply(Graph& graph, Node& node, RewriteRuleEffect& m "Fused Gemm with Transpose", new_gemm_input_defs, {}, - {}, + gemm_node, + nullptr, gemm_node.Domain()); new_gemm_node.AddAttribute("transA", static_cast(transA)); new_gemm_node.AddAttribute("transB", static_cast(transB)); diff --git a/onnxruntime/core/optimizer/layer_norm_fusion.cc b/onnxruntime/core/optimizer/layer_norm_fusion.cc index 3ade3864255ea..c10e070ef8f09 100644 --- a/onnxruntime/core/optimizer/layer_norm_fusion.cc +++ b/onnxruntime/core/optimizer/layer_norm_fusion.cc @@ -474,7 +474,7 @@ Status LayerNormFusion::ApplyImpl(Graph& graph, bool& modified, int graph_level, "LayerNormalization", "fused LayerNorm subgraphs ", layer_norm_input_defs, - {}, {}, kOnnxDomain); + {}, mul_node, nullptr, kOnnxDomain); // Get constant "epsilon" from "Add2" node if available. Else, default value will be used. const ONNX_NAMESPACE::TensorProto* tensor_proto = graph_utils::GetConstantInitializer(graph, add2_node.MutableInputDefs()[1]->Name()); @@ -719,7 +719,7 @@ Status SimplifiedLayerNormFusion::ApplyImpl(Graph& graph, bool& modified, int gr InlinedVector layer_norm_input_defs{x_input, scale}; Node& layer_norm_node = graph.AddNode(graph.GenerateNodeName(mul_node.Name() + "/SimplifiedLayerNormFusion/"), "SimplifiedLayerNormalization", - "fused LayerNorm subgraphs ", layer_norm_input_defs, {}, {}, kOnnxDomain); + "fused LayerNorm subgraphs ", layer_norm_input_defs, {}, mul_node, nullptr, kOnnxDomain); // Get constant "epsilon" from "Add" node if available. Else, default value will be used. const ONNX_NAMESPACE::TensorProto* tensor_proto = diff --git a/onnxruntime/core/optimizer/matmul_add_fusion.cc b/onnxruntime/core/optimizer/matmul_add_fusion.cc index 5db61877811aa..f567609c979a9 100644 --- a/onnxruntime/core/optimizer/matmul_add_fusion.cc +++ b/onnxruntime/core/optimizer/matmul_add_fusion.cc @@ -7,6 +7,7 @@ #include "core/optimizer/graph_transformer_utils.h" #include "core/optimizer/initializer.h" #include "core/optimizer/matmul_add_fusion.h" +#include "core/optimizer/utils.h" #include #include @@ -204,7 +205,8 @@ Status MatMulAddFusion::ApplyImpl(Graph& graph, bool& modified, int graph_level, NodeArg* new_arg = &graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(name + "_reshape_arg"), &new_arg_type); Node& reshape_node = graph.AddNode(graph.GenerateNodeName(name + "_reshape"), "Reshape", "Reshape for " + name, {is_input ? gemm_input_defs[0] : new_arg, shape_arg}, - {is_input ? new_arg : gemm_output_defs[0]}); + {is_input ? new_arg : gemm_output_defs[0]}, + matmul_node); reshape_node.SetExecutionProviderType(matmul_node.GetExecutionProviderType()); return &reshape_node; }; @@ -217,7 +219,8 @@ Status MatMulAddFusion::ApplyImpl(Graph& graph, bool& modified, int graph_level, } Node& gemm_node = graph.AddNode(graph.GenerateNodeName(matmul_node.Name() + "/MatMulAddFusion"), "Gemm", - "fused Matmul and Add", gemm_input_defs, gemm_output_defs); + "fused Matmul and Add", gemm_input_defs, gemm_output_defs, + matmul_node); gemm_node.SetExecutionProviderType(matmul_node.GetExecutionProviderType()); if (need_reshape) { diff --git a/onnxruntime/core/optimizer/matmul_bn_fusion.cc b/onnxruntime/core/optimizer/matmul_bn_fusion.cc index 871571ea64881..be52e26a2901f 100644 --- a/onnxruntime/core/optimizer/matmul_bn_fusion.cc +++ b/onnxruntime/core/optimizer/matmul_bn_fusion.cc @@ -227,6 +227,7 @@ Status MatmulBNFusion::Apply(Graph& graph, Node& matmul_node, RewriteRuleEffect& "Generated from Matmul BatchNormalization fusion", {matmul_node.MutableInputDefs()[0], &new_gemm_b_node_arg, &new_gemm_bias_node_arg}, matmul_node.MutableOutputDefs(), + matmul_node, nullptr, kOnnxDomain); diff --git a/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc b/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc index 9d53e28921784..c79e4142a9ee2 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc @@ -10,6 +10,7 @@ #include "core/graph/graph_utils.h" #include "core/graph/graph_viewer.h" #include "core/optimizer/qdq_transformer/qdq_util.h" +#include "core/optimizer/utils.h" namespace onnxruntime { @@ -53,6 +54,7 @@ Status DuplicateDQForOutputEdge(const graph_utils::GraphEdge& original_dq_output MakeString("Added by ", kTransformerName), dq_inputs, {&new_dq_output_nodearg}, + original_dq_node, &original_dq_node.GetAttributes(), original_dq_node.Domain()); diff --git a/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc b/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc index b8252bc7a75b4..0d732a71b7ed0 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc @@ -194,6 +194,8 @@ Status InsertQDQPairs(Graph& graph, gsl::span insertion } } + optimizer_utils::DuplicateNodeAnnotation(*src_node, q_node); + // Add edge from src to Q node. src_node->MutableOutputDefs()[first_edge.src->arg_idx] = &pre_q_nodearg; graph.AddEdge(src_node->Index(), q_node.Index(), first_edge.src->arg_idx, 0); @@ -221,6 +223,10 @@ Status InsertQDQPairs(Graph& graph, gsl::span insertion &dq_attrs, // attributes qdq_domain); + if (src_node) { + optimizer_utils::DuplicateNodeAnnotation(*src_node, dq_node); + } + ORT_RETURN_IF_NOT(graph.SetOpSchemaFromRegistryForNode(dq_node), "Failed to set op schema for added DQ node."); Node* dst_node = insertion_edge.GetMutableNodeAtEnd(graph, ExtendedGraphEdge::End::Destination); diff --git a/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc b/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc index 5a6eb82c3e6c0..ba3ea09564c17 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc @@ -189,14 +189,14 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_weight_q"), &weight_q_type_proto); Node& weight_q_node = graph.AddNode( graph.GenerateNodeArgName(node.Name() + "_weight_q"), QDQ::QOpName, "Weight Q node", - {node.MutableInputDefs()[1], weight_scale_arg, &weight_zp_arg}, {&weight_q_arg}, nullptr, node.Domain()); + {node.MutableInputDefs()[1], weight_scale_arg, &weight_zp_arg}, {&weight_q_arg}, node, nullptr, node.Domain()); // DQ from int8 to float32. NodeArg& weight_dq_arg = graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_weight_dq"), weight_arg->TypeAsProto()); Node& weight_dq_node = graph.AddNode(graph.GenerateNodeArgName(node.Name() + "_weight_dq"), QDQ::DQOpName, "Weight DQ node", - {&weight_q_arg, weight_scale_arg, &weight_zp_arg}, {&weight_dq_arg}, nullptr, node.Domain()); + {&weight_q_arg, weight_scale_arg, &weight_zp_arg}, {&weight_dq_arg}, node, nullptr, node.Domain()); graph.AddEdge(weight_q_node.Index(), weight_dq_node.Index(), 0, 0); node.MutableInputDefs()[1] = &weight_dq_arg; graph.AddEdge(weight_dq_node.Index(), node.Index(), 0, 1); @@ -211,14 +211,14 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph weight_scale_arg->TypeAsProto()); Node& mul_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_scale"), "Mul", "Bias scale node", - {dq_0.MutableInputDefs()[1], weight_scale_arg}, {&bias_scale_arg}, nullptr, node.Domain()); + {dq_0.MutableInputDefs()[1], weight_scale_arg}, {&bias_scale_arg}, node, nullptr, node.Domain()); // fp_bias / scale. NodeArg& bias_div_arg = graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_bias_div"), bias_arg->TypeAsProto()); Node& div_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_div"), "Div", "Bias div node", - {node.MutableInputDefs()[2], &bias_scale_arg}, {&bias_div_arg}, nullptr, node.Domain()); + {node.MutableInputDefs()[2], &bias_scale_arg}, {&bias_div_arg}, node, nullptr, node.Domain()); graph.AddEdge(mul_node.Index(), div_node.Index(), 0, 1); // Round(fp_bias / scale). @@ -226,7 +226,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_bias_div_round"), bias_arg->TypeAsProto()); Node& round_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_div_round"), "Round", "Bias div round node", - {&bias_div_arg}, {&bias_div_round_arg}, nullptr, node.Domain()); + {&bias_div_arg}, {&bias_div_round_arg}, node, nullptr, node.Domain()); graph.AddEdge(div_node.Index(), round_node.Index(), 0, 0); // Cast(Round(fp_bias / scale)) to int32. @@ -236,7 +236,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph NodeArg& bias_int32_arg = graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_bias_int32"), &bias_int32_type_proto); Node& cast_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_int32"), "Cast", "Bias INT32 node", - {&bias_div_round_arg}, {&bias_int32_arg}, nullptr, node.Domain()); + {&bias_div_round_arg}, {&bias_int32_arg}, node, nullptr, node.Domain()); cast_node.AddAttribute("to", static_cast(ONNX_NAMESPACE::TensorProto_DataType_INT32)); graph.AddEdge(round_node.Index(), cast_node.Index(), 0, 0); @@ -245,7 +245,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_bias_dq"), bias_arg->TypeAsProto()); Node& bias_dq_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_dq"), QDQ::DQOpName, "Bias DQ node", - {&bias_int32_arg, &bias_scale_arg}, {&bias_dq_arg}, nullptr, node.Domain()); + {&bias_int32_arg, &bias_scale_arg}, {&bias_dq_arg}, node, nullptr, node.Domain()); if (!is_per_tensor_scale) { bias_dq_node.AddAttribute("axis", static_cast(0)); } diff --git a/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc b/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc index 9bd91e7916ecb..94fc7f6c03fa1 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc @@ -134,6 +134,7 @@ Status WhereDummyDq::InsertDummyDQ(Node& node, Graph& graph, bool& modified, con "DeQuantizeLinear from WhereDummyDq GraphTransformer", {&dummy_data_arg, &dummy_scale_arg, &dummy_zp_arg}, {&dummy_dq_arg}, + node, nullptr, dq_node->Domain()); diff --git a/onnxruntime/core/optimizer/reshape_fusion.cc b/onnxruntime/core/optimizer/reshape_fusion.cc index 6a2b4295093d8..167952356ff58 100644 --- a/onnxruntime/core/optimizer/reshape_fusion.cc +++ b/onnxruntime/core/optimizer/reshape_fusion.cc @@ -495,7 +495,8 @@ bool ReshapeFusion::FuseContiguousReshapes(Node& reshape, Graph& graph) { NodeArg* shape_arg = &graph_utils::AddInitializerWithOrtValue(graph, shape_initializer_proto); Node& reshape_node = graph.AddNode(graph.GenerateNodeName(name + "_new_reshape"), "Reshape", "Reshape for " + name, {contiguous_reshapes[0].get().MutableInputDefs()[0], shape_arg}, - {contiguous_reshapes.back().get().MutableOutputDefs()[0]}); + {contiguous_reshapes.back().get().MutableOutputDefs()[0]}, + reshape); reshape_node.SetExecutionProviderType(contiguous_reshapes[0].get().GetExecutionProviderType()); graph_utils::FinalizeNodeFusion(graph, contiguous_reshapes, reshape_node); diff --git a/onnxruntime/core/optimizer/slice_concat_to_space_to_depth_fusion.cc b/onnxruntime/core/optimizer/slice_concat_to_space_to_depth_fusion.cc index f72f74e3b4a5c..8caea2c150990 100644 --- a/onnxruntime/core/optimizer/slice_concat_to_space_to_depth_fusion.cc +++ b/onnxruntime/core/optimizer/slice_concat_to_space_to_depth_fusion.cc @@ -492,6 +492,7 @@ bool FuseSliceConcatToSpaceToDepth(Node& concat, Graph& graph, const logging::Lo : "Fused Slice*4 + Concat into SpaceToDepth + channel permutation", {space_to_depth_input}, space_to_depth_outputs, + concat, nullptr, kOnnxDomain); space_to_depth.AddAttribute("blocksize", kBlockSize); @@ -517,6 +518,7 @@ bool FuseSliceConcatToSpaceToDepth(Node& concat, Graph& graph, const logging::Lo "Reorder SpaceToDepth channels to preserve Slice+Concat block order", {space_to_depth.MutableOutputDefs()[0], gather_indices_arg}, {}, + concat, nullptr, kOnnxDomain); gather.AddAttribute("axis", static_cast(kChannelAxis)); diff --git a/onnxruntime/core/optimizer/stft_decomposition.cc b/onnxruntime/core/optimizer/stft_decomposition.cc index 60ab064465f2f..c84e60e64bd2d 100644 --- a/onnxruntime/core/optimizer/stft_decomposition.cc +++ b/onnxruntime/core/optimizer/stft_decomposition.cc @@ -58,27 +58,43 @@ NodeArg* AddShapeInitializer(Graph& graph, const char* name, const int64_t (&sha std::pair AddNode(Graph& graph, const char* op_type, ProviderType execution_provider_type, - gsl::span inputs) { + gsl::span inputs, + const Node* annotation_source = nullptr) { auto def_name = graph.GenerateNodeArgName(op_type); auto node_arg = &graph.GetOrCreateNodeArg(def_name, nullptr); - Node& node = graph.AddNode(graph.GenerateNodeName(op_type), - op_type, - "", - inputs, - {node_arg}); + Node& node = annotation_source + ? graph.AddNode(graph.GenerateNodeName(op_type), + op_type, + "", + inputs, + {node_arg}, + *annotation_source) + : graph.AddNode(graph.GenerateNodeName(op_type), + op_type, + "", + inputs, + {node_arg}); node.SetExecutionProviderType(execution_provider_type); return std::make_pair(&node, node_arg); } std::pair AddNodeCast(Graph& graph, NodeArg* in, - ONNX_NAMESPACE::TensorProto_DataType data_type) { + ONNX_NAMESPACE::TensorProto_DataType data_type, + const Node* annotation_source = nullptr) { auto def_name = graph.GenerateNodeArgName("Cast"); auto node_arg = &graph.GetOrCreateNodeArg(def_name, nullptr); - Node& node = graph.AddNode(graph.GenerateNodeName("Cast"), - "Cast", - "", - {in}, - {node_arg}); + Node& node = annotation_source + ? graph.AddNode(graph.GenerateNodeName("Cast"), + "Cast", + "", + {in}, + {node_arg}, + *annotation_source) + : graph.AddNode(graph.GenerateNodeName("Cast"), + "Cast", + "", + {in}, + {node_arg}); node.AddAttribute("to", static_cast(data_type)); node.SetExecutionProviderType(kCpuExecutionProvider); return std::make_pair(&node, node_arg); @@ -238,7 +254,7 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve Node* reshape_signal_node = nullptr; NodeArg* reshape_output = nullptr; std::tie(reshape_signal_node, reshape_output) = - AddNode(graph, "Reshape", stft.GetExecutionProviderType(), signal_reshaped_inputs); + AddNode(graph, "Reshape", stft.GetExecutionProviderType(), signal_reshaped_inputs, &stft); NodeArg* real_weights_final = real_weights; NodeArg* imag_weights_final = imaginary_weights; @@ -246,11 +262,11 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve // When we are missing a window function if (real_weights_final->TypeAsProto()->tensor_type().elem_type() != data_type) { std::tie(std::ignore, real_weights_final) = - AddNodeCast(graph, real_weights_final, data_type); + AddNodeCast(graph, real_weights_final, data_type, &stft); } if (imag_weights_final->TypeAsProto()->tensor_type().elem_type() != data_type) { std::tie(std::ignore, imag_weights_final) = - AddNodeCast(graph, imag_weights_final, data_type); + AddNodeCast(graph, imag_weights_final, data_type, &stft); } } else { // When we have a window function @@ -261,7 +277,7 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve if (window->TypeAsProto()->tensor_type().elem_type() != GetDataType()) { Node* window_cast_node = nullptr; std::tie(window_cast_node, window_final) = - AddNodeCast(graph, window, GetDataType()); + AddNodeCast(graph, window, GetDataType(), &stft); window_recipient = window_cast_node; } @@ -269,7 +285,7 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve Node* window_reshape_node; NodeArg* window_reshaped = nullptr; std::tie(window_reshape_node, window_reshaped) = - AddNode(graph, "Reshape", kCpuExecutionProvider, window_reshaped_inputs); + AddNode(graph, "Reshape", kCpuExecutionProvider, window_reshaped_inputs, &stft); if (!window_recipient) { window_recipient = window_reshape_node; } @@ -277,17 +293,17 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve NodeArg* scale_real_weights_inputs[] = {real_weights, window_reshaped}; NodeArg* windowed_real_weights_output = nullptr; std::tie(std::ignore, windowed_real_weights_output) = - AddNode(graph, "Mul", kCpuExecutionProvider, scale_real_weights_inputs); + AddNode(graph, "Mul", kCpuExecutionProvider, scale_real_weights_inputs, &stft); NodeArg* scale_imag_weights_inputs[] = {imaginary_weights, window_reshaped}; NodeArg* windowed_imag_weights_output = nullptr; std::tie(std::ignore, windowed_imag_weights_output) = - AddNode(graph, "Mul", kCpuExecutionProvider, scale_imag_weights_inputs); + AddNode(graph, "Mul", kCpuExecutionProvider, scale_imag_weights_inputs, &stft); std::tie(std::ignore, real_weights_final) = - AddNodeCast(graph, windowed_real_weights_output, data_type); + AddNodeCast(graph, windowed_real_weights_output, data_type, &stft); std::tie(std::ignore, imag_weights_final) = - AddNodeCast(graph, windowed_imag_weights_output, data_type); + AddNodeCast(graph, windowed_imag_weights_output, data_type, &stft); } // Add Convolution (reals) @@ -295,7 +311,7 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve Node* real_conv_node = nullptr; NodeArg* real_conv_output = nullptr; std::tie(real_conv_node, real_conv_output) = - AddNode(graph, "Conv", stft.GetExecutionProviderType(), conv_real_inputs); + AddNode(graph, "Conv", stft.GetExecutionProviderType(), conv_real_inputs, &stft); real_conv_node->AddAttribute("strides", std::vector{1, frame_step_value}); // Add Convolution (imaginary) @@ -303,7 +319,7 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve Node* imag_conv_node = nullptr; NodeArg* imag_conv_output = nullptr; std::tie(imag_conv_node, imag_conv_output) = - AddNode(graph, "Conv", stft.GetExecutionProviderType(), conv_imag_inputs); + AddNode(graph, "Conv", stft.GetExecutionProviderType(), conv_imag_inputs, &stft); imag_conv_node->AddAttribute("strides", std::vector{1, frame_step_value}); // Concatenate @@ -311,21 +327,21 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve Node* concat_node = nullptr; NodeArg* concatenated_conv_output = nullptr; std::tie(concat_node, concatenated_conv_output) = - AddNode(graph, "Concat", stft.GetExecutionProviderType(), concatenate_inputs); + AddNode(graph, "Concat", stft.GetExecutionProviderType(), concatenate_inputs, &stft); concat_node->AddAttribute("axis", static_cast(0)); // Unsqueeze Reshape NodeArg* unsqueeze_reshape_inputs[] = {concatenated_conv_output, unsqueezed_shape}; NodeArg* unsqueezed_output = nullptr; std::tie(std::ignore, unsqueezed_output) = - AddNode(graph, "Reshape", stft.GetExecutionProviderType(), unsqueeze_reshape_inputs); + AddNode(graph, "Reshape", stft.GetExecutionProviderType(), unsqueeze_reshape_inputs, &stft); // Transpose NodeArg* transpose_inputs[] = {unsqueezed_output}; Node* transpose_node = nullptr; NodeArg* transpose_output = nullptr; std::tie(transpose_node, transpose_output) = - AddNode(graph, "Transpose", stft.GetExecutionProviderType(), transpose_inputs); + AddNode(graph, "Transpose", stft.GetExecutionProviderType(), transpose_inputs, &stft); transpose_node->AddAttribute("perm", std::vector{1, 3, 2, 0}); signal_recipient = reshape_signal_node; diff --git a/onnxruntime/core/optimizer/transpose_optimization/onnx_transpose_optimization.cc b/onnxruntime/core/optimizer/transpose_optimization/onnx_transpose_optimization.cc index 29b603da56e29..467d0c090070f 100755 --- a/onnxruntime/core/optimizer/transpose_optimization/onnx_transpose_optimization.cc +++ b/onnxruntime/core/optimizer/transpose_optimization/onnx_transpose_optimization.cc @@ -531,6 +531,7 @@ static bool MakeQDQNodeUnit(api::GraphRef& graph, const api::NodeRef& dq_node) { // Add Q auto new_q_node = MakeQuantizeOp(graph, dq_domain, inputs, axis, dq_node.GetAttributeInt("block_size"), dq_node.GetAttributeInt("output_dtype"), dq_node.GetAttributeInt("saturate")); + new_q_node->SetLayeringAnnotation(dq_node.GetLayeringAnnotation()); auto q_node_outputs = new_q_node->Outputs(); // copy value info from the dq input for the type information, and update the shape to match next_node's output @@ -543,6 +544,7 @@ static bool MakeQDQNodeUnit(api::GraphRef& graph, const api::NodeRef& dq_node) { // Add DQ auto new_dq_node = MakeDequantizeOp(graph, dq_domain, inputs, axis, dq_node.GetAttributeInt("block_size")); + new_dq_node->SetLayeringAnnotation(dq_node.GetLayeringAnnotation()); auto dq_node_outputs = new_dq_node->Outputs(); // straight copy of value info as the type and shape are the same as next_node's output @@ -1007,6 +1009,7 @@ static void UnsqueezeInput(OptimizerCtx& ctx, api::NodeRef& node, size_t i, cons // (see Case 2). if (consumers->nodes.size() > 0) { auto squeeze_ptr = MakeSqueezeOrUnsqueeze(ctx.opset, ctx.graph, "Squeeze", value_to_modify, axes); + squeeze_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& squeeze = *squeeze_ptr; std::string_view sq_out = squeeze.Outputs()[0]; ctx.graph.CopyValueInfo(value_to_modify, sq_out); @@ -1075,6 +1078,7 @@ static void UnsqueezeInput(OptimizerCtx& ctx, api::NodeRef& node, size_t i, cons // Case 3: Add an Unsqueeze node. auto unsqueeze_ptr = MakeSqueezeOrUnsqueeze(ctx.opset, ctx.graph, "Unsqueeze", input, axes); + unsqueeze_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& unsqueeze = *unsqueeze_ptr; std::string_view unsq_out = unsqueeze.Outputs()[0]; ctx.graph.CopyValueInfo(input, unsq_out); @@ -1207,6 +1211,7 @@ static void TransposeInputImpl(api::GraphRef& graph, api::NodeRef& node, size_t // Transpose the initializer. If there are existing consumers, add Transpose nodes to them using perm_inv // to counteract the effect. These Transposes will hopefully be optimized out later. auto transpose_inv_ptr = MakeTranspose(graph, constant_to_modify, perm_inv); + transpose_inv_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& transpose_inv = *transpose_inv_ptr; std::string_view transpose_out = transpose_inv.Outputs()[0]; graph.CopyValueInfo(constant_to_modify, transpose_out); @@ -1267,6 +1272,7 @@ static void TransposeInputImpl(api::GraphRef& graph, api::NodeRef& node, size_t // the other Transpose. const std::vector& perm_combined = ComposePerm(*perm2, perm); auto transpose_ptr = MakeTranspose(graph, inp_node->Inputs()[0], perm_combined); + transpose_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& transpose = *transpose_ptr; std::string_view transpose_out = transpose.Outputs()[0]; graph.CopyValueInfo(input, transpose_out); @@ -1301,6 +1307,7 @@ static void TransposeInputImpl(api::GraphRef& graph, api::NodeRef& node, size_t // Case 4: Add a new Transpose op auto transpose_ptr = MakeTranspose(graph, input, perm); + transpose_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& transpose = *transpose_ptr; std::string_view transpose_out = transpose.Outputs()[0]; graph.CopyValueInfo(input, transpose_out); @@ -1376,6 +1383,7 @@ std::string_view TransposeOutput(api::GraphRef& graph, api::NodeRef& node, size_ // X -> Node -> Y, Transpose auto transpose = MakeTranspose(graph, "", perm); + transpose->SetLayeringAnnotation(node.GetLayeringAnnotation()); // X -> Node -> *Y', Transpose -> Y *shape/dtype not set graph.MoveOutput(node, i, *transpose, 0); @@ -1730,6 +1738,7 @@ static bool HandleShape(HandlerArgs& args) { // X -> Shape -> Y, Gather std::vector gather_inputs{"", perm_const}; auto gather_ptr = args.ctx.graph.AddNode("Gather", "Gather", gather_inputs, /*num_outputs*/ 1); + gather_ptr->SetLayeringAnnotation(args.node.GetLayeringAnnotation()); api::NodeRef& gather = *gather_ptr; gather.SetAttributeInt("axis", 0); @@ -1773,6 +1782,7 @@ static void PermuteInput(api::GraphRef& graph, api::NodeRef& node, size_t i, con std::string_view gather_indices_const = AddInitializerInt64(graph, /*shape*/ {rank_int}, perm); std::vector gather_inputs{input_name, gather_indices_const}; auto gather_ptr = graph.AddNode("Gather", "Gather", gather_inputs, /*num_outputs*/ 1); + gather_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& gather = *gather_ptr; std::string_view gather_output = gather.Outputs()[0]; graph.CopyValueInfo(input_name, gather_output); @@ -2221,6 +2231,7 @@ static bool HandleTile(HandlerArgs& args) { std::string_view perm_inv_const = AddInitializerInt64(args.ctx.graph, perm_shape, args.perm_inv); std::vector gather_inputs{repeats_inp, perm_inv_const}; auto gather_node_ptr = args.ctx.graph.AddNode("Gather", "Gather", gather_inputs, /*num_outputs*/ 1); + gather_node_ptr->SetLayeringAnnotation(args.node.GetLayeringAnnotation()); api::NodeRef& gather_node = *gather_node_ptr; std::string_view gather_output = gather_node.Outputs()[0]; args.ctx.graph.CopyValueInfo(repeats_inp, gather_output); @@ -2271,6 +2282,7 @@ static void RemoveCancelingTransposeNodes(HandlerArgs& args) { // despite computing the same value. Use an Identity op instead. std::vector single_empty_input{""}; auto identity_ptr = args.ctx.graph.AddNode("Identity", "Identity", single_empty_input, /*num_outputs*/ 1); + identity_ptr->SetLayeringAnnotation(args.node.GetLayeringAnnotation()); api::NodeRef& identity = *identity_ptr; args.ctx.graph.MoveOutput(args.node, 0, identity, 0); identity.SetInput(0, transpose_input); @@ -2303,6 +2315,7 @@ static bool HandleTransposeImpl(HandlerArgs& args, const std::vector& n // use the same input as the 1st Transpose, move the output from the Reshape to the new Transpose node, // and remove the Reshape node. new_node = args.ctx.graph.AddNode("Transpose", "Transpose", {args.transpose.Inputs()[0]}, 1); + new_node->SetLayeringAnnotation(args.node.GetLayeringAnnotation()); args.ctx.graph.MoveOutput(args.node, 0, *new_node, 0); args.ctx.graph.RemoveNode(args.node); } else { @@ -2973,6 +2986,7 @@ static bool TryFixTransposeMissingDQ(OptimizerCtx& ctx, api::NodeRef& transpose_ // Add Q auto new_q_node = MakeQuantizeOp(ctx.graph, q_domain, inputs, axis, q_node.GetAttributeInt("block_size"), q_node.GetAttributeInt("output_dtype"), q_node.GetAttributeInt("saturate")); + new_q_node->SetLayeringAnnotation(transpose_node.GetLayeringAnnotation()); auto new_q_node_output = new_q_node->Outputs()[0]; // Copy value info from the q output for the type information, and update the shape to match Transpose's input @@ -2985,6 +2999,7 @@ static bool TryFixTransposeMissingDQ(OptimizerCtx& ctx, api::NodeRef& transpose_ // Add new DQ. auto new_dq_node = MakeDequantizeOp(ctx.graph, q_domain, inputs, axis, q_node.GetAttributeInt("block_size")); + new_dq_node->SetLayeringAnnotation(transpose_node.GetLayeringAnnotation()); auto new_dq_node_output = new_dq_node->Outputs()[0]; ctx.graph.CopyValueInfo(transpose_input_name, new_dq_node_output); diff --git a/onnxruntime/core/optimizer/transpose_optimization/optimizer_api.h b/onnxruntime/core/optimizer/transpose_optimization/optimizer_api.h index 6ff4da05fbf57..4ee5a65b9b9fb 100644 --- a/onnxruntime/core/optimizer/transpose_optimization/optimizer_api.h +++ b/onnxruntime/core/optimizer/transpose_optimization/optimizer_api.h @@ -258,6 +258,18 @@ class NodeRef { /// Id virtual int64_t Id() const = 0; + /// + /// Get the layering annotation of the node. + /// + /// annotation + virtual std::string_view GetLayeringAnnotation() const = 0; + + /// + /// Set layering annotation + /// + /// + virtual void SetLayeringAnnotation(std::string_view annotation) = 0; + virtual ~NodeRef() {}; }; diff --git a/onnxruntime/core/optimizer/transpose_optimization/ort_optimizer_api_impl.cc b/onnxruntime/core/optimizer/transpose_optimization/ort_optimizer_api_impl.cc index 6a02ca3578da2..5d5ed663cca05 100644 --- a/onnxruntime/core/optimizer/transpose_optimization/ort_optimizer_api_impl.cc +++ b/onnxruntime/core/optimizer/transpose_optimization/ort_optimizer_api_impl.cc @@ -105,6 +105,14 @@ class ApiNode final : public api::NodeRef { int SinceVersion() const override; int64_t Id() const override; + std::string_view GetLayeringAnnotation() const override { + return node_.GetLayeringAnnotation(); + } + + void SetLayeringAnnotation(std::string_view annotation) override { + node_.SetLayeringAnnotation(std::string(annotation)); + } + private: ORT_DISALLOW_COPY_ASSIGNMENT_AND_MOVE(ApiNode); }; @@ -763,6 +771,9 @@ std::unique_ptr ApiGraph::CopyNode(const api::NodeRef& source_node source_node.Outputs().size(), domain, new_node_since_version, source_node.GetExecutionProviderType()); + const auto& layering_annotation = source_node.GetLayeringAnnotation(); + node.SetLayeringAnnotation(std::string(layering_annotation)); + std::unique_ptr new_node = std::make_unique(node, graph_); new_node->CopyAttributes(source_node); diff --git a/onnxruntime/core/optimizer/utils.cc b/onnxruntime/core/optimizer/utils.cc index 4a323eefe1fe7..6d40b389d5fa3 100644 --- a/onnxruntime/core/optimizer/utils.cc +++ b/onnxruntime/core/optimizer/utils.cc @@ -495,6 +495,13 @@ bool IsScalar(const NodeArg& input_arg) { return dim_size == 0 || (dim_size == 1 && shape->dim(0).has_dim_value() && shape->dim(0).dim_value() == 1); } +void DuplicateNodeAnnotation(const Node& src, Node& dst) { + const auto& src_annotation = src.GetLayeringAnnotation(); + if (!src_annotation.empty()) { + dst.SetLayeringAnnotation(src_annotation); + } +} + template bool GetScalarInitializerValue(const onnxruntime::Graph& graph, const onnxruntime::NodeArg& input_arg, T& value, bool is_constant) { diff --git a/onnxruntime/core/optimizer/utils.h b/onnxruntime/core/optimizer/utils.h index 857640f861238..2f9b48df7a75f 100644 --- a/onnxruntime/core/optimizer/utils.h +++ b/onnxruntime/core/optimizer/utils.h @@ -175,6 +175,8 @@ bool CheckOutputEdges(const Graph& graph, const Node& node, size_t expected_outp // Check if NodeArg takes in a scalar tensor. bool IsScalar(const NodeArg& input_arg); +void DuplicateNodeAnnotation(const Node& src, Node& dst); + #endif // #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) } // namespace optimizer_utils diff --git a/onnxruntime/core/providers/cuda/cuda_execution_provider.cc b/onnxruntime/core/providers/cuda/cuda_execution_provider.cc index 953858dbfde6f..59cd42c72b951 100755 --- a/onnxruntime/core/providers/cuda/cuda_execution_provider.cc +++ b/onnxruntime/core/providers/cuda/cuda_execution_provider.cc @@ -3110,16 +3110,20 @@ CUDAExecutionProvider::GetCapability(const onnxruntime::GraphViewer& graph, } auto threshold = resource_accountant->GetThreshold(); - if (!threshold.has_value()) { + if (!threshold) { // info_.gpu_mem_limit is for BFC arena size_t free_memory, total_memory; if (0 != cudaMemGetInfo(&free_memory, &total_memory)) { memory_threshold = info_.gpu_mem_limit; + LOGS(logger, INFO) + << "CUDA_EP failed to get available GPU memory info. Using info_.gpu_mem_limit instead: " << info_.gpu_mem_limit; } else { memory_threshold = std::min(free_memory, info_.gpu_mem_limit); + LOGS(logger, VERBOSE) + << "CUDA_EP Using threshold: " << memory_threshold << " Free memory reported: " << free_memory; } } else { - memory_threshold = std::get<0>(threshold.value()); + memory_threshold = std::get<0>(*threshold); } consumed_memory = std::get<0>(resource_accountant->GetConsumedAmount()); diff --git a/onnxruntime/core/session/inference_session.cc b/onnxruntime/core/session/inference_session.cc index b873c95b496bb..2ba52a3e989bd 100644 --- a/onnxruntime/core/session/inference_session.cc +++ b/onnxruntime/core/session/inference_session.cc @@ -29,6 +29,7 @@ #include "core/framework/kernel_registry.h" #include "core/framework/kernel_type_str_resolver.h" #include "core/framework/kernel_type_str_resolver_utils.h" +#include "core/framework/layering_annotations.h" #include "core/framework/mldata_type_utils.h" #include "core/framework/TensorSeq.h" #include "core/framework/tensorprotoutils.h" @@ -1518,11 +1519,33 @@ common::Status InferenceSession::TransformGraph(onnxruntime::Graph& graph, bool } } + LayeringIndex* layering_index = nullptr; +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + std::optional layering_index_storage; + const auto layering_config = session_options_.config_options.GetConfigOrDefault(kOrtSessionOptionsLayerAssignmentSettings, ""); + if (!layering_config.empty()) { + ORT_RETURN_IF_ERROR_SESSIONID_(LayeringIndex::Create(graph, layering_config, {}, execution_providers_, + *session_logger_, layering_index_storage)); + if (layering_index_storage) { + layering_index = &layering_index_storage.value(); + } + } +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) // Do partitioning based on execution providers' capabilities. ORT_RETURN_IF_ERROR_SESSIONID_(partitioner.Partition(graph, session_state_->GetMutableFuncMgr(), transform_layout_fn, - session_options_.config_options, *session_logger_, + session_options_.config_options, *session_logger_, layering_index, mode, session_options_.GetEpContextGenerationOptions(), debug_graph_fn)); +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + if (layering_index) { + // Layering annotations maybe present even if index is not built although unlikely. + ORT_RETURN_IF_ERROR_SESSIONID_(graph.RemoveAllLayeringAnnotations()); + // We are currently not using it beyond this point. Clear it to free up memory. + layering_index = nullptr; + layering_index_storage.reset(); + } +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + // Get graph optimizations loop level from session config, if not present, set to default value of 1 as per // the definition of kOrtSessionOptionsGraphOptimizationsLoopLevel. unsigned int graph_optimizations_loop_level = static_cast(std::stoi( @@ -2039,6 +2062,7 @@ Status PartitionOrtFormatModel(onnxruntime::Graph& graph, transform_layout_fn, sess_options.config_options, logger, + nullptr /*layering_index*/, GraphPartitioner::Mode::kOrtFormatLoad)); #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) diff --git a/onnxruntime/core/session/onnxruntime_c_api.cc b/onnxruntime/core/session/onnxruntime_c_api.cc index 37a74a5de22a6..9834902cea2b1 100644 --- a/onnxruntime/core/session/onnxruntime_c_api.cc +++ b/onnxruntime/core/session/onnxruntime_c_api.cc @@ -3031,7 +3031,23 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, "src_graph is a ModelEditorGraph which doesn't support Graph_GetGraphView."); } const GraphViewer& graph_viewer = ep_graph->GetGraphViewer(); - const Graph& graph = graph_viewer.GetGraph(); + + // Create subgraph's node set and convert them to internal Node + InlinedHashSet node_set; + InlinedVector internal_nodes; + internal_nodes.reserve(num_nodes); + for (size_t i = 0; i < num_nodes; i++) { + const EpNode* ep_node = EpNode::ToInternal(nodes[i]); + if (ep_node != nullptr) { + const Node& node = ep_node->GetInternalNode(); + node_set.insert(node.Index()); + internal_nodes.push_back(&node); + } else { + std::ostringstream oss; + oss << "node indexed [" << i << "] appears to be a ModelEditorNode"; + return OrtApis::CreateStatus(OrtErrorCode::ORT_INVALID_ARGUMENT, oss.str().c_str()); + } + } // Create a GraphViewer with filtered info // TODO: Investigate whether utils::MakeComputeCapability can be extended and reused instead @@ -3040,178 +3056,93 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, // Following data structures help determine the final inputs/outputs of the subgraph. // Note: The 'subgraph' here refers to a graph contains a subset of nodes in the 'src_graph'. - // Subgraph's node set - const std::unordered_set node_set = [&]() { - std::unordered_set node_set; - for (size_t i = 0; i < num_nodes; i++) { - const OrtNode* ort_node = nodes[i]; - const EpNode* ep_node = EpNode::ToInternal(ort_node); - if (ep_node != nullptr) { - node_set.insert(ep_node->GetInternalNode().Index()); - } + // Pre-pass: Identify all outputs produced by nodes within the subgraph. + // This allows O(1) checks to determine if an input is internal or from the boundary. + InlinedHashSet internal_outputs; + for (size_t i = 0, lim = internal_nodes.size(); i < lim; i++) { + const auto& node = *internal_nodes[i]; + for (const auto& output : node.OutputDefs()) { + internal_outputs.insert(output); } - - return node_set; - }(); + } // Source graph output names - std::unordered_set graph_output_names; + InlinedHashSet graph_output_names; for (const auto* output_arg : graph_viewer.GetOutputs()) { graph_output_names.insert(output_arg->Name()); } // These maps store the inputs and outputs of the subgraph. - // Please note that the inputs and outputs of the maps will be dynamically updated during node iteration - // to determine the final inputs and outputs of the subgraph. - std::unordered_map subgraph_inputs, subgraph_outputs; - - // This map stores the node's output that will be consumed by another node outside of this subgraph. - // So the node's output should be put into the subgraph's output list. - std::unordered_map subgraph_outputs_to_add; - - // This map stores the node's output that is original graph's output. - // So the node's output should be put into the subgraph's output list. - std::unordered_map graph_outputs_to_add; + // Value is order index to maintain deterministic order. + InlinedHashMap subgraph_inputs, subgraph_outputs; - std::unordered_set erased; - - // This is the relative ordering that ensures node's input or output being added to the 'subgraph_inputs', - // 'subgraph_outputs', 'subgraph_outputs_to_add' and 'graph_outputs_to_add' maps is associated with a relative order index. - // Items added earlier receive a smaller order index than items added later. - // When constructing the final subgraph's input or output lists, entries with smaller - // order indices will appear before those with larger indices. int input_order = 0; int output_order = 0; - // node arg to its consumer nodes. - // Note: graph.GetConsumerNodes() is not available in minimal build, in order to use unified implementation across - // all builds, this map is needed to determine if node arg is consumed by other nodes. - std::unordered_map> node_arg_to_consumer_nodes; - - std::vector initializers; + InlinedVector initializers; - // Add nodes - for (size_t i = 0; i < num_nodes; i++) { - const OrtNode* ort_node = nodes[i]; - const EpNode* ep_node = EpNode::ToInternal(ort_node); - if (ep_node == nullptr) { - return OrtApis::CreateStatus(OrtErrorCode::ORT_INVALID_ARGUMENT, - "node is a ModelEditorNode which doesn't support Graph_GetGraphView."); - } - const Node& node = ep_node->GetInternalNode(); + // Add nodes and identify boundary inputs/outputs + for (size_t i = 0, lim = internal_nodes.size(); i < lim; i++) { + const auto& node = *internal_nodes[i]; indexed_sub_graph->nodes.push_back(node.Index()); - for (const auto& input : node.InputDefs()) { - if (!input->Exists()) { - continue; - } + // Process Inputs: If an input is not produced internally, it's a subgraph input. + auto process_inputs = [&](gsl::span inputs) { + for (const auto& input : inputs) { + if (!input->Exists()) continue; - if (graph_viewer.IsConstantInitializer(input->Name(), true)) { - initializers.push_back(input->Name()); - continue; - } - const auto& it = subgraph_outputs.find(input); - if (it != subgraph_outputs.end()) { - subgraph_outputs.erase(it); - erased.insert(input); - } else if (erased.find(input) == erased.end()) { - // Only when input is neither in output list nor erased list, add the input to input list - subgraph_inputs.insert({input, input_order++}); - } - } + if (graph_viewer.IsConstantInitializer(input->Name(), true)) { + initializers.push_back(input->Name()); + continue; + } - for (const auto& input : node.ImplicitInputDefs()) { - if (!input->Exists()) { - continue; + // If not produced by this subgraph, it's a boundary input + if (internal_outputs.count(input) == 0) { + // Use insert to keep the first occurrence's order + auto p = subgraph_inputs.emplace(input, input_order); + if (p.second) { + input_order++; + } + } } + }; - if (graph_viewer.IsConstantInitializer(input->Name(), true)) { - initializers.push_back(input->Name()); - continue; - } - const auto& it = subgraph_outputs.find(input); - if (it != subgraph_outputs.end()) { - subgraph_outputs.erase(it); - erased.insert(input); - } else if (erased.find(input) == erased.end()) { - // Only when input is neither in output list nor erased list, add the input to input list - subgraph_inputs.insert({input, input_order++}); - } - } + process_inputs(gsl::make_span(node.InputDefs().data(), node.InputDefs().size())); + process_inputs(gsl::make_span(node.ImplicitInputDefs().data(), node.ImplicitInputDefs().size())); - // For output searching, there are two special cases, - // One is, if subgraph's node output is parent graph's output. the node output should - // be also added to the subgraph's output list - // The other one is, if node's OutputEdges are more than its outputs, meaning certain output is used more than once, - // if the output is connected to nodes that don't belong to the subgraph, the output need to be added - // to the output list + // Process Outputs: If an output is graph output OR consumed externally, it's a subgraph output. for (const auto& output : node.OutputDefs()) { - if (!output->Exists()) { - continue; - } + if (!output->Exists()) continue; + + bool is_boundary_output = false; - const auto& it = subgraph_inputs.find(output); - if (it != subgraph_inputs.end()) { - subgraph_inputs.erase(it); - erased.insert(output); - } else if (erased.find(output) == erased.end()) { - auto has_consumer_nodes = [&](const std::string& node_arg_str) -> bool { - // Same implementation as Graph::PopulateNodeArgToProducerConsumerLookupsFromNodes() - if (node_arg_to_consumer_nodes.empty()) { - for (const auto& node : graph.Nodes()) { - node.ForEachDef([&](const NodeArg& node_arg, bool is_input) { - if (is_input) { - node_arg_to_consumer_nodes[node_arg.Name()].insert(node.Index()); - } - }); + // 1. Is it a graph output? + if (graph_output_names.count(output->Name()) > 0) { + is_boundary_output = true; + } else { + // 2. Is it consumed by any node outside the subgraph? + for (auto it = node.OutputEdgesBegin(), end = node.OutputEdgesEnd(); it != end; ++it) { + // Check if the edge uses this specific output + if (it->GetSrcArgIndex() < static_cast(node.OutputDefs().size()) && + node.OutputDefs()[it->GetSrcArgIndex()] == output) { + if (node_set.count(it->GetNode().Index()) == 0) { + is_boundary_output = true; + break; } } - return node_arg_to_consumer_nodes.find(node_arg_str) != node_arg_to_consumer_nodes.end(); - }; - - if (has_consumer_nodes(output->Name())) { - // Only when output is neither in input list nor erased list, - // and the output is consumed by another node, add the output to output list - subgraph_outputs.insert({output, output_order++}); } } - if (graph_output_names.find(output->Name()) != graph_output_names.end()) { - // This output is the graph's output. - // So the output should be put into the subgraph's output list. - graph_outputs_to_add.insert({output, output_order++}); - } - } - - if (node.GetOutputEdgesCount() > node.OutputDefs().size()) { - for (auto it = node.OutputEdgesBegin(), end = node.OutputEdgesEnd(); it != end; ++it) { - const auto& node_idx = it->GetNode().Index(); - - if (node_set.find(node_idx) == node_set.end()) { - // This output will be consumed by another node outside of this subgraph. - // So the output should be put into the subgraph's output list. - const NodeArg* output = nullptr; - - // The dst_arg_index from GetDstArgIndex() could be the index for explicit/implicit input defs of the node. - // We need to get the correct input index accordingly. (See Graph::BuildConnections() in graph.cc for more details) - if (it->GetDstArgIndex() < static_cast(it->GetNode().InputDefs().size())) { - output = (it->GetNode()).InputDefs()[it->GetDstArgIndex()]; - } else { - output = (it->GetNode()).ImplicitInputDefs()[it->GetDstArgIndex() - it->GetNode().InputDefs().size()]; - } - subgraph_outputs_to_add.insert({output, output_order++}); - } + if (is_boundary_output) { + subgraph_outputs.insert({output, output_order++}); } } } - subgraph_outputs.insert(subgraph_outputs_to_add.begin(), subgraph_outputs_to_add.end()); - subgraph_outputs.insert(graph_outputs_to_add.begin(), graph_outputs_to_add.end()); - std::multimap inputs, outputs; // Get the input order of the original graph - std::unordered_map original_inputs; + InlinedHashMap original_inputs; int order = 0; for (const auto* input : graph_viewer.GetInputs()) { original_inputs[input] = order++; @@ -3219,22 +3150,22 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, // input order needs to be consistent with original graph's input order for (const auto& [node_arg, subgraph_input_order] : subgraph_inputs) { - const auto& original_input_it = original_inputs.find(node_arg); + const auto original_input_it = original_inputs.find(node_arg); if (original_input_it != original_inputs.end()) { - inputs.insert(std::make_pair( + inputs.emplace( original_input_it->second, // input order from original graph - node_arg)); + node_arg); } else { - inputs.insert(std::make_pair( + inputs.emplace( subgraph_input_order, // input order from subgraph - node_arg)); + node_arg); } } // Sort outputs by the order they were added - for (auto it = subgraph_outputs.begin(), end = subgraph_outputs.end(); it != end; ++it) { - outputs.insert(std::pair(it->second, it->first)); + for (const auto& [node_arg, subgraph_output_order] : subgraph_outputs) { + outputs.emplace(subgraph_output_order, node_arg); } std::unique_ptr meta_def = std::make_unique(); @@ -3259,7 +3190,8 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, } indexed_sub_graph->SetMetaDef(std::move(meta_def)); - auto new_graph_viewer = std::make_unique(graph, *indexed_sub_graph.get()); + const Graph& graph = graph_viewer.GetGraph(); + auto new_graph_viewer = std::make_unique(graph, *indexed_sub_graph); std::unique_ptr result; ORT_API_RETURN_IF_STATUS_NOT_OK(EpGraph::Create(std::move(new_graph_viewer), std::move(indexed_sub_graph), result)); diff --git a/onnxruntime/python/tools/layering/layer_annotate.py b/onnxruntime/python/tools/layering/layer_annotate.py new file mode 100644 index 0000000000000..738c528b28754 --- /dev/null +++ b/onnxruntime/python/tools/layering/layer_annotate.py @@ -0,0 +1,165 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import logging +import pathlib + +import onnx + + +def get_logger(name, level=logging.DEBUG): + logging.basicConfig(format="%(asctime)s %(name)s [%(levelname)s] - %(message)s") + logger = logging.getLogger(name) + logger.setLevel(level) + return logger + + +def getargs(): + argparser = argparse.ArgumentParser( + description="Read a config file with a list of node annotations and apply them to an ONNX model.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + argparser.add_argument( + "--config_file_path", + type=pathlib.Path, + required=True, + help="Path to the configuration file with node annotations.", + ) + argparser.add_argument( + "--model_path", + type=pathlib.Path, + required=True, + help="Path to a single model to process.", + ) + argparser.add_argument( + "--annotated_model", + type=pathlib.Path, + required=True, + help="Path to write the annotated model to.", + ) + + return argparser.parse_args() + + +def read_annotation_config(config_file_path): + """ + Reads a configuration file to map substrings to annotations. + + The file format is expected to be: + annotation_string: substring1, substring2, ... + + The same annotation string can appear multiple times. + The node names in the configuration are treated as substrings. + + Args: + config_file_path (str or Path): Path to the configuration file. + + Returns: + list: A list of tuples (substring, annotation_string). + """ + substring_annotations = [] + with open(config_file_path) as f: + for unstripped_line in f: + line = unstripped_line.strip() + if not line: + continue + parts = line.split(":", 1) + if len(parts) < 2: + continue + annotation = parts[0].strip() + substrings = parts[1].split(",") + for substr in substrings: + substring = substr.strip() + if substring: + substring_annotations.append((substring, annotation)) + return substring_annotations + + +def process_nodes(nodes, substring_annotations): + """ + Helper function to process a list of nodes sequentially. + """ + logger = get_logger("annotate_model") + logger.info(f"Processing {len(nodes)} nodes.") + + for node in nodes: + matched_annotation = None + for substring, annotation in substring_annotations: + if substring in node.name: + matched_annotation = annotation + + if matched_annotation: + # Check if annotation already exists + entry = None + for prop in node.metadata_props: + if prop.key == "layer_ann": + entry = prop + break + + if entry: + entry.value = matched_annotation + else: + entry = node.metadata_props.add() + entry.key = "layer_ann" + entry.value = matched_annotation + + # Recurse into subgraphs for control flow nodes + for attr in node.attribute: + if attr.type == onnx.AttributeProto.GRAPH: + annotate_graph(attr.g, substring_annotations) + elif attr.type == onnx.AttributeProto.GRAPHS: + for sub_graph in attr.graphs: + annotate_graph(sub_graph, substring_annotations) + + +def annotate_graph(graph, substring_annotations): + """ + Recursively applies annotations to nodes where a configured substring appears in the node name. + + This function iterates over all nodes in the given graph. It checks if any + substring from the configuration appears in the node's name. If matched, + it adds or updates a metadata property with key 'layer_ann' containing + the annotation string. If multiple substrings match, the last one defined + in the configuration list applies. + + It also handles control flow nodes (like 'If' or 'Loop') by recursively + processing their subgraphs (attributes of type GRAPH or GRAPHS). + + Args: + graph (onnx.GraphProto): The ONNX graph to process. + substring_annotations (list): A list of tuples (substring, annotation_string). + """ + process_nodes(graph.node, substring_annotations) + + +def annotate_model(model, substring_annotations): + """ + Annotates an ONNX model with metadata based on a provided mapping. + + This function serves as the entry point to annotate the model's graph. + It delegates the work to `annotate_graph`, which recursively processes + all nodes in the main graph and any nested subgraphs. + + Args: + model (onnx.ModelProto): The ONNX model to annotate. + substring_annotations (list): A list of tuples (substring, annotation_string). + """ + annotate_graph(model.graph, substring_annotations) + + +if __name__ == "__main__": + args = getargs() + logger = get_logger("annotate_model") + + # Read the mapping from the configuration file + substring_annotations = read_annotation_config(args.config_file_path) + + logger.info(f"Loading model from {args.model_path}") + onnx_model = onnx.load(args.model_path, load_external_data=False) + + logger.info(f"Applying annotations from {args.config_file_path}") + annotate_model(onnx_model, substring_annotations) + + logger.info(f"Saving annotated model to {args.annotated_model}") + onnx.save_model(onnx_model, args.annotated_model) diff --git a/onnxruntime/test/framework/function_test.cc b/onnxruntime/test/framework/function_test.cc index 699d1b1a2c27a..9e28882b9a65d 100644 --- a/onnxruntime/test/framework/function_test.cc +++ b/onnxruntime/test/framework/function_test.cc @@ -662,5 +662,161 @@ TEST(FunctionTest, Test_GH_issue_16438) { status = session_object.Initialize(); ASSERT_TRUE(status.IsOK()) << status.ErrorMessage(); } + +// Verify that when a function node with a layering annotation is inlined, +// the inlined nodes inherit the parent function node's annotation. +TEST(FunctionTest, InlinedNodesInheritLayeringAnnotation) { + // Parse and build a Model with a local function (multi-node body: Constant + Mul). + ONNX_NAMESPACE::OnnxParser parser(basic_code); + ONNX_NAMESPACE::ModelProto model_proto; + auto parse_status = parser.Parse(model_proto); + ASSERT_TRUE(parse_status.IsOK()) << parse_status.ErrorMessage(); + ASSERT_TRUE(parser.EndOfInput()) << "Extra unparsed input unexpected."; + + auto& logger = DefaultLoggingManager().DefaultLogger(); + std::shared_ptr model; + ASSERT_STATUS_OK(Model::Load(std::move(model_proto), model, nullptr, logger)); + + Graph& graph = model->MainGraph(); + ASSERT_STATUS_OK(graph.Resolve()); + + // Find the function call node (local.myfun) and annotate it. + Node* func_node = nullptr; + for (auto& node : graph.Nodes()) { + if (node.OpType() == "myfun") { + func_node = &node; + break; + } + } + ASSERT_NE(func_node, nullptr) << "Could not find function call node 'myfun'"; + ASSERT_TRUE(func_node->CanBeInlined()); + + const std::string annotation = "TestLayerAnnotation"; + func_node->SetLayeringAnnotation(annotation); + + // Inline the function node. + ASSERT_STATUS_OK(graph.InlineFunction(*func_node)); + ASSERT_STATUS_OK(graph.Resolve()); + + // After inlining, the original function call node is removed and replaced + // by the function body nodes (a Mul node; the Constant becomes an initializer). + // Verify every remaining node inherited the annotation. + int node_count = 0; + for (const auto& node : graph.Nodes()) { + ++node_count; + EXPECT_EQ(node.GetLayeringAnnotation(), annotation) + << "Node '" << node.Name() << "' (op: " << node.OpType() + << ") did not inherit the parent function's layering annotation."; + } + EXPECT_GT(node_count, 0) << "Expected at least one inlined node in the graph."; +} + +// Verify that when a function node with no layering annotation is inlined, +// the inlined nodes remain unannotated. +TEST(FunctionTest, InlinedNodesNoAnnotationWhenParentUnannotated) { + ONNX_NAMESPACE::OnnxParser parser(basic_code); + ONNX_NAMESPACE::ModelProto model_proto; + auto parse_status = parser.Parse(model_proto); + ASSERT_TRUE(parse_status.IsOK()) << parse_status.ErrorMessage(); + ASSERT_TRUE(parser.EndOfInput()) << "Extra unparsed input unexpected."; + + auto& logger = DefaultLoggingManager().DefaultLogger(); + std::shared_ptr model; + ASSERT_STATUS_OK(Model::Load(std::move(model_proto), model, nullptr, logger)); + + Graph& graph = model->MainGraph(); + ASSERT_STATUS_OK(graph.Resolve()); + + Node* func_node = nullptr; + for (auto& node : graph.Nodes()) { + if (node.OpType() == "myfun") { + func_node = &node; + break; + } + } + ASSERT_NE(func_node, nullptr); + // Do NOT set any annotation on the function node. + ASSERT_TRUE(func_node->GetLayeringAnnotation().empty()); + + ASSERT_STATUS_OK(graph.InlineFunction(*func_node)); + ASSERT_STATUS_OK(graph.Resolve()); + + for (const auto& node : graph.Nodes()) { + EXPECT_TRUE(node.GetLayeringAnnotation().empty()) + << "Node '" << node.Name() << "' should not have a layering annotation " + << "when the parent function node was unannotated."; + } +} + +// Verify annotation inheritance with two calls to the same function, +// where each call has a different annotation. +TEST(FunctionTest, InlinedNodesInheritDistinctAnnotationsPerCallSite) { + const char* code = R"( + < + ir_version: 8, + opset_import: [ "" : 16, "local" : 1 ] + > + agraph (float[N] x) => (float[N] y) + { + y1 = local.myfun (x) + y = local.myfun (y1) + } + + < + opset_import: [ "" : 16 ], + domain: "local" + > + myfun (lx) => (ly) { + two = Constant () + ly = Mul (lx, two) + } + )"; + + ONNX_NAMESPACE::OnnxParser parser(code); + ONNX_NAMESPACE::ModelProto model_proto; + auto parse_status = parser.Parse(model_proto); + ASSERT_TRUE(parse_status.IsOK()) << parse_status.ErrorMessage(); + ASSERT_TRUE(parser.EndOfInput()); + + auto& logger = DefaultLoggingManager().DefaultLogger(); + std::shared_ptr model; + ASSERT_STATUS_OK(Model::Load(std::move(model_proto), model, nullptr, logger)); + + Graph& graph = model->MainGraph(); + ASSERT_STATUS_OK(graph.Resolve()); + + // Collect the two function call nodes in graph order. + std::vector func_nodes; + for (auto& node : graph.Nodes()) { + if (node.OpType() == "myfun") { + func_nodes.push_back(&node); + } + } + ASSERT_EQ(func_nodes.size(), 2u); + + // Annotate each call site differently. + func_nodes[0]->SetLayeringAnnotation("AnnotationA"); + func_nodes[1]->SetLayeringAnnotation("AnnotationB"); + + // Inline the first call, then the second. + ASSERT_STATUS_OK(graph.InlineFunction(*func_nodes[0])); + ASSERT_STATUS_OK(graph.InlineFunction(*func_nodes[1])); + ASSERT_STATUS_OK(graph.Resolve()); + + // After inlining both calls, the graph should have nodes from both expansions. + // Each group should carry its respective annotation. + bool found_a = false; + bool found_b = false; + for (const auto& node : graph.Nodes()) { + const auto& ann = node.GetLayeringAnnotation(); + EXPECT_TRUE(ann == "AnnotationA" || ann == "AnnotationB") + << "Node '" << node.Name() << "' has unexpected annotation: '" << ann << "'"; + if (ann == "AnnotationA") found_a = true; + if (ann == "AnnotationB") found_b = true; + } + EXPECT_TRUE(found_a) << "No node found with AnnotationA"; + EXPECT_TRUE(found_b) << "No node found with AnnotationB"; +} + } // namespace test } // namespace onnxruntime diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc new file mode 100644 index 0000000000000..f865be7bfc686 --- /dev/null +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -0,0 +1,1763 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + +#include "core/framework/execution_providers.h" +#include "core/framework/ortmemoryinfo.h" +#include "core/framework/layering_annotations.h" +#include "core/session/abi_devices.h" +#include "core/framework/execution_provider.h" +#include "core/framework/ortdevice.h" +#include "core/graph/constants.h" +#include "core/graph/model.h" // For Model, Graph +#include "gtest/gtest.h" + +#include "test/util/include/asserts.h" +#include "test/util/include/test_environment.h" + +namespace onnxruntime { +namespace test { + +TEST(LayeringRuleMatcherTest, ExactMatches) { + LayeringRules rules; + rules.rules.push_back({"Device1", "Annotation1", false}); // Index 0 + rules.rules.push_back({"Device2", "Annotation2", false}); // Index 1 + + LayeringRuleMatcher matcher(rules); + + { + auto result = matcher.Match("Annotation1"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); + } + { + auto result = matcher.Match("Annotation2"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 1u); + } + { + auto result = matcher.Match("Annotation3"); + EXPECT_FALSE(result.has_value()); + } +} + +TEST(LayeringRuleMatcherTest, PrefixMatches) { + LayeringRules rules; + rules.rules.push_back({"Device1", "Prefix1", true}); // Index 0: =Prefix1 + rules.rules.push_back({"Device2", "Pre", true}); // Index 1: =Pre + + LayeringRuleMatcher matcher(rules); + + // "Prefix1Suffix" matches "Prefix1" (idx 0) and "Pre" (idx 1). 0 < 1, so 0. + { + auto result = matcher.Match("Prefix1Suffix"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); + } + + // "PreSuffix" matches "Pre" (idx 1). "Prefix1" does not match. + { + auto result = matcher.Match("PreSuffix"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 1u); + } + + // "Other" matches nothing + { + auto result = matcher.Match("Other"); + EXPECT_FALSE(result.has_value()); + } +} + +TEST(LayeringRuleMatcherTest, PriorityPrefixOverExact) { + // Prefix matches should take precedence over exact matches regardless of order. + + // Case 1: Prefix rule comes before Exact rule + { + LayeringRules rules; + rules.rules.push_back({"Device1", "A", true}); // Index 0: =A (Prefix) + rules.rules.push_back({"Device2", "AB", false}); // Index 1: AB (Exact) + + LayeringRuleMatcher matcher(rules); + // "AB" matches prefix "A" (idx 0) and exact "AB" (idx 1). + // Since prefix matches are checked first and returned if found, we expect 0. + auto result = matcher.Match("AB"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); + } + + // Case 2: Exact rule comes before Prefix rule + { + LayeringRules rules; + rules.rules.push_back({"Device1", "AB", false}); // Index 0: AB (Exact) + rules.rules.push_back({"Device2", "A", true}); // Index 1: =A (Prefix) + + LayeringRuleMatcher matcher(rules); + // "AB" matches exact "AB" (idx 0) and prefix "A" (idx 1). + // Priority says Prefix matches are returned first. + auto result = matcher.Match("AB"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 1u); + } +} + +TEST(LayeringRuleMatcherTest, LongestOrShortestPrefixPriority) { + // If multiple prefix rules match, the one with the lowest index (earliest in config) wins. + + // Case 1: Shorter prefix first + { + LayeringRules rules; + rules.rules.push_back({"Device1", "A", true}); // Index 0 + rules.rules.push_back({"Device2", "AB", true}); // Index 1 + + LayeringRuleMatcher matcher(rules); + // "ABC" matches "A" (0) and "AB" (1). Since 0 < 1, best match is 0. + auto result = matcher.Match("ABC"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); + } + + // Case 2: Longer prefix first + { + LayeringRules rules; + rules.rules.push_back({"Device1", "AB", true}); // Index 0 + rules.rules.push_back({"Device2", "A", true}); // Index 1 + + LayeringRuleMatcher matcher(rules); + // "ABC" matches "AB" (0) and "A" (1). Since 0 < 1, best match is 0. + auto result = matcher.Match("ABC"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); + } +} + +TEST(LayeringRuleMatcherTest, OverlappingExactMatchPriority) { + // If duplicates exist, first one wins. + LayeringRules rules; + rules.rules.push_back({"Device1", "A", false}); // Index 0 + rules.rules.push_back({"Device2", "A", false}); // Index 1 + + LayeringRuleMatcher matcher(rules); + auto result = matcher.Match("A"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); +} + +TEST(LayeringRuleMatcherTest, OverlappingPrefixMatchPriority) { + // If duplicates exist, first one wins. + LayeringRules rules; + rules.rules.push_back({"Device1", "A", true}); // Index 0 + rules.rules.push_back({"Device2", "A", true}); // Index 1 + + LayeringRuleMatcher matcher(rules); + auto result = matcher.Match("AB"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); +} + +namespace { + +// Helper to construct OrtEpDevice wrappers for testing +struct TestEpDevice { + std::string ep_name; + OrtHardwareDevice hw_device; + bool has_hw_device = false; + OrtMemoryInfo mem_info; + bool has_mem_info = false; + + // We need to keep the structures alive while OrtEpDevice points to them + OrtEpDevice Get() const { + OrtEpDevice ep; + ep.ep_name = ep_name; + ep.device = has_hw_device ? &hw_device : nullptr; + ep.device_memory_info = has_mem_info ? &mem_info : nullptr; + return ep; + } +}; + +TestEpDevice CreateEp(const std::string& name) { + TestEpDevice ep; + ep.ep_name = name; + return ep; +} + +TestEpDevice CreateHwEp(const std::string& name, OrtHardwareDeviceType type, uint32_t vendor_id = 0, + uint32_t device_id = 0, const std::string& vendor_str = std::string()) { + TestEpDevice ep; + ep.ep_name = name; + ep.hw_device = {type, vendor_id, device_id, vendor_str, {}}; + ep.has_hw_device = true; + return ep; +} + +TestEpDevice CreateMemEp(const std::string& name, OrtDevice::DeviceType type, int device_id = 0) { + TestEpDevice ep; + ep.ep_name = name; + // Note: OrtMemoryInfo name doesn't matter for logic now, but required for ctor + ep.mem_info = OrtMemoryInfo("TestMem", OrtAllocatorType::OrtDeviceAllocator, + OrtDevice(type, OrtDevice::MemType::DEFAULT, OrtDevice::VendorIds::NONE, + static_cast(device_id)), + OrtMemType::OrtMemTypeDefault); + ep.has_mem_info = true; + return ep; +} + +} // namespace + +TEST(EpLayeringMatcherTest, MatchCPU) { + LayerAnnotation rule = {"CPU", "Anno1", false}; + + // Case 1: EP Name kCpuExecutionProvider + { + auto test_ep = CreateEp(kCpuExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCpuExecutionProvider); + } + + // Case 2: Hardware Device CPU + { + auto test_ep = CreateHwEp("SomeCPU_EP", OrtHardwareDeviceType_CPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "SomeCPU_EP"); + } + + // Case 3: Memory Info CPU + { + auto test_ep = CreateMemEp("MemCPU_EP", OrtDevice::CPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MemCPU_EP"); + } +} + +TEST(EpLayeringMatcherTest, MatchGPU) { + LayerAnnotation rule = {"GPU", "Anno1", false}; + + // Case 1: Hardware Device GPU + { + auto test_ep = CreateHwEp("MyGPU_EP", OrtHardwareDeviceType_GPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyGPU_EP"); + } + + // Case 2: Memory Info GPU + { + auto test_ep = CreateMemEp("MemGPU_EP", OrtDevice::GPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MemGPU_EP"); + } + + // Case 3: Heuristic kCudaExecutionProvider + { + auto test_ep = CreateEp(kCudaExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCudaExecutionProvider); + } + + // Case 4: Heuristic kDmlExecutionProvider + { + auto test_ep = CreateEp(kDmlExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kDmlExecutionProvider); + } +} + +TEST(EpLayeringMatcherTest, MatchSpecificGPU_VendorString) { + LayerAnnotation rule = {"gpu:nvidia", "Anno1", false}; + + // Case 1: Vendor String Match + { + auto test_ep = CreateHwEp("MyNvidia_EP", OrtHardwareDeviceType_GPU, 0, 0, "NVIDIA"); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyNvidia_EP"); + } + + // Case 2: Vendor String Mismatch + { + auto test_ep = CreateHwEp("MyAMD_EP", OrtHardwareDeviceType_GPU, 0, 0, "AMD"); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + EXPECT_FALSE(result.has_value()); + } +} + +TEST(EpLayeringMatcherTest, MatchSpecificGPU_VendorId) { + LayerAnnotation rule_intel = {"gpu:intel", "Anno1", false}; + LayerAnnotation rule_nvidia = {"gpu:nvidia", "Anno2", false}; + LayerAnnotation rule_amd = {"gpu:amd", "Anno3", false}; + + // Case 1: Vendor ID Match Intel + { + auto test_ep = CreateHwEp("Intel_EP", OrtHardwareDeviceType_GPU, OrtDevice::VendorIds::INTEL); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule_intel); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "Intel_EP"); + } + + // Case 2: Vendor ID Match Nvidia + { + auto test_ep = CreateHwEp("Nvidia_EP", OrtHardwareDeviceType_GPU, OrtDevice::VendorIds::NVIDIA); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule_nvidia); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "Nvidia_EP"); + } + + // Case 3: Vendor ID Match AMD + { + auto test_ep = CreateHwEp("AMD_EP", OrtHardwareDeviceType_GPU, OrtDevice::VendorIds::AMD); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule_amd); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "AMD_EP"); + } +} + +TEST(EpLayeringMatcherTest, MatchSpecificGPU_Heuristic) { + LayerAnnotation rule = {"gpu:nvidia", "Anno1", false}; + + // Case 1: kCudaExecutionProvider -> nvidia + { + // Need an EP with GPU HW type but generic vendor info to trigger the heuristic + auto test_ep_hw = CreateHwEp(kCudaExecutionProvider, OrtHardwareDeviceType_GPU); + OrtEpDevice ep_device = test_ep_hw.Get(); + std::vector devices = {&ep_device}; + + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCudaExecutionProvider); + } +} + +TEST(EpLayeringMatcherTest, MatchSpecificGPU_Index) { + LayerAnnotation rule = {"gpu:1", "Anno1", false}; + + // Case 1: ID Match + { + auto test_ep = CreateHwEp("GPU1", OrtHardwareDeviceType_GPU, 0, 1); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "GPU1"); + } + + // Case 2: ID Mismatch + { + auto test_ep = CreateHwEp("GPU0", OrtHardwareDeviceType_GPU, 0, 0); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + EXPECT_FALSE(result.has_value()); + } +} + +TEST(EpLayeringMatcherTest, MatchAccelerator) { + LayerAnnotation rule = {"accelerator", "Anno1", false}; + + // Case 1: CPU EP should NOT match + { + auto test_ep = CreateEp(kCpuExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + EXPECT_FALSE(result.has_value()); + } + + // Case 2: Custom EP, No HW/Mem info, considered accelerator + { + auto test_ep = CreateEp("MyCustomAccel"); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyCustomAccel"); + } + + // Case 3: GPU HW is an accelerator + { + auto test_ep = CreateHwEp("MyGPU", OrtHardwareDeviceType_GPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyGPU"); + } +} + +TEST(EpLayeringMatcherTest, MatchNPU) { + LayerAnnotation rule = {"npu", "Anno1", false}; + + // Case 1: Hardware NPU + { + auto test_ep = CreateHwEp("MyNPU", OrtHardwareDeviceType_NPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyNPU"); + } + + // Case 2: QNN Heuristic + { + auto test_ep = CreateEp(kQnnExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kQnnExecutionProvider); + } +} + +TEST(EpLayeringMatcherTest, MatchFPGA) { + LayerAnnotation rule = {"fpga", "Anno1", false}; + + // Case 1: MemInfo says FPGA + { + auto test_ep = CreateMemEp("MyFPGA", OrtDevice::FPGA); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyFPGA"); + } +} + +TEST(EpLayeringMatcherTest, MatchDirectDesignators) { + LayerAnnotation rule_cuda = {"cuda", "A", false}; + LayerAnnotation rule_dml = {"dml", "B", false}; + + { + auto test_ep = CreateEp(kCudaExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule_cuda); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCudaExecutionProvider); + } + { + auto test_ep = CreateEp(kDmlExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule_dml); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kDmlExecutionProvider); + } +} + +TEST(EpLayeringMatcherTest, MatchExactEPName) { + LayerAnnotation rule = {"MyCustomEP", "Anno1", false}; + + { + auto test_ep = CreateEp("MyCustomEP"); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyCustomEP"); + } +} + +namespace { + +// Minimal concrete implementation of IExecutionProvider for testing +class MockExecutionProvider : public IExecutionProvider { + public: + MockExecutionProvider(const std::string& type, OrtDevice device) + : IExecutionProvider(type, device) {} + + std::shared_ptr GetKernelRegistry() const override { return nullptr; } +}; + +} // namespace + +TEST(EpLayeringMatcherTest, MatchExecutionProviders_CPU) { + LayerAnnotation rule = {"CPU", "Anno1", false}; + ExecutionProviders providers; + + // Add CPU provider + auto cpu_ep = std::make_shared(kCpuExecutionProvider, OrtDevice(OrtDevice::CPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCpuExecutionProvider, cpu_ep)); + + // Add a GPU provider (should be skipped for CPU rule) + auto gpu_ep = std::make_shared(kCudaExecutionProvider, OrtDevice(OrtDevice::GPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCudaExecutionProvider, gpu_ep)); + + auto result = EpLayeringMatcher::Match(providers, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCpuExecutionProvider); +} + +TEST(EpLayeringMatcherTest, MatchExecutionProviders_GPU) { + LayerAnnotation rule = {"GPU", "Anno1", false}; + ExecutionProviders providers; + + // Add CPU provider (should be skipped) + auto cpu_ep = std::make_shared(kCpuExecutionProvider, OrtDevice(OrtDevice::CPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCpuExecutionProvider, cpu_ep)); + + // Add CUDA provider (GPU) + auto gpu_ep = std::make_shared(kCudaExecutionProvider, OrtDevice(OrtDevice::GPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCudaExecutionProvider, gpu_ep)); + + auto result = EpLayeringMatcher::Match(providers, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCudaExecutionProvider); +} + +TEST(EpLayeringMatcherTest, MatchExecutionProviders_GPU_Specific) { + LayerAnnotation rule = {"gpu:nvidia", "Anno1", false}; // Assumes heuristics or vendor ID logic + ExecutionProviders providers; + + // Add CPU provider + auto cpu_ep = std::make_shared(kCpuExecutionProvider, OrtDevice(OrtDevice::CPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCpuExecutionProvider, cpu_ep)); + + // Add CUDA provider (NVIDIA vendor ID) + auto gpu_ep = std::make_shared(kCudaExecutionProvider, + OrtDevice(OrtDevice::GPU, OrtDevice::MemType::DEFAULT, OrtDevice::VendorIds::NVIDIA, 0)); + ASSERT_STATUS_OK(providers.Add(kCudaExecutionProvider, gpu_ep)); + + auto result = EpLayeringMatcher::Match(providers, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCudaExecutionProvider); +} + +TEST(EpLayeringMatcherTest, MatchExecutionProviders_NoMatch) { + LayerAnnotation rule = {"GPU", "Anno1", false}; + ExecutionProviders providers; + + // Only CPU provider available + auto cpu_ep = std::make_shared(kCpuExecutionProvider, OrtDevice(OrtDevice::CPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCpuExecutionProvider, cpu_ep)); + + auto result = EpLayeringMatcher::Match(providers, rule); + EXPECT_FALSE(result.has_value()); +} + +TEST(EpLayeringMatcherTest, MatchExecutionProviders_Accelerator) { + LayerAnnotation rule = {"accelerator", "Anno1", false}; + ExecutionProviders providers; + + // Add CPU + auto cpu_ep = std::make_shared(kCpuExecutionProvider, OrtDevice(OrtDevice::CPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCpuExecutionProvider, cpu_ep)); + + // Add custom accelerator + auto accel_ep = std::make_shared("MyAccel", OrtDevice(OrtDevice::NPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add("MyAccel", accel_ep)); + + auto result = EpLayeringMatcher::Match(providers, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyAccel"); +} + +TEST(LayeringIndexTest, AssignNodesBasedOnAnnotations) { + // 1. Setup Graph + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + // Create nodes + // Node 0: "AnnotatedNode" -> Annotated with "RuleA" + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg0 = &graph.GetOrCreateNodeArg("output0", &type_proto); + Node& node0 = graph.AddNode("node0", "Abs", "Node 0", {input_arg}, {output_arg0}); + node0.SetLayeringAnnotation("RuleA"); + + // Node 1: "UnannotatedNode" -> No annotation + NodeArg* output_arg1 = &graph.GetOrCreateNodeArg("output1", &type_proto); + Node& node1 = graph.AddNode("node1", "Abs", "Node 1", {output_arg0}, {output_arg1}); + // No annotation + + // Node 2: "AnnotatedNode2" -> Annotated with "RuleB" + NodeArg* output_arg2 = &graph.GetOrCreateNodeArg("output2", &type_proto); + Node& node2 = graph.AddNode("node2", "Abs", "Node 2", {output_arg1}, {output_arg2}); + node2.SetLayeringAnnotation("RuleB"); + + ASSERT_STATUS_OK(graph.Resolve()); + + // 2. Setup Rules and Matcher + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + rules.rules.push_back({"DeviceB", "RuleB", false}); // Index 1 + LayeringRuleMatcher matcher(rules); + + // 3. Setup Pre-computed Mappings (simulating Partitioning Manager) + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + ep_map["DeviceB"].insert(1); + + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + rule_map[1] = "DeviceB"; + + // 4. Create LayeringIndex + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // 5. Verify Assignments + // Node 0: Annotated "RuleA" -> Index 0 -> DeviceA + auto assign0 = index.GetNodeAssignment(graph, node0.Index()); + ASSERT_TRUE(assign0.has_value()); + EXPECT_EQ(*assign0, 0u); + + // Node 1: Unannotated -> Should generally map to nothing (unless defaulting logic exists, + // but current impl leaves unannotated in main graph as unassigned) + auto assign1 = index.GetNodeAssignment(graph, node1.Index()); + EXPECT_FALSE(assign1.has_value()); + + // Node 2: Annotated "RuleB" -> Index 1 -> DeviceB + auto assign2 = index.GetNodeAssignment(graph, node2.Index()); + ASSERT_TRUE(assign2.has_value()); + EXPECT_EQ(*assign2, 1u); +} + +TEST(LayeringIndexTest, AssignNodeWithInvalidEpMapping) { + // Scenario: Node annotated with a rule that maps to an EP that is NOT present/valid + + // 1. Setup Graph with one node annotated "RuleX" + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& node = graph.AddNode("node", "Abs", "Node", {input_arg}, {output_arg}); + node.SetLayeringAnnotation("RuleX"); + + ASSERT_STATUS_OK(graph.Resolve()); + + // 2. Setup Rules: RuleX exists at index 0, maps to "PhantomDevice" + LayeringRules rules; + rules.rules.push_back({"PhantomDevice", "RuleX", false}); // Index 0 + + // 3. Setup Mappings: But "PhantomDevice" is NOT in the mappings (simulating EP unavailable) + LayeringIndex::EpNameToLayeringIndices ep_map; + // ep_map["PhantomDevice"] is empty/missing + + LayeringIndex::LayeringIndexToEpName rule_map; + // rule_map[0] is missing + + // 4. Create Index + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + // 5. Verify: Node should NOT be assigned because the mapped EP is missing + auto assign = index.GetNodeAssignment(graph, node.Index()); + EXPECT_FALSE(assign.has_value()); +} + +TEST(LayeringIndexTest, SubgraphInheritance) { + // Scenario: Annotated Node containing a subgraph. + // Nodes inside subgraph (unannotated) should inherit parent's assignment. + + // 1. Setup Parent Graph + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_BOOL); + NodeArg* cond_arg = &graph.GetOrCreateNodeArg("cond", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + // Create "If" node + Node& if_node = graph.AddNode("if_node", "If", "If Node", {cond_arg}, {output_arg}); + if_node.SetLayeringAnnotation("RuleA"); // Annotate Parent + + auto build_subgraph = [](ONNX_NAMESPACE::GraphProto& proto, const std::string& graph_name, + const std::string& node_name, const std::string& input_name, const std::string& output_name) { + proto.set_name(graph_name); + // Inputs: Implicit from outer scope for 'cond' + + auto* node = proto.add_node(); + node->set_name(node_name); + node->set_op_type("Identity"); + node->add_input(input_name); + node->add_output(output_name); + + auto* out_vi = proto.add_output(); + out_vi->set_name(output_name); + out_vi->mutable_type()->mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_BOOL); + }; + + // Create Subgraph (then_branch) + ONNX_NAMESPACE::GraphProto then_graph_proto; + build_subgraph(then_graph_proto, "then_graph", "sub_node", "cond", "sub_out"); + if_node.AddAttribute("then_branch", then_graph_proto); + + // Create 'else_branch' + ONNX_NAMESPACE::GraphProto else_graph_proto; + build_subgraph(else_graph_proto, "else_graph", "else_sub_node", "cond", "else_sub_out"); + if_node.AddAttribute("else_branch", else_graph_proto); + + // First Resolve to create subgraph instances + ASSERT_STATUS_OK(graph.Resolve()); + + // Get subgraph instances (checked to ensure they exist) + Graph* then_graph = if_node.GetMutableGraphAttribute("then_branch"); + ASSERT_NE(then_graph, nullptr); + Graph* else_graph = if_node.GetMutableGraphAttribute("else_branch"); + ASSERT_NE(else_graph, nullptr); + + // 2. Setup Rules + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + // 3. Create Index + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // 4. Verify Parent Assignment + auto assign_parent = index.GetNodeAssignment(graph, if_node.Index()); + ASSERT_TRUE(assign_parent.has_value()); + EXPECT_EQ(*assign_parent, 0u); + + // 5. Verify Subgraph Node Assignment (Inheritance) + bool validated_then = false; + for (const auto& node : then_graph->Nodes()) { + if (node.OpType() == "Identity") { + auto assign_sub = index.GetNodeAssignment(*then_graph, node.Index()); + ASSERT_TRUE(assign_sub.has_value()) << "Subgraph node should inherit parent annotation"; + EXPECT_EQ(*assign_sub, 0u); + validated_then = true; + } + } + ASSERT_TRUE(validated_then); +} + +TEST(LayeringIndexTest, SubgraphOverride) { + // Scenario: Annotated Node containing a subgraph. + // Node inside subgraph HAS annotation -> Should override inheritance. + + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_BOOL); + NodeArg* cond_arg = &graph.GetOrCreateNodeArg("cond", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& if_node = graph.AddNode("if_node", "If", "If Node", {cond_arg}, {output_arg}); + if_node.SetLayeringAnnotation("RuleA"); // Annotate Parent = Rule A (Index 0) + + auto build_subgraph = [](ONNX_NAMESPACE::GraphProto& proto, const std::string& graph_name, + const std::string& node_name, const std::string& input_name, const std::string& output_name) { + proto.set_name(graph_name); + + auto* node = proto.add_node(); + node->set_name(node_name); + node->set_op_type("Identity"); + node->add_input(input_name); + node->add_output(output_name); + + auto* out_vi = proto.add_output(); + out_vi->set_name(output_name); + out_vi->mutable_type()->mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_BOOL); + }; + + ONNX_NAMESPACE::GraphProto then_graph_proto; + build_subgraph(then_graph_proto, "then_graph", "sub_node", "cond", "sub_out"); + if_node.AddAttribute("then_branch", then_graph_proto); + + ONNX_NAMESPACE::GraphProto else_graph_proto; + build_subgraph(else_graph_proto, "else_graph", "else_sub_node", "cond", "else_sub_out"); + if_node.AddAttribute("else_branch", else_graph_proto); + + ASSERT_STATUS_OK(graph.Resolve()); + + Graph* then_graph = if_node.GetMutableGraphAttribute("then_branch"); + ASSERT_NE(then_graph, nullptr); + + // Find sub_node to set annotation + Node* sub_node = nullptr; + for (auto& node : then_graph->Nodes()) { + if (node.Name() == "sub_node") { + sub_node = &node; + break; + } + } + ASSERT_NE(sub_node, nullptr); + + // OVERRIDE: Annotate sub_node with Rule B + sub_node->SetLayeringAnnotation("RuleB"); + + // Rules: RuleA(0)->DeviceA, RuleB(1)->DeviceB + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); + rules.rules.push_back({"DeviceB", "RuleB", false}); + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + ep_map["DeviceB"].insert(1); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + rule_map[1] = "DeviceB"; + + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // Verify Parent = 0 + auto assign_parent = index.GetNodeAssignment(graph, if_node.Index()); + ASSERT_TRUE(assign_parent.has_value()); + EXPECT_EQ(*assign_parent, 0u); + + // Verify Sub = 1 (Override) + auto assign_sub = index.GetNodeAssignment(*then_graph, sub_node->Index()); + ASSERT_TRUE(assign_sub.has_value()); + EXPECT_EQ(*assign_sub, 1u); +} + +TEST(LayeringIndexTest, UpdateIndex) { + // 1. Setup Graph with one node + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& node = graph.AddNode("node", "Abs", "Node", {input_arg}, {output_arg}); + ASSERT_STATUS_OK(graph.Resolve()); + + // 2. Setup Rules and Index + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + // Creates index (node has no annotation, so not assigned) + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + EXPECT_FALSE(index.GetNodeAssignment(graph, node.Index()).has_value()); + + // 3. Update Node with Annotation + node.SetLayeringAnnotation("RuleA"); + + // 4. Call Update + std::vector nodes_to_update = {node.Index()}; + index.Update(graph, nodes_to_update); + + // 5. Verify Assignment + auto assignment = index.GetNodeAssignment(graph, node.Index()); + ASSERT_TRUE(assignment.has_value()); + EXPECT_EQ(*assignment, 0u); +} + +TEST(LayeringRulesTest, LayeringRulesParsing) { + // Test empty string + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString("", rules)); + EXPECT_TRUE(rules.rules.empty()); + } + + // Test simple valid string + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(Annotation1)", rules)); + ASSERT_EQ(rules.rules.size(), 1u); + EXPECT_EQ(rules.rules[0].device, "EP1"); + EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); + EXPECT_TRUE(rules.rules[0].prefix_match); + } + + // Test multiple annotations for one device + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(Annotation1, Annotation2)", rules)); + ASSERT_EQ(rules.rules.size(), 2u); + EXPECT_EQ(rules.rules[0].device, "EP1"); + EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); + EXPECT_TRUE(rules.rules[0].prefix_match); + EXPECT_EQ(rules.rules[1].device, "EP1"); + EXPECT_EQ(rules.rules[1].annotation, "Annotation2"); + EXPECT_TRUE(rules.rules[1].prefix_match); + } + + // Test multiple devices + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(Annotation1); EP2(Annotation2)", rules)); + ASSERT_EQ(rules.rules.size(), 2u); + EXPECT_EQ(rules.rules[0].device, "EP1"); + EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); + EXPECT_TRUE(rules.rules[0].prefix_match); + EXPECT_EQ(rules.rules[1].device, "EP2"); + EXPECT_EQ(rules.rules[1].annotation, "Annotation2"); + EXPECT_TRUE(rules.rules[1].prefix_match); + } + + // Test exact match + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(=Annotation1)", rules)); + ASSERT_EQ(rules.rules.size(), 1u); + EXPECT_EQ(rules.rules[0].device, "EP1"); + EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); + EXPECT_FALSE(rules.rules[0].prefix_match); + } + + // Test trimming whitespace + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString(" EP1 ( Annotation1 , =Annotation2 ) ; EP2 ( Annotation3 ) ", rules)); + ASSERT_EQ(rules.rules.size(), 3u); + EXPECT_EQ(rules.rules[0].device, "EP1"); + EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); + EXPECT_TRUE(rules.rules[0].prefix_match); + EXPECT_EQ(rules.rules[1].device, "EP1"); + EXPECT_EQ(rules.rules[1].annotation, "Annotation2"); + EXPECT_FALSE(rules.rules[1].prefix_match); + EXPECT_EQ(rules.rules[2].device, "EP2"); + EXPECT_EQ(rules.rules[2].annotation, "Annotation3"); + EXPECT_TRUE(rules.rules[2].prefix_match); + } +} + +TEST(LayeringRulesTest, FromConfigString_InvalidFormat) { + LayeringRules rules; + + // Error: Missing parentheses structure entirely + EXPECT_FALSE(LayeringRules::FromConfigString("Device1Annotation1", rules).IsOK()); + + // Error: Missing closing parenthesis + EXPECT_FALSE(LayeringRules::FromConfigString("Device1(Annotation1", rules).IsOK()); + + // Error: Missing opening parenthesis (or only closing present) + EXPECT_FALSE(LayeringRules::FromConfigString("Device1Annotation1)", rules).IsOK()); + + // Error: Parentheses reversed + EXPECT_FALSE(LayeringRules::FromConfigString("Device1)Annotation1(", rules).IsOK()); + + // Error: Empty device name (starts with parenthesis) + EXPECT_FALSE(LayeringRules::FromConfigString("(Annotation1)", rules).IsOK()); +} + +TEST(LayeringRulesTest, FromConfigString_IgnoresEmptyEntries) { + LayeringRules rules; + // "; ;" should result in 0 rules but Status::OK + ASSERT_STATUS_OK(LayeringRules::FromConfigString("; ;", rules)); + EXPECT_TRUE(rules.rules.empty()); +} + +TEST(LayeringRulesTest, FromConfigString_RejectsDuplicateAnnotations) { + LayeringRules rules; + + // Duplicate prefix annotation within the same device + EXPECT_FALSE(LayeringRules::FromConfigString("EP1(Ann1, Ann1)", rules).IsOK()); + + // Duplicate prefix annotation across different devices + EXPECT_FALSE(LayeringRules::FromConfigString("EP1(Ann1); EP2(Ann1)", rules).IsOK()); + + // Duplicate exact annotation within the same device + EXPECT_FALSE(LayeringRules::FromConfigString("EP1(=Ann1, =Ann1)", rules).IsOK()); + + // Duplicate exact annotation across different devices + EXPECT_FALSE(LayeringRules::FromConfigString("EP1(=Ann1); EP2(=Ann1)", rules).IsOK()); + + // Same annotation but different match types (prefix vs exact) should be OK + ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(Ann1, =Ann1)", rules)); + ASSERT_EQ(rules.rules.size(), 2u); + EXPECT_TRUE(rules.rules[0].prefix_match); + EXPECT_FALSE(rules.rules[1].prefix_match); +} + +TEST(LayeringIndexTest, MakeNodeUnassigned_PreservesEpRuleMapping) { + // Scenario: All nodes for a rule are unassigned in one graph. + // ep_name_to_layering_indices_ must still contain the rule so that + // sibling subgraphs (or the same graph on a subsequent pass) can still + // use it for filtering. + + // 1. Setup Graph with two nodes, both annotated with the same rule + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + // Create nodes + // Node 0: "AnnotatedNode" -> Annotated with "RuleA" + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* mid_arg = &graph.GetOrCreateNodeArg("mid", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& node0 = graph.AddNode("node0", "Abs", "Node 0", {input_arg}, {mid_arg}); + node0.SetLayeringAnnotation("RuleA"); + Node& node1 = graph.AddNode("node1", "Abs", "Node 1", {mid_arg}, {output_arg}); + node1.SetLayeringAnnotation("RuleA"); + + ASSERT_STATUS_OK(graph.Resolve()); + + // 2. Setup Rules: RuleA -> DeviceA + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + // 3. Create Index + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // Both nodes should be assigned + ASSERT_TRUE(index.GetNodeAssignment(graph, node0.Index()).has_value()); + ASSERT_TRUE(index.GetNodeAssignment(graph, node1.Index()).has_value()); + + // 3. Unassign both nodes (simulating EP failing to claim them) + index.MakeNodeUnassigned(graph, node0.Index()); + index.MakeNodeUnassigned(graph, node1.Index()); + + // Nodes should be unassigned + EXPECT_FALSE(index.GetNodeAssignment(graph, node0.Index()).has_value()); + EXPECT_FALSE(index.GetNodeAssignment(graph, node1.Index()).has_value()); + + // 4. CRITICAL: ep_name_to_layering_indices_ must still map DeviceA -> {0} + // so that other graphs/passes can still use this rule for filtering. + auto rules_opt = index.GetLayeringRulesForThisEp("DeviceA"); + ASSERT_TRUE(rules_opt.has_value()) << "EP-to-rule mapping should not be erased when nodes are unassigned"; + EXPECT_EQ(rules_opt->get().count(0), 1u); +} + +TEST(LayeringIndexTest, UpdateAfterFullUnassignment_RestoresVisibility) { + // Scenario: All nodes for a rule are unassigned, then Update() adds + // a new node matching the same rule. The new node must be visible + // to the EP via GetLayeringRulesForThisEp. + + // 1. Setup Graph with one annotated node + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& node0 = graph.AddNode("node0", "Abs", "Node 0", {input_arg}, {output_arg}); + node0.SetLayeringAnnotation("RuleA"); + ASSERT_STATUS_OK(graph.Resolve()); + + // 2. Setup Rules: RuleA -> DeviceA + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + ASSERT_TRUE(index.GetNodeAssignment(graph, node0.Index()).has_value()); + + // 3. Unassign the only node + index.MakeNodeUnassigned(graph, node0.Index()); + EXPECT_FALSE(index.GetNodeAssignment(graph, node0.Index()).has_value()); + + // 4. Simulate layout transform adding a new node with inherited annotation + NodeArg* new_output_arg = &graph.GetOrCreateNodeArg("new_output", &type_proto); + Node& new_node = graph.AddNode("new_node", "Abs", "Node with inherited assignment", + {output_arg}, {new_output_arg}); + new_node.SetLayeringAnnotation("RuleA"); // Inherits parent's annotation + ASSERT_STATUS_OK(graph.Resolve()); + + // Record the new node index + NodeIndex new_node_index = new_node.Index(); + + // 5. Update index with the new node + std::vector new_nodes = {new_node_index}; + index.Update(graph, new_nodes); + + // 6. New node should be assigned to rule 0 + auto assign = index.GetNodeAssignment(graph, new_node.Index()); + ASSERT_TRUE(assign.has_value()); + EXPECT_EQ(*assign, 0u); + + // 7. CRITICAL: The rule must still be visible for DeviceA + auto rules_opt = index.GetLayeringRulesForThisEp("DeviceA"); + ASSERT_TRUE(rules_opt.has_value()) << "EP-to-rule mapping must be intact for Update to be effective"; + EXPECT_EQ(rules_opt->get().count(0), 1u); +} + +// ============================================================================ +// Tests for graph_partitioner.cc LayeringIndex integration +// These tests exercise behaviors from GetCapabilityForEP, InlineNodes, and +// the partitioning pipeline when a LayeringIndex is present. +// ============================================================================ + +// Helper to create a simple linear graph: input -> node0 -> node1 -> ... -> output +namespace { + +struct SimpleGraphHelper { + std::unique_ptr model; + Graph* graph = nullptr; + std::vector node_indices; + + static SimpleGraphHelper Create(int num_nodes, const std::string& op_type = "Abs") { + SimpleGraphHelper h; + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + h.model = std::make_unique("test_model", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + h.graph = &h.model->MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + + NodeArg* prev_arg = &h.graph->GetOrCreateNodeArg("input", &type_proto); + + for (int i = 0; i < num_nodes; ++i) { + std::string out_name = (i == num_nodes - 1) ? "output" : "mid_" + std::to_string(i); + NodeArg* out_arg = &h.graph->GetOrCreateNodeArg(out_name, &type_proto); + Node& node = h.graph->AddNode("node_" + std::to_string(i), op_type, + "Node " + std::to_string(i), {prev_arg}, {out_arg}); + h.node_indices.push_back(node.Index()); + prev_arg = out_arg; + } + return h; + } +}; + +LayeringIndex CreateTwoEpIndex(const Graph& graph, + const std::string& ep_a, const std::string& annotation_a, + const std::string& ep_b, const std::string& annotation_b) { + LayeringRules rules; + rules.rules.push_back({ep_a, annotation_a, false}); // Index 0 + rules.rules.push_back({ep_b, annotation_b, false}); // Index 1 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map[ep_a].insert(0); + ep_map[ep_b].insert(1); + + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = ep_a; + rule_map[1] = ep_b; + + return LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); +} + +} // namespace + +TEST(LayeringIndexPartitionerTest, FilteredGraphViewerExcludesOtherEpNodes) { + // Validates the filtering logic in create_graph_viewer (GetCapabilityForEP): + // When layering_index is present, nodes assigned to other EPs should be excluded + // from the GraphViewer presented to the current EP. + + // Setup: 3-node chain, node0 -> RuleA (DeviceA), node1 -> unannotated, node2 -> RuleB (DeviceB) + auto h = SimpleGraphHelper::Create(3); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + node0->SetLayeringAnnotation("RuleA"); + node2->SetLayeringAnnotation("RuleB"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); + + // Verify: From DeviceA's perspective, node2 should be excluded + auto rules_a = index.GetLayeringRulesForThisEp("DeviceA"); + ASSERT_TRUE(rules_a.has_value()); + + // node0 should be assigned to rule 0 (DeviceA) + auto assign0 = index.GetNodeAssignment(*h.graph, h.node_indices[0]); + ASSERT_TRUE(assign0.has_value()); + EXPECT_EQ(*assign0, 0u); + + // node1 should be unassigned (available to any EP) + auto assign1 = index.GetNodeAssignment(*h.graph, h.node_indices[1]); + EXPECT_FALSE(assign1.has_value()); + + // node2 should be assigned to rule 1 (DeviceB) + auto assign2 = index.GetNodeAssignment(*h.graph, h.node_indices[2]); + ASSERT_TRUE(assign2.has_value()); + EXPECT_EQ(*assign2, 1u); + + // Simulate the filtering logic from create_graph_viewer: + // For DeviceA: include nodes with no assignment OR assignment in DeviceA's rules + InlinedVector filtered_for_device_a; + for (auto& node : h.graph->Nodes()) { + auto rule_idx_opt = index.GetNodeAssignment(*h.graph, node.Index()); + bool include = true; + if (rule_idx_opt) { + // Node has assignment - include only if it belongs to DeviceA + if (rules_a->get().count(*rule_idx_opt) == 0) { + include = false; + } + } + if (include) { + filtered_for_device_a.push_back(&node); + } + } + + // DeviceA should see node0 (assigned to it) and node1 (unassigned), but NOT node2 + EXPECT_EQ(filtered_for_device_a.size(), 2u); + bool found_node0 = false, found_node1 = false, found_node2 = false; + for (const auto* n : filtered_for_device_a) { + if (n->Index() == h.node_indices[0]) found_node0 = true; + if (n->Index() == h.node_indices[1]) found_node1 = true; + if (n->Index() == h.node_indices[2]) found_node2 = true; + } + EXPECT_TRUE(found_node0) << "DeviceA's assigned node should be included"; + EXPECT_TRUE(found_node1) << "Unassigned node should be included for any EP"; + EXPECT_FALSE(found_node2) << "DeviceB's assigned node should be excluded from DeviceA's view"; +} + +TEST(LayeringIndexPartitionerTest, FilteredGraphViewerForDeviceBExcludesDeviceANodes) { + // Mirror of the above test but from DeviceB's perspective. + + auto h = SimpleGraphHelper::Create(3); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + node0->SetLayeringAnnotation("RuleA"); + node2->SetLayeringAnnotation("RuleB"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); + + auto rules_b = index.GetLayeringRulesForThisEp("DeviceB"); + ASSERT_TRUE(rules_b.has_value()); + + // Simulate filtering for DeviceB + InlinedVector filtered_for_device_b; + for (auto& node : h.graph->Nodes()) { + auto rule_idx_opt = index.GetNodeAssignment(*h.graph, node.Index()); + bool include = true; + if (rule_idx_opt) { + if (rules_b->get().count(*rule_idx_opt) == 0) { + include = false; + } + } + if (include) { + filtered_for_device_b.push_back(&node); + } + } + + // DeviceB should see node1 (unassigned) and node2 (assigned to it), but NOT node0 + EXPECT_EQ(filtered_for_device_b.size(), 2u); + bool found_node0 = false, found_node1 = false, found_node2 = false; + for (const auto* n : filtered_for_device_b) { + if (n->Index() == h.node_indices[0]) found_node0 = true; + if (n->Index() == h.node_indices[1]) found_node1 = true; + if (n->Index() == h.node_indices[2]) found_node2 = true; + } + EXPECT_FALSE(found_node0) << "DeviceA's assigned node should be excluded from DeviceB's view"; + EXPECT_TRUE(found_node1) << "Unassigned node should be included for any EP"; + EXPECT_TRUE(found_node2) << "DeviceB's assigned node should be included"; +} + +TEST(LayeringIndexPartitionerTest, ResetUnclaimedNodesRemovesAssignment) { + // Validates the reset_assignment_unclaimed_nodes logic: + // Nodes that were pre-assigned to an EP via layering but NOT claimed in capabilities + // should be unassigned so subsequent EPs can pick them up. + + auto h = SimpleGraphHelper::Create(4); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node1 = h.graph->GetNode(h.node_indices[1]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + + node0->SetLayeringAnnotation("RuleA"); + node1->SetLayeringAnnotation("RuleA"); + node2->SetLayeringAnnotation("RuleA"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + auto index = LayeringIndex::Create(*h.graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // All 3 nodes should be assigned initially + ASSERT_TRUE(index.GetNodeAssignment(*h.graph, h.node_indices[0]).has_value()); + ASSERT_TRUE(index.GetNodeAssignment(*h.graph, h.node_indices[1]).has_value()); + ASSERT_TRUE(index.GetNodeAssignment(*h.graph, h.node_indices[2]).has_value()); + + // Simulate: EP only claims node0 and node2 (not node1) + InlinedHashSet claimed; + claimed.insert(h.node_indices[0]); + claimed.insert(h.node_indices[2]); + + auto ep_rules_opt = index.GetLayeringRulesForThisEp("DeviceA"); + ASSERT_TRUE(ep_rules_opt.has_value()); + const auto& ep_rules = ep_rules_opt->get(); + + // Replicate reset_assignment_unclaimed_nodes logic: + // For each assigned-filtered-in node, if not claimed, unassign it + std::vector assigned_filtered_in = {h.node_indices[0], h.node_indices[1], h.node_indices[2]}; + for (auto node_index : assigned_filtered_in) { + if (claimed.count(node_index) == 0) { + auto rule_idx_opt = index.GetNodeAssignment(*h.graph, node_index); + if (rule_idx_opt && ep_rules.count(*rule_idx_opt) > 0) { + index.MakeNodeUnassigned(*h.graph, node_index); + } + } + } + + // node0 and node2 should still be assigned + EXPECT_TRUE(index.GetNodeAssignment(*h.graph, h.node_indices[0]).has_value()); + EXPECT_TRUE(index.GetNodeAssignment(*h.graph, h.node_indices[2]).has_value()); + // node1 should be unassigned (not claimed by EP) + EXPECT_FALSE(index.GetNodeAssignment(*h.graph, h.node_indices[1]).has_value()); +} + +TEST(LayeringIndexPartitionerTest, UpdateAfterLayoutTransformAddsNewNodes) { + // Validates the LayeringIndex update after layout transformation creates new nodes. + // In GetCapabilityForEP, after layout transform, new nodes with inherited annotations + // are added and the index is updated. + + auto h = SimpleGraphHelper::Create(1); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + node0->SetLayeringAnnotation("RuleA"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); + + // Record the max node index before "layout transformation" + const NodeIndex first_new_node = h.graph->MaxNodeIndex(); + + // Simulate layout transformation adding new nodes with inherited annotation + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* extra_out = &h.graph->GetOrCreateNodeArg("extra_output", &type_proto); + NodeArg* output_arg = &h.graph->GetOrCreateNodeArg("output", nullptr); // reuse existing + Node& new_node = h.graph->AddNode("new_node", "Abs", "Node with inherited annotation", + {output_arg}, {extra_out}); + new_node.SetLayeringAnnotation("RuleA"); // Inherits parent's annotation + ASSERT_STATUS_OK(h.graph->Resolve()); + + const NodeIndex end_node = h.graph->MaxNodeIndex(); + + // Collect new node indices (as done in graph_partitioner.cc) + InlinedVector new_node_indices; + for (NodeIndex idx = first_new_node; idx < end_node; ++idx) { + if (h.graph->GetNode(idx) != nullptr) { + new_node_indices.push_back(idx); + } + } + + // Update index + ASSERT_FALSE(new_node_indices.empty()); + index.Update(*h.graph, new_node_indices); + + // New node should be assigned to rule 0 (DeviceA) + auto assign = index.GetNodeAssignment(*h.graph, new_node.Index()); + ASSERT_TRUE(assign.has_value()); + EXPECT_EQ(*assign, 0u); + + // And the annotation string should be on the node + EXPECT_EQ(new_node.GetLayeringAnnotation(), "RuleA"); +} + +TEST(LayeringIndexPartitionerTest, UpdateWithUnannotatedNewNodeRemainsUnassigned) { + // New nodes created by layout transform that do NOT have annotations + // should remain unassigned after Update. + + auto h = SimpleGraphHelper::Create(1); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + node0->SetLayeringAnnotation("RuleA"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); + + // Add a new node WITHOUT annotation + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* extra_out = &h.graph->GetOrCreateNodeArg("extra_output", &type_proto); + NodeArg* output_arg = &h.graph->GetOrCreateNodeArg("output", nullptr); + Node& new_node = h.graph->AddNode("unannotated_node", "Abs", "No annotation", + {output_arg}, {extra_out}); + // Deliberately NOT setting annotation + ASSERT_STATUS_OK(h.graph->Resolve()); + + std::vector new_nodes = {new_node.Index()}; + index.Update(*h.graph, new_nodes); + + // New node should remain unassigned + auto assign = index.GetNodeAssignment(*h.graph, new_node.Index()); + EXPECT_FALSE(assign.has_value()); +} + +TEST(LayeringIndexPartitionerTest, InlineAnnotationMaterialization) { + // Validates the InlineNodes logic where a node has an inherited-only assignment + // (no explicit annotation string) and the annotation is materialized before inlining. + // This tests the code path: + // if (layering_index != nullptr && !has_explicit_annotation) { + // auto rule_idx = layering_index->GetNodeAssignment(graph, node->Index()); + // if (rule_idx) { ... node->SetLayeringAnnotation(rules.rules[*rule_idx].annotation); } + // } + + // Setup: A graph where a node is assigned via inheritance (subgraph scenario) + // but has no explicit annotation string on it. + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + // Create a node without explicit annotation + Node& node = graph.AddNode("inherited_node", "Abs", "Node with inherited assignment", + {input_arg}, {output_arg}); + ASSERT_STATUS_OK(graph.Resolve()); + + // Create index where the node is somehow assigned (e.g., through inheritance) + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // The node has no annotation, so it shouldn't be assigned yet + ASSERT_TRUE(node.GetLayeringAnnotation().empty()); + EXPECT_FALSE(index.GetNodeAssignment(graph, node.Index()).has_value()); + + // Now simulate what InlineNodes does: manually annotate and update + // This simulates the case where GetNodeAssignment returns a value + // for a node in a subgraph that inherited its parent's assignment. + node.SetLayeringAnnotation("RuleA"); + std::vector updated = {node.Index()}; + index.Update(graph, updated); + + // After materialization + update, the node should be properly assigned + auto assign = index.GetNodeAssignment(graph, node.Index()); + ASSERT_TRUE(assign.has_value()); + EXPECT_EQ(*assign, 0u); + + // And the annotation string should be on the node + EXPECT_EQ(node.GetLayeringAnnotation(), "RuleA"); +} + +TEST(LayeringIndexPartitionerTest, UpdateBatchMultipleNewAnnotatedNodes) { + // Tests that Update correctly handles a batch of multiple new nodes, + // some annotated with different rules. This mirrors the behavior after + // layout transformation creates several new nodes. + + auto h = SimpleGraphHelper::Create(1); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + node0->SetLayeringAnnotation("RuleA"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + + // Add 3 new nodes: one for RuleA, one for RuleB, one unannotated + NodeArg* out1 = &h.graph->GetOrCreateNodeArg("new_out1", &type_proto); + NodeArg* out2 = &h.graph->GetOrCreateNodeArg("new_out2", &type_proto); + NodeArg* out3 = &h.graph->GetOrCreateNodeArg("new_out3", &type_proto); + NodeArg* output = &h.graph->GetOrCreateNodeArg("output", nullptr); + + Node& new_a = h.graph->AddNode("new_a", "Abs", "", {output}, {out1}); + new_a.SetLayeringAnnotation("RuleA"); + + Node& new_b = h.graph->AddNode("new_b", "Abs", "", {out1}, {out2}); + new_b.SetLayeringAnnotation("RuleB"); + + Node& new_none = h.graph->AddNode("new_none", "Abs", "", {out2}, {out3}); + // No annotation + + ASSERT_STATUS_OK(h.graph->Resolve()); + + std::vector new_nodes = {new_a.Index(), new_b.Index(), new_none.Index()}; + index.Update(*h.graph, new_nodes); + + // new_a -> RuleA -> rule index 0 + auto assign_a = index.GetNodeAssignment(*h.graph, new_a.Index()); + ASSERT_TRUE(assign_a.has_value()); + EXPECT_EQ(*assign_a, 0u); + + // new_b -> RuleB -> rule index 1 + auto assign_b = index.GetNodeAssignment(*h.graph, new_b.Index()); + ASSERT_TRUE(assign_b.has_value()); + EXPECT_EQ(*assign_b, 1u); + + // new_none -> unassigned + auto assign_none = index.GetNodeAssignment(*h.graph, new_none.Index()); + EXPECT_FALSE(assign_none.has_value()); +} + +TEST(LayeringIndexPartitionerTest, MakeUnassignedThenReassignViaPrefixRule) { + // Test that prefix rules work correctly after unassign+update cycle. + // This covers the interaction between MakeNodeUnassigned, prefix matching, + // and Update. + + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& node = graph.AddNode("node", "Abs", "Node", {input_arg}, {output_arg}); + node.SetLayeringAnnotation("Layer_GPU_Compute"); + ASSERT_STATUS_OK(graph.Resolve()); + + // Prefix rule: "Layer_GPU" matches "Layer_GPU_Compute" + LayeringRules rules; + rules.rules.push_back({"GPUDevice", "Layer_GPU", true}); // Index 0, prefix match + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["GPUDevice"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "GPUDevice"; + + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // Node should be assigned via prefix match + auto assign = index.GetNodeAssignment(graph, node.Index()); + ASSERT_TRUE(assign.has_value()); + EXPECT_EQ(*assign, 0u); + + // Unassign the node + index.MakeNodeUnassigned(graph, node.Index()); + EXPECT_FALSE(index.GetNodeAssignment(graph, node.Index()).has_value()); + + // Add a new node with a different annotation that also matches the prefix + NodeArg* new_out = &graph.GetOrCreateNodeArg("new_output", &type_proto); + Node& new_node = graph.AddNode("new_node", "Abs", "Node with inherited annotation", + {output_arg}, {new_out}); + new_node.SetLayeringAnnotation("Layer_GPU_Memory"); + ASSERT_STATUS_OK(graph.Resolve()); + + std::vector new_nodes = {new_node.Index()}; + index.Update(graph, new_nodes); + + // New node should also be assigned via prefix match + auto new_assign = index.GetNodeAssignment(graph, new_node.Index()); + ASSERT_TRUE(new_assign.has_value()); + EXPECT_EQ(*new_assign, 0u); +} + +TEST(LayeringIndexPartitionerTest, NoLayeringIndexAllNodesVisible) { + // When layering_index is nullptr (no layering configuration), + // all nodes should be visible to all EPs. This verifies the baseline + // behavior that the filtering code path is only active when layering is enabled. + + auto h = SimpleGraphHelper::Create(3); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + + // Even if nodes have annotations, without a LayeringIndex, everything is visible + node0->SetLayeringAnnotation("RuleA"); + node2->SetLayeringAnnotation("RuleB"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + // Without LayeringIndex, a standard GraphViewer should see all nodes + GraphViewer viewer(*h.graph); + EXPECT_EQ(viewer.NumberOfNodes(), 3); + + // All nodes accessible + EXPECT_NE(viewer.GetNode(h.node_indices[0]), nullptr); + EXPECT_NE(viewer.GetNode(h.node_indices[1]), nullptr); + EXPECT_NE(viewer.GetNode(h.node_indices[2]), nullptr); +} + +TEST(LayeringIndexPartitionerTest, EpWithNoLayeringRulesSeesAllUnassignedNodes) { + // An EP that has no rules in the LayeringIndex (i.e., GetLayeringRulesForThisEp returns nullopt) + // should still see unassigned nodes, but nodes assigned to other EPs are excluded. + // This is the behavior for a CPU fallback EP not mentioned in layering config, + // as implemented in graph_partitioner.cc create_graph_viewer: + // if (!rules_opt || rules_opt->get().count(*rule_idx_opt) == 0) { include = false; } + + auto h = SimpleGraphHelper::Create(4); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + node0->SetLayeringAnnotation("RuleA"); + node2->SetLayeringAnnotation("RuleB"); + // node1 and node3 are unannotated + ASSERT_STATUS_OK(h.graph->Resolve()); + + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); + + // "CPUDevice" has no rules in the index + auto rules_cpu = index.GetLayeringRulesForThisEp("CPUDevice"); + EXPECT_FALSE(rules_cpu.has_value()); + + // Replicate create_graph_viewer filtering logic for an EP with no rules. + // When rules_opt is nullopt, any node with an assignment is excluded: + // if (!rules_opt || ...) { include = false; } + // Unassigned nodes remain included. + InlinedVector filtered_for_cpu; + for (auto& node : h.graph->Nodes()) { + auto rule_idx_opt = index.GetNodeAssignment(*h.graph, node.Index()); + bool include = true; + if (rule_idx_opt) { + if (!rules_cpu || rules_cpu->get().count(*rule_idx_opt) == 0) { + include = false; + } + } + if (include) { + filtered_for_cpu.push_back(&node); + } + } + + // CPUDevice should see only the 2 unassigned nodes (node1, node3). + // node0 (RuleA/DeviceA) and node2 (RuleB/DeviceB) are excluded. + EXPECT_EQ(filtered_for_cpu.size(), 2u); + + bool found[4] = {}; + for (const auto* n : filtered_for_cpu) { + for (size_t i = 0; i < std::size(found); ++i) { + if (n->Index() == h.node_indices[i]) found[i] = true; + } + } + EXPECT_FALSE(found[0]) << "node0 assigned to DeviceA should be excluded"; + EXPECT_TRUE(found[1]) << "node1 unassigned should be included"; + EXPECT_FALSE(found[2]) << "node2 assigned to DeviceB should be excluded"; + EXPECT_TRUE(found[3]) << "node3 unassigned should be included"; +} +TEST(LayeringIndexPartitionerTest, MultipleRulesForSameEp) { + // An EP can have multiple rules assigned to it. All nodes matching any of its + // rules should be visible to it, while nodes matching other EP rules should not. + + auto h = SimpleGraphHelper::Create(4); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node1 = h.graph->GetNode(h.node_indices[1]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + + node0->SetLayeringAnnotation("RuleA1"); + node1->SetLayeringAnnotation("RuleA2"); + node2->SetLayeringAnnotation("RuleB"); + // node3 unannotated + ASSERT_STATUS_OK(h.graph->Resolve()); + + // DeviceA has two rules: RuleA1 (index 0) and RuleA2 (index 1) + // DeviceB has one rule: RuleB (index 2) + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA1", false}); // Index 0 + rules.rules.push_back({"DeviceA", "RuleA2", false}); // Index 1 + rules.rules.push_back({"DeviceB", "RuleB", false}); // Index 2 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + ep_map["DeviceA"].insert(1); + ep_map["DeviceB"].insert(2); + + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + rule_map[1] = "DeviceA"; + rule_map[2] = "DeviceB"; + + auto index = LayeringIndex::Create(*h.graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + auto rules_a = index.GetLayeringRulesForThisEp("DeviceA"); + ASSERT_TRUE(rules_a.has_value()); + EXPECT_EQ(rules_a->get().size(), 2u); // Both rule indices 0 and 1 + + // Simulate filtering for DeviceA + InlinedVector filtered_for_a; + for (auto& node : h.graph->Nodes()) { + auto rule_idx_opt = index.GetNodeAssignment(*h.graph, node.Index()); + bool include = true; + if (rule_idx_opt) { + if (rules_a->get().count(*rule_idx_opt) == 0) { + include = false; + } + } + if (include) { + filtered_for_a.push_back(&node); + } + } + + // DeviceA should see node0, node1 (both its rules), and node3 (unassigned) = 3 nodes + // node2 (RuleB/DeviceB) should be excluded + EXPECT_EQ(filtered_for_a.size(), 3u); + + bool found[4] = {}; + for (const auto* n : filtered_for_a) { + for (int i = 0; i < 4; ++i) { + if (n->Index() == h.node_indices[i]) found[i] = true; + } + } + EXPECT_TRUE(found[0]); // node0 - RuleA1 + EXPECT_TRUE(found[1]); // node1 - RuleA2 + EXPECT_FALSE(found[2]); // node2 - RuleB (excluded) + EXPECT_TRUE(found[3]); // node3 - unassigned +} + +} // namespace test +} // namespace onnxruntime + +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) \ No newline at end of file diff --git a/onnxruntime/test/framework/resource_accountant_test.cc b/onnxruntime/test/framework/resource_accountant_test.cc new file mode 100644 index 0000000000000..a102fe4e7770b --- /dev/null +++ b/onnxruntime/test/framework/resource_accountant_test.cc @@ -0,0 +1,327 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "core/framework/resource_accountant.h" +#include "core/graph/indexed_sub_graph.h" +#include "core/graph/constants.h" +#include "core/graph/model.h" + +#include "gtest/gtest.h" + +#include "test/util/include/asserts.h" +#include "test/util/include/test_environment.h" + +namespace onnxruntime { +namespace test { + +// Test accountant mimicking SizeBasedStatsAccountant ad-hoc path: +// Uses pending/committed weight sets so that: +// - Within a GetCapability pass, shared weights are deduped +// - Across passes, only committed weights persist and pending are discarded +class TestDedupAccountant : public IResourceAccountant { + public: + TestDedupAccountant() = default; + + ResourceCount GetConsumedAmount() const override { + return consumed_; + } + + void AddConsumedAmount(const ResourceCount& amount) noexcept override { + if (std::holds_alternative(amount)) { + consumed_ += std::get(amount); + } + } + + void RemoveConsumedAmount(const ResourceCount& amount) noexcept override { + if (std::holds_alternative(amount)) { + consumed_ -= std::get(amount); + } + } + + ResourceCount ComputeResourceCount(const Node& node) override { + const auto* graph = node.GetContainingGraph(); + if (graph == nullptr) { + return static_cast(0); + } + + size_t total = 0; + for (const auto* input_def : node.InputDefs()) { + if (!input_def->Exists()) { + continue; + } + const auto& name = input_def->Name(); + constexpr bool check_outer_scope = true; + const auto* init = graph->GetInitializer(name, check_outer_scope); + if (init != nullptr) { + if (committed_weights_.count(name) > 0) { + continue; + } + if (pending_weights_.count(name) > 0) { + continue; + } + auto it = weight_sizes_.find(name); + if (it != weight_sizes_.end()) { + total += it->second; + } + pending_weights_.insert(name); + pending_weights_by_node_[node.Index()].insert(name); + } + } + return total; + } + + void ResetPendingWeights() override { + pending_weights_.clear(); + pending_weights_by_node_.clear(); + } + + void CommitWeightsForNode(NodeIndex node_index) override { + auto it = pending_weights_by_node_.find(node_index); + if (it != pending_weights_by_node_.end()) { + for (const auto& name : it->second) { + pending_weights_.erase(name); + } + committed_weights_.insert(it->second.begin(), it->second.end()); + pending_weights_by_node_.erase(it); + } + } + + void RegisterWeight(const std::string& name, size_t size) { + weight_sizes_[name] = size; + } + + size_t GetConsumedSizeT() const { return consumed_; } + + private: + size_t consumed_ = 0; + InlinedHashSet committed_weights_; + InlinedHashSet pending_weights_; + InlinedHashMap> pending_weights_by_node_; + InlinedHashMap weight_sizes_; +}; + +// Two Add nodes that share a single initializer weight_W. +struct SharedWeightGraph { + std::unique_ptr model; + Graph* graph = nullptr; + Node* node_a = nullptr; + Node* node_b = nullptr; + + static SharedWeightGraph Create() { + SharedWeightGraph h; + std::unordered_map dom; + dom[kOnnxDomain] = 12; + h.model = std::make_unique( + "test_model", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), dom, + std::vector(), + DefaultLoggingManager().DefaultLogger()); + h.graph = &h.model->MainGraph(); + + ONNX_NAMESPACE::TypeProto ft; + ft.mutable_tensor_type()->set_elem_type( + ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + ft.mutable_tensor_type()->mutable_shape()->add_dim()->set_dim_value(250); + + ONNX_NAMESPACE::TensorProto wp; + wp.set_name("weight_W"); + wp.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + wp.add_dims(250); + for (int i = 0; i < 250; ++i) { + wp.add_float_data(0.0f); + } + h.graph->AddInitializedTensor(wp); + + auto* ia = &h.graph->GetOrCreateNodeArg("input_a", &ft); + auto* ib = &h.graph->GetOrCreateNodeArg("input_b", &ft); + auto* wa = &h.graph->GetOrCreateNodeArg("weight_W", &ft); + auto* oa = &h.graph->GetOrCreateNodeArg("out_a", &ft); + auto* ob = &h.graph->GetOrCreateNodeArg("out_b", &ft); + + h.node_a = &h.graph->AddNode("node_A", "Add", "A", {ia, wa}, {oa}); + h.node_b = &h.graph->AddNode("node_B", "Add", "B", {ib, wa}, {ob}); + + auto status = h.graph->Resolve(); + ORT_ENFORCE(status.IsOK(), status.ErrorMessage()); + return h; + } +}; + +// Regression: AccountForAllNodes sums pre-stored per-node costs +// that already have correct within-pass weight deduplication. +TEST(ResourceAccountantTest, AccountForAllNodes_CorrectlyUsesPreStoredCosts) { + auto h = SharedWeightGraph::Create(); + TestDedupAccountant accountant; + accountant.RegisterWeight("weight_W", 1000); + + IndexedSubGraph sub_graph; + sub_graph.nodes.push_back(h.node_a->Index()); + sub_graph.nodes.push_back(h.node_b->Index()); + sub_graph.SetAccountant(&accountant); + + auto cost_a = accountant.ComputeResourceCount(*h.node_a); + sub_graph.AppendNodeCost(cost_a); + EXPECT_EQ(std::get(cost_a), size_t{1000}); + + auto cost_b = accountant.ComputeResourceCount(*h.node_b); + sub_graph.AppendNodeCost(cost_b); + EXPECT_EQ(std::get(cost_b), size_t{0}); + + ASSERT_TRUE(sub_graph.IsAccountingEnabled()); + sub_graph.AccountForAllNodes(); + + EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{1000}) + << "AccountForAllNodes should sum pre-stored costs (1000 + 0)"; +} + +// Verifies that ResetPendingWeights + re-probe produces correct results. +// After probing (which only writes to pending), resetting pending and +// re-probing should see the full weight cost again since nothing was committed. +TEST(ResourceAccountantTest, ComputeAndAccountForNode_CorrectAfterReset) { + auto h = SharedWeightGraph::Create(); + TestDedupAccountant accountant; + accountant.RegisterWeight("weight_W", 1000); + + // Probing pass populates pending weights + auto cost_a = accountant.ComputeResourceCount(*h.node_a); + EXPECT_EQ(std::get(cost_a), size_t{1000}); + auto cost_b = accountant.ComputeResourceCount(*h.node_b); + EXPECT_EQ(std::get(cost_b), size_t{0}); + + // Discard the pass (simulating capabilities.clear() before second GetCapability) + accountant.ResetPendingWeights(); + + // Re-probe: weight_W was never committed, so it should be counted again + IndexedSubGraph sub_graph; + sub_graph.nodes.push_back(h.node_a->Index()); + sub_graph.SetAccountant(&accountant); + auto recomputed_cost = accountant.ComputeResourceCount(*h.node_a); + sub_graph.AccountForNode(h.node_a->Index(), recomputed_cost); + + EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{1000}) + << "After ResetPendingWeights, re-probe should see full weight cost"; +} + +// Each node has a unique initializer. AccountForAllNodes sums both. +TEST(ResourceAccountantTest, AccountForAllNodes_NoSharedWeights) { + std::unordered_map dom; + dom[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), dom, + std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto ft; + ft.mutable_tensor_type()->set_elem_type( + ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + ft.mutable_tensor_type()->mutable_shape()->add_dim()->set_dim_value(100); + + const char* names[] = {"weight_1", "weight_2"}; + for (const char* wn : names) { + ONNX_NAMESPACE::TensorProto tp; + tp.set_name(wn); + tp.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + tp.add_dims(100); + for (int i = 0; i < 100; ++i) { + tp.add_float_data(0.0f); + } + graph.AddInitializedTensor(tp); + } + + auto* input = &graph.GetOrCreateNodeArg("input", &ft); + auto* w1 = &graph.GetOrCreateNodeArg("weight_1", &ft); + auto* w2 = &graph.GetOrCreateNodeArg("weight_2", &ft); + auto* out1 = &graph.GetOrCreateNodeArg("out1", &ft); + auto* out2 = &graph.GetOrCreateNodeArg("out2", &ft); + + auto& node1 = graph.AddNode("n1", "Add", "", {input, w1}, {out1}); + auto& node2 = graph.AddNode("n2", "Add", "", {out1, w2}, {out2}); + ASSERT_STATUS_OK(graph.Resolve()); + + TestDedupAccountant accountant; + accountant.RegisterWeight("weight_1", 400); + accountant.RegisterWeight("weight_2", 600); + + IndexedSubGraph sub_graph; + sub_graph.nodes.push_back(node1.Index()); + sub_graph.nodes.push_back(node2.Index()); + sub_graph.SetAccountant(&accountant); + + sub_graph.AppendNodeCost(accountant.ComputeResourceCount(node1)); + sub_graph.AppendNodeCost(accountant.ComputeResourceCount(node2)); + + ASSERT_TRUE(sub_graph.IsAccountingEnabled()); + sub_graph.AccountForAllNodes(); + + EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{1000}) + << "No shared weights: should sum all costs (400 + 600)"; +} + +// AccountForNode per-node and AccountForAllNodes bulk produce same result. +TEST(ResourceAccountantTest, AccountForNode_MatchesAccountForAllNodes) { + auto h = SharedWeightGraph::Create(); + + // Per-node path + TestDedupAccountant acc1; + acc1.RegisterWeight("weight_W", 1000); + IndexedSubGraph sub1; + sub1.nodes.push_back(h.node_a->Index()); + sub1.nodes.push_back(h.node_b->Index()); + sub1.SetAccountant(&acc1); + sub1.AppendNodeCost(acc1.ComputeResourceCount(*h.node_a)); + sub1.AppendNodeCost(acc1.ComputeResourceCount(*h.node_b)); + sub1.AccountForNode(0); + sub1.AccountForNode(1); + size_t per_node = acc1.GetConsumedSizeT(); + + // Bulk path + TestDedupAccountant acc2; + acc2.RegisterWeight("weight_W", 1000); + IndexedSubGraph sub2; + sub2.nodes.push_back(h.node_a->Index()); + sub2.nodes.push_back(h.node_b->Index()); + sub2.SetAccountant(&acc2); + sub2.AppendNodeCost(acc2.ComputeResourceCount(*h.node_a)); + sub2.AppendNodeCost(acc2.ComputeResourceCount(*h.node_b)); + sub2.AccountForAllNodes(); + size_t bulk = acc2.GetConsumedSizeT(); + + EXPECT_EQ(per_node, bulk) + << "Per-node and bulk should produce identical results"; + EXPECT_EQ(per_node, size_t{1000}); +} + +// Cross-subgraph dedup: EP1 commits node_A, EP2 probes node_B and +// correctly sees weight_W as already accounted. +TEST(ResourceAccountantTest, CrossSubGraph_DedupWorks) { + auto h = SharedWeightGraph::Create(); + TestDedupAccountant accountant; + accountant.RegisterWeight("weight_W", 1000); + + // EP1 probes and commits node_A + IndexedSubGraph sub1; + sub1.nodes.push_back(h.node_a->Index()); + sub1.SetAccountant(&accountant); + sub1.AppendNodeCost(accountant.ComputeResourceCount(*h.node_a)); + sub1.AccountForNode(0); + EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{1000}); + + // EP2 probes node_B: weight_W already committed + auto cost_b = accountant.ComputeResourceCount(*h.node_b); + EXPECT_EQ(std::get(cost_b), size_t{0}) + << "weight_W was committed by EP1, should be deduped for EP2"; + + // EP2 commits node_B with cost 0 + IndexedSubGraph sub2; + sub2.nodes.push_back(h.node_b->Index()); + sub2.SetAccountant(&accountant); + sub2.AppendNodeCost(cost_b); + sub2.AccountForNode(0); + + EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{1000}) + << "Total should still be 1000 - weight_W counted once across both"; +} + +} // namespace test +} // namespace onnxruntime diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index ed2b98e5280b5..656b0ef86289d 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#include #include #include @@ -9,9 +10,11 @@ #include "core/framework/execution_providers.h" #include "core/framework/graph_partitioner.h" #include "core/framework/kernel_registry.h" +#include "core/framework/layering_annotations.h" #include "core/framework/op_kernel.h" #include "core/framework/bfc_arena.h" #include "core/framework/ep_context_options.h" +#include "core/framework/resource_accountant.h" #include "core/framework/session_state.h" #include "core/graph/graph_utils.h" #include "core/graph/graph_viewer.h" @@ -280,7 +283,7 @@ TEST_P(SessionStateTestP, TestInitializerProcessing) { graph, modified, execution_provider, std::move(cpu_allocator), debug_graph_fn); }, sess_options.config_options, - DefaultLoggingManager().DefaultLogger())); + DefaultLoggingManager().DefaultLogger(), nullptr /*layering_index*/)); ASSERT_STATUS_OK(session_state.FinalizeSessionState(oss.str(), krm)); @@ -367,7 +370,8 @@ TEST(SessionStateTest, TestInitializerMemoryAllocatedUsingNonArenaMemory) { cpu_allocator, debug_graph_fn); }, sess_options.config_options, - default_logger)); + default_logger, + nullptr /*layering_index*/)); EXPECT_STATUS_OK(session_state.FinalizeSessionState(model.ModelPath(), krm)); @@ -414,9 +418,50 @@ namespace { using ParitionVerifierFn = std::function; +// Collect unique node names from a graph and all its subgraphs +// using the same naming scheme as the resource accountant. +static void CollectNodeNames(const Graph& graph, std::vector& names) { + for (const auto& node : graph.Nodes()) { + names.push_back(IResourceAccountant::MakeUniqueNodeName(node)); + for (const auto& [_, subgraph] : node.GetAttributeNameToSubgraphMap()) { + CollectNodeNames(*subgraph, names); + } + } +} + +// Generates a node stats file dynamically from the current graph, +// assigning each node a fixed cost. Returns the total cost across +// all nodes so callers can choose a threshold relative to the actual total. +// This avoids relying on a pre-baked stats file whose node name hashes +// become stale when graph optimizers change node input/output names. +static void GenerateDynamicNodeStatsFile(const ORTCHAR_T* model_path, + const std::filesystem::path& output_path, + size_t& total_cost, + size_t cost_per_node = 1024) { + const auto& default_logger = DefaultLoggingManager().DefaultLogger(); + std::shared_ptr model; + ASSERT_STATUS_OK(Model::Load(model_path, model, nullptr, default_logger)); + Graph& graph = model->MainGraph(); + ASSERT_STATUS_OK(graph.Resolve()); + + std::vector node_names; + CollectNodeNames(graph, node_names); + + std::ofstream ofs(output_path); + ASSERT_TRUE(ofs.is_open()); + ofs << "#name,input_sizes,initializers_sizes,total_dynamic_sizes,total_temp_allocations\n"; + for (const auto& name : node_names) { + ofs << name << "," << cost_per_node << ",0,0,0\n"; + } + ofs.close(); + + total_cost = node_names.size() * cost_per_node; +} + void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, const SessionOptions& sess_options, - const ParitionVerifierFn& verifier_fn) { + const ParitionVerifierFn& verifier_fn, + const std::string& layering_config = std::string()) { const auto& log_manager = DefaultLoggingManager(); log_manager.SetDefaultLoggerSeverity(onnxruntime::logging::Severity::kVERBOSE); const auto& default_logger = log_manager.DefaultLogger(); @@ -431,9 +476,12 @@ void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, auto tp = concurrency::CreateThreadPool(&onnxruntime::Env::Default(), to, concurrency::ThreadPoolType::INTRA_OP); ExecutionProviders execution_providers; - auto tmp_cpu_execution_provider = DefaultCudaExecutionProvider(); - tmp_cpu_execution_provider->SetLogger(&default_logger); - ASSERT_STATUS_OK(execution_providers.Add(kCudaExecutionProvider, std::move(tmp_cpu_execution_provider))); + auto tmp_execution_provider = DefaultCudaExecutionProvider(); + tmp_execution_provider->SetLogger(&default_logger); + ASSERT_STATUS_OK(execution_providers.Add(kCudaExecutionProvider, std::move(tmp_execution_provider))); + tmp_execution_provider = DefaultCpuExecutionProvider(); + tmp_execution_provider->SetLogger(&default_logger); + ASSERT_STATUS_OK(execution_providers.Add(kCpuExecutionProvider, std::move(tmp_execution_provider))); KernelRegistryManager krm; ASSERT_STATUS_OK(krm.RegisterKernels(execution_providers)); @@ -445,6 +493,16 @@ void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, SessionState session_state(model->MainGraph(), execution_providers, tp.get(), nullptr, dtm, edlm, default_logger, profiler, sess_options); + LayeringIndex* layering_index = nullptr; + std::optional layering_index_storage; + if (!layering_config.empty()) { + ASSERT_STATUS_OK(LayeringIndex::Create(graph, layering_config, {}, execution_providers, + default_logger, layering_index_storage)); + if (layering_index_storage.has_value()) { + layering_index = &layering_index_storage.value(); + } + } + // Create GraphOptimizerRegistry instance for providing predefined graph optimizers and selection functions for EPs to lookup auto graph_optimizer_registry = std::make_unique(&sess_options, execution_providers.Get(onnxruntime::kCpuExecutionProvider), @@ -455,7 +513,8 @@ void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, layout_transformation::DebugGraphFn debug_graph_fn; ASSERT_STATUS_OK( partitioner.Partition(graph, session_state.GetMutableFuncMgr(), transform_layout_fn, - sess_options.config_options, default_logger, GraphPartitioner::Mode::kNormal, + sess_options.config_options, default_logger, layering_index, + GraphPartitioner::Mode::kNormal, epctx::ModelGenOptions{}, debug_graph_fn)); @@ -484,16 +543,28 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_NoLimit) { TEST(SessionStateTest, TestResourceAwarePartitioning_LargeLimit) { constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/transformers/tiny_gpt2_beamsearch.onnx"); - constexpr const char* limit_setting = "10000,tiny_gpt2_beamsearch_node_stats.txt"; + std::error_code ec; + const std::filesystem::path stats_path = + std::filesystem::temp_directory_path(ec) / "tiny_gpt2_beamsearch_dynamic_stats_large.txt"; + ASSERT_FALSE(ec) << "temp_directory_path failed: " << ec.message(); + + // Generate node stats dynamically so names always match the current graph + constexpr size_t cost_per_node = 1024; + size_t total_cost = 0; + GenerateDynamicNodeStatsFile(model_path, stats_path, total_cost, cost_per_node); + ASSERT_GT(total_cost, 0U); + + // Use a limit much larger than total cost so all nodes are assigned CUDA. + size_t large_limit_kb = (total_cost * 2) / 1024 + 1; + std::string limit_setting = std::to_string(large_limit_kb) + "," + stats_path.string(); - // Large limit, all nodes are still assigned SessionOptions sess_options; sess_options.enable_mem_pattern = false; sess_options.execution_mode = ExecutionMode::ORT_SEQUENTIAL; sess_options.use_deterministic_compute = false; sess_options.enable_mem_reuse = false; ASSERT_STATUS_OK(sess_options.config_options.AddConfigEntry( - kOrtSessionOptionsResourceCudaPartitioningSettings, limit_setting)); + kOrtSessionOptionsResourceCudaPartitioningSettings, limit_setting.c_str())); LoadWithResourceAwarePartitioning(model_path, sess_options, [](const Graph& graph) { const auto& graph_nodes = graph.Nodes(); @@ -501,20 +572,36 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_LargeLimit) { EXPECT_EQ(node.GetExecutionProviderType(), kCudaExecutionProvider); } }); + + std::error_code remove_ec; + std::filesystem::remove(stats_path, remove_ec); } TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/transformers/tiny_gpt2_beamsearch.onnx"); - constexpr const char* limit_setting = "5000,tiny_gpt2_beamsearch_node_stats.txt"; + std::error_code ec; + const std::filesystem::path stats_path = + std::filesystem::temp_directory_path(ec) / "tiny_gpt2_beamsearch_dynamic_stats_offload.txt"; + ASSERT_FALSE(ec) << "temp_directory_path failed: " << ec.message(); + + // Generate node stats dynamically so names always match the current graph. + constexpr size_t cost_per_node = 1024; + size_t total_cost = 0; + GenerateDynamicNodeStatsFile(model_path, stats_path, total_cost, cost_per_node); + ASSERT_GT(total_cost, 0U); + + // Set threshold to half the total cost so some nodes must be offloaded to CPU. + size_t half_limit_kb = (total_cost / 2) / 1024; + ASSERT_GT(half_limit_kb, 0U); + std::string limit_setting = std::to_string(half_limit_kb) + "," + stats_path.string(); - // Large limit, all nodes are still assigned SessionOptions sess_options; sess_options.enable_mem_pattern = false; sess_options.execution_mode = ExecutionMode::ORT_SEQUENTIAL; sess_options.use_deterministic_compute = false; sess_options.enable_mem_reuse = false; ASSERT_STATUS_OK(sess_options.config_options.AddConfigEntry( - kOrtSessionOptionsResourceCudaPartitioningSettings, limit_setting)); + kOrtSessionOptionsResourceCudaPartitioningSettings, limit_setting.c_str())); LoadWithResourceAwarePartitioning(model_path, sess_options, [](const Graph& graph) { const auto& graph_nodes = graph.Nodes(); @@ -527,6 +614,38 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { } EXPECT_TRUE(cpu_node_found); }); + + std::error_code remove_ec; + std::filesystem::remove(stats_path, remove_ec); +} + +TEST(SessionStateTest, TestLayeringPartitioning) { + constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/layering/tiny_gpt2_beamsearch_layering.onnx"); + constexpr const char* layering_setting = + "cpu(Embed,Decode);gpu(GptAttention0,GptAttention1,GptAttention2,GptAttention3,GptAttention4)"; + + // Set the session options for layering + SessionOptions sess_options; + sess_options.enable_mem_pattern = false; + sess_options.execution_mode = ExecutionMode::ORT_SEQUENTIAL; + sess_options.use_deterministic_compute = false; + sess_options.enable_mem_reuse = false; + ASSERT_STATUS_OK(sess_options.config_options.AddConfigEntry( + kOrtSessionOptionsLayerAssignmentSettings, layering_setting)); + + LoadWithResourceAwarePartitioning(model_path, sess_options, [](const Graph& graph) { + const auto& graph_nodes = graph.Nodes(); + for (const auto& node : graph_nodes) { + const std::string& name = node.Name(); + const bool expected_on_cpu = (name.find("EmbedLayer") == 0) || (name == "LayerNorm_10") || (name == "MatMul_1165"); + + const std::string& ep = node.GetExecutionProviderType(); + if (expected_on_cpu) { + EXPECT_EQ(ep, kCpuExecutionProvider) << "Node " << name << " expected on CPU but found on " << ep; + } else { + EXPECT_EQ(ep, kCudaExecutionProvider) << "Node " << name << " expected on CUDA but found on " << ep; + } + } }, layering_setting); } #endif // USE_CUDA @@ -909,9 +1028,8 @@ TEST_F(SessionStateTestSharedInitalizersWithPrePacking, test2) { OrtMemoryInfo mem_info(CPU, OrtDeviceAllocator); std::vector float_data(1, 1); auto value = std::make_unique(); - Tensor::InitOrtValue(DataTypeImpl::GetType(), - TensorShape(std::vector{1}), reinterpret_cast(float_data.data()), - mem_info, *value); + Tensor::InitOrtValue(DataTypeImpl::GetType(), TensorShape(std::vector{1}), + float_data.data(), mem_info, *value); ASSERT_STATUS_OK(sess_options.AddInitializer("node_0_input_1", value.get())); @@ -1379,6 +1497,5 @@ INSTANTIATE_TEST_SUITE_P(SessionStateTests, PrepackingTestParam{true, false}, PrepackingTestParam{true, true})); #endif - } // namespace test } // namespace onnxruntime diff --git a/onnxruntime/test/framework/tensorutils_test.cc b/onnxruntime/test/framework/tensorutils_test.cc index 8c5859823ac16..572fb6992ec76 100644 --- a/onnxruntime/test/framework/tensorutils_test.cc +++ b/onnxruntime/test/framework/tensorutils_test.cc @@ -728,6 +728,64 @@ TEST_F(PathValidationTest, ValidateExternalDataPathEmptyModelPathWithSymlinkOuts EXPECT_THAT(status.ErrorMessage(), testing::HasSubstr("escapes working directory")); } +TEST(TensorProtoUtilsTest, GetNodeProtoLayeringAnnotation) { + // Case 1: Annotation exists + { + ONNX_NAMESPACE::NodeProto node_proto; + node_proto.set_name("test_node"); + auto* prop = node_proto.add_metadata_props(); + prop->set_key(utils::kNodeProtoLayerAnnotation); + prop->set_value("foo"); + + auto result = utils::GetNodeProtoLayeringAnnotation(node_proto); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "foo"); + } + + // Case 2: Annotation missing (empty metadata_props) + { + ONNX_NAMESPACE::NodeProto node_proto; + node_proto.set_name("test_node"); + + auto result = utils::GetNodeProtoLayeringAnnotation(node_proto); + EXPECT_FALSE(result.has_value()); + } + + // Case 3: Other metadata exists, but not the annotation + { + ONNX_NAMESPACE::NodeProto node_proto; + node_proto.set_name("test_node"); + auto* prop = node_proto.add_metadata_props(); + prop->set_key("some_other_key"); + prop->set_value("some_value"); + + auto result = utils::GetNodeProtoLayeringAnnotation(node_proto); + EXPECT_FALSE(result.has_value()); + } + + // Case 4: Multiple metadata, including the annotation + { + ONNX_NAMESPACE::NodeProto node_proto; + node_proto.set_name("test_node"); + + auto* prop1 = node_proto.add_metadata_props(); + prop1->set_key("some_other_key"); + prop1->set_value("some_value"); + + auto* prop2 = node_proto.add_metadata_props(); + prop2->set_key(utils::kNodeProtoLayerAnnotation); + prop2->set_value("bar"); + + auto* prop3 = node_proto.add_metadata_props(); + prop3->set_key("yet_another_key"); + prop3->set_value("baz"); + + auto result = utils::GetNodeProtoLayeringAnnotation(node_proto); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "bar"); + } +} + // Tests for ValidateEmbeddedTensorProtoDataSizeAndShape and embedded initializer size limits TEST(TensorProtoDataSizeShapeValidationTest, ValidTensorProtoWithRawData) { diff --git a/onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.onnx b/onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.onnx new file mode 100644 index 0000000000000000000000000000000000000000..57efb4ebe11a3d241d1395417068e8fc9819a9ce GIT binary patch literal 580131 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%0D$EJLN)|?b@`!kExdS8w&6bvjc0CnRR_i zuxF@>Hi|pL+?E9>q$moP%i{UHHa0}FD~5(9v%ubSgc~jLOkr^roI;f5FVG_q%KiYS+jQ&S&Pjxm}vvsKqzu0T5W!ULOmgvt!4l}{SpLP z6&C2ATm{vAhU`zLIm|hS>1@cpWHh~$f?8?{tcH*{WERH)xseVy=K&mINAO~%H$Gn) zL5ErjG2JK|5)P{hZpi!4nv^-jbN^;^E__GIDsJQSRS`7mUlUQcwB^e={eu@P)`C*! z1(INuNH5mUXTJ096Ir(}R4dDmZ=Et1o@ai+=0$$=%efJ9&s~y1Foz?G=h-hs!>I&quV zF#6XlB-2>#d<6F}EF;GJY2ig&eSNR>75&oy^3x0_}uB}pPoR(^SKPtIwK-Ec|9lxRZ!kzBf9U*X`*%Z1gIQt zAyzwHQXwA#>o>$8{W=P^8WS1iIts@%D#4w{Fyi(nNL6GAJ5+lOE?v##wBmEPt~?O( zUw(jgrR(tR*cR-tE~2hw(-=|49mL|c;|l#aZap!in`0H3(SHv>@y<~}&6z%d&dyPq z<`)D5JEN)J#WWDxEW$`;nBdVW2Jh*d!ScFqH1%0Exv_i(nR6-)RP@e6kX0P6@w4Wi z_^ksTv(vE1e*zP~dnR|Ls)8qvL|7rC5R5gfA+P3!Vuhy%+P;5Gq&B4!>zR>c>F8gy zo;iXC9@;_m?Ie)!`9>A4&SvxsJ;5m-q5NqsIh^l`I^q{G2&$lSs6vpqX+P|6ID(~< z-f205n+Mmxuy~qyyb>=xp2pO|IHRg=f6$iFc zy$&kpEAVwbx06#JZd0DF6fWJa&&my23pTt`VaehwEN@tj@y!8vWR5V(xJ$sx;Tg~< z+kg}k1%AK#COGBPsnaLL()kdrLOjHU9}dv;+ER#pNg*VABPS{)p( z-A;>VvM6bD5GOjA;Gpe6xN}(shWLf3!R5?b15@$UT}{T{;}v!sQACA+dF=UjgD7#o z4FitGKp@u{+$t7Jn*=%_r!@~2S^IN+ysvg)M^ zK}gjFVA*0e;(;7{_104S#r4>h98w~4on7F_0e$wps0nMd$B!oOR)x3zT9|p^7I9T{ zV0Vo+z{Z(Q>=qRbruE%TDtxj*Q2p{5x#}9vPwhQ}6VpW4ElCGKzUvyznl6I_aq%Q) z*-n%%u>rB+KHP$_STX52{khbfq^s&MC$tALF^6=>@V&^d>q#>BQ*vT>K`VLk>LT z4{kjK<=j1Kv=(EgwoZhSO{VSM$Oln=!D!2Qk=FD zh{11uqfqHQ_N3!20dMbg*4U?2(7#)sRr~J?P0hbfEPW?1^Bsn$LSz@lzqx=Da~1IA zU0K$~p^TW9rV89*tx$P>1E_DF$OfjiQ~Q6h*lJ`;`#Y3a@4R0Weh8pHGzz@MoN;WQ zBX)J~AjX-}07tTMYfm?*1Q~$n-x%^HE(CN=T&EE!eQ@&J0Wc}fK<`_x!RU+x$Y1IO zQ*V|E-4tbnUUm|nzQtJ9%JQRvxPHOAFxujwjbi(kFdFL`@l&QK+H{Pe(3(aF+n7bm zzVq4jOD3|G%lm16Sv%vqnbJ_w~wH;LPCs` zMF2XAM-kQCvI6Zm8MYSxkk(ac^iyvW$XB$HqwPXi^|_L2W-7Bc731N=7F}xj*AAch zOkovY6vEc=di-yKi#TWgYdkk;1?kG+(-$)Y*eBM8W1l#Wy}FIOOrC(rp)=^b8Qin% z;<_iZ%3lW&V;P_PoaIX z8=y!pc)rUzMsKF;6bV%pylj&Yt|0Va2+!K6KbWSwj*Sh{?spVHuv;&~z(J>P>iPb|>regtToHeg&f zx8Sqo?Rd>Q0QExla60);xSq;&@{>wPLzyZYy&?~1>r7%4pXQO7E+V)|DFb`!MA7Hb zA5s~1279>(fmTQ!s0oU&hG)zqoIOZdZ{(7;gQCoi!B*Oymq8baNwdLw>*=N!DWr7U zNk|e^hUWoEXz+U#duVev?zvZtF?yF#_WfySH_L^XgTf&9BnjJo%dr{Py;&P(0u#pR z!l^bAtmLN}!SfxZ_- zKSL8?K5o+#Vn2zfGs>n8u;lnuJg^}jqpsY+`E7p$Df=z3$h8986tgfm-I(!xJsEoQ z+bK+%#h!|gWJFFT2rcs8IcjzwFmSh(R--E6z`=R`P6ilhIVkSrPfoHjvw0tWA7s+!FSnCI4H%@?_ zl?OQil-7gNE@?}zwTBL zGcElu+&^T6c7DM?dSh^GZYH1Soj}6l&*5L=35@3^9-Drs60ZhtBqIV5rZiuY+vncI zY577>IItIOUcI2-3V*@6l-K+rl1_D1ma=T2F&#{Mf(Ax#o_U1n74q0E z`m;PVO2A8w&`Nahuea~0Gu$3gtqOyM=^~#(X@SMbCN~OW~dzt7YgYbOk z0``94Z#op)0w7`wJGPyp#=#0qusdLK2<+#}dv_XJ+% zh+y|5%3t%L8QQu-$)g2A%!&y%*!EVF_&P2?nME)8I%UalM*ld+2blu}ztYJGH$O=G z8;&tr4=^UBj!g6Bp5Hu^7K|$4NSG8ujok%4rNv;hy$p8I2xwZR4KklDgR`6o*O_*L z?Ve9TwWJoJtunAZz@DA(JRD_P#rgGeHu$aHoW{E+Vb117aAp1)G=Q|nZU%rx0mbM1_3w zIlk3-=sKVPdrZ!QveXoIs(C7ul>Fq|#NWq=90TYwxsN3$*Lro)2B1_4j0L}IW4@oH8QNy#PhT| z2N;hN1@JArlGf|k;EAd*&|0|`-nMna(#PE1*DwY=>iY4d%wcfqm?2m#{R3M{S3&0* zc~{kp1jL-mAweRIsGB#z{ELgR`Q>v6c>R>hXt@Zq+YOnAy;IRo=mbp<^({AP zrzG0u4_fPPg2-JnN$i2Na@i0)_z0%-qM8ZT+P~y?I%Q+`>Ser_lhdeqi8h;a={2{` zzW{OHAGrNm0jiWt289XlAS)~$@|8|e=idxy+QyT*qfxM!%jPng7T{AULeJTC^P6QP zVVR>0o-v;Wy>jE2q$8_AB2JnyEsZAI<`42d9r-{G-yO#^{PV!tc@vo)<-$GTiIC^H z7*9;ng@`>xRJu+LjqlFDC?WtoI~&p*s}I+<$+0doHq$4flbL6M#o(Ul#MW4wV%48! zdcM*ENAHS5XPqoFx>Jg#oReZ#J-976dP^VPc4@LQwRid7s6O*5+z8XeOThMVEG_J{ zCC^WGV}H&9#?SXGT$wly`;MJQ{~tl*i?%&x*uE!@zZZf1?QoLyBZ=s6dQAVkxy<#M zwU8q}jv2!++{Nj3JKpL+iESTw^-7K{YrBK@{f^Olp+UIQ^*Qkq_a!4aCVZKqRCux? zi~r}3IL80m&u%!c2x|TTbkU_*s9%_YBa?H9#!rfi9KK>=qAPl~exX)PpW#s5Fx~tv z2u`_okak@)YAn_U2H}?U?BYpW&RLqyx8*Z^maEWA_65|ex`393EboiqS4g_I6`$-) zM%|%QIJL|Ou1l#g@$d5?b?-T1{CWbK$poTJzXElx4dqtK7-V)E;-2|VD3!Ss6i)@i zhE7dpe8@+bT*M084KBiF-VE$nbP=R~jb~-PRH0nnOt>_l$mGlXLA@K*)KODI;Ilmx z4w{~&zMj@7_4f=`+Nh)N&k0m9j^nIFFND(BXE}yvE5zz&g>H&JxGv*HRi`WC#I@p#(aZ;+F-GaYA72=pJsGB~ zd`In!I6p|rA14Ye<8-7FTwRh4TX(KvoTY383N~B@J?{i7X=4Z9O+Vm^=mu!s^OqFw zScb>FjTr6jR<28EMIyg7kk04bc*HUfRgxuOi`hqRy?H|)Td3i=u5-9+;ak4z`_*Jq zL>@M6IgjPsepp6s1n!;o!{1Ne!&O5GW_8UMG;+R0DR~QK`)8A>Jpmw_dV%Tpx520z z8K`gehnKw}9$I()5m+6+!hdJw18XHhu*q=&(|N^^TKH7p*vb9)y&?~kdHuw}W+{w+ zR}Z=0cJgbRQ=w=-*A=bQW=o%E!=9_*peZRtyvm)x)A0pQ<&zmv=DC3~Q^9Ww<58mI zfwM0c^G^%Lkte5@)8wgHg0#W+)KxB&|Lsc##LZp-@OXrrS<20lMepDge<>Q=6k$TE z%Bf=3O~G;F$LRjJ4!h^PAXZ*E*f6@Iz0?i*Hst$Yp~nIeY6 zsZRLr@l>XH&sBImH5)c`??vsQ1i{Q?8K$KO@kN~@9{st1Rg6yK_oPl@PNm!d^o;~# z(FIIXoezziq(vO_bGaUTBee|@V|VJF!-#-%`p;J$hAMney}@PxTY z*?e6NVX%xzv2#T`n|^9)@|~pJ5n~^UiLysU6==tV3FtKR8v67Gh=GSAo;DU@yZEoL zg>{ADJ~5i_I9u@e19x~V(8eU;xAe}NMA~pX0Cv9P!HLLi;LBWtU0$VpsoG#vRjq=C z-ThR5Sqr+#7jnM8AubxI<#&-I_*bQmO6*<1CT*(3nb&Hlf{Yy=wSP(M7YMU9|2kov zYXqT-Q^=R^t6{%_0h5T?xN}i1EKoW_N{W)G$kz*G?*ei7(J4#yXWb;z>Jl((Fd9w? zkHYKYpOCE-fyf_~_~Yt`Y#_2Hc(-rJ^ z8HmR5<*)}%p`0$l=L9YG_mn6Q9cjls7PrXRf+-MpVvJaXDRJ3`MEo%)j%*G831a&e z!Jd5%c)H{)*|gh)$)B_t>XX(%&hUMHhXcUKFFB}?o(oN)cVM)?1=_Z#z?UL1cFg-8 z1~$r*H8RiX#PbC-f7%W5@xC}J4J)(V&EKhY+*SNlm4?^1JcDNwMg%x?ACvE}0{J&C zxU-W5Lk%}LY%oYAWf0wjPDAB`ZAdQ2qsOkRJnfnyNE`vGIv|6_3R_V3>_yDanF}Sy zSxnT^ppwQ5F=hS)#$6$Y+RopGO1uQ{JR3O3JP`Q zfLO9V{@rcF*v+@WdsBarjoEo1l9xyqcnE>{p79LnzD1&1j$o04K70*{rB#nIAU}?) zXWOl%zH{Q>Rbv#t^mIL5Z+niF+l9c}RRu4QiRjw49y;d9LC>CUynpyN?v*T|57vDV zygq10WBf8{$hk1|<5))u3qO+71r?a;u#4o(t`85C4ejgFVX7d3c_o7JPikqOr{rDr1lJM5(I)oq1!>+Y8Tn;7!75X{O!}?=jzv>*xn!%!9>u)?% zTmw@~yf|*~d9*Z`h<|5$0#PqS;Roh?tL-`bjp4c2f7uY?CrRUE*DqACH;}y7iJ&)z z$1@J&?1*Mc2pN(d}#B@YnDLQMV|F$i@98Q-0pSfM1T3ef^QY-9-!* zh)APwv=Ni3K0pt7H)3+T9KWelg7L`vPOC54!IEYLLBl&v1Dvu3f>gv{_?aG))7m3& zI2z5(fg)lmlmgWcJRr}1F4Owe2b%XGT)(pdR0WMla&y7rbsA4LL%^~h%7RZd`{lHyU{=uoMd>FssH2nS|gIiq| z;~uvk*u3=$n)lqsuDYvuT&W*_u8~6R6^Xp2&beeu+!;IsE6I-f=lp2LsknWT87hPw zrRIx_d8}bIjGVNG)W}eHZ#bUaap^wnZc^iAt^0%uSv(prXAHOQ<+xi0nF4bqJy7vF z3@4RdK(zWAOex~whKTJSS zyguN0hl$L_`uXgu-XQvOu`Ftu&4jI{lThQiEhvoDg1DpzmvOvGf5jE?OD1hVC1oX$ zpAv`gp34WQnNq#_3J_ax2JLuuOw1sUSR|{!d`nGw@^>0A0RdQ(aG#{cO+f2E1u*wy zI6hH4OrOk*U|$a{!^W<0jN2QoV;SsB8kcAg8IK0R(6oK5x1u>V^;~0=m)GHloHgpq zmVj|#!E|z+h`@ZUKP*|7OA`JJ@f+kH;l#stfTx{JO!HP?+u?95;>|+6s^!qXsfOdF zrlG#nAZ+zfW%r3pV2)E`vhi3s9r7E6AmdCTTxLqLo+E0y-NK6--{7!&4pco_(& zX1Bm9g|pPXu?^DH;z7poH?E&Ik?S+<$Bd;snp3}re2m-527VkTs9dbX9IbvqOSOdA zTGuw*RKN<-pL$~AXf6bAR05ZYTX2>CT!D^uKT6I&imMdwqQQ_jtnHaZ1BE8Cdmk3z zQQf6DndgP~zpq2x{ko8MC(-#;u58p=*MNs=D@gIT*Vr=Y2y6@+#y{cFU~qa8U6je`WVZ~U zes`k4dXg*XzBtJ@@(F-Te=~^WvR3%@sgED?pENVLVx}OIV?mcjwh+Vu%ivW&sD3RuSJ@li*nkrNLU zP~0>b2KIfUWm1btaCSP*vQ)!_;7rnNl1r~Hn?%0YyFrn=9Gl*#!TK7^VOCtp#_53) z<&!=B@ve=Dfn#Sh$IB6B#L_}Bx3(KFr4LPK&A~Ntds)+dX)qp8gX4V?%<t7iE`5q=Ea^KUd809aXMW4B+vCZ!|1bmJY7(7#E z`&1r5@KbFp7Kx!jQykI8ECi{?367hz8Rp0eNW#vkI7cKMwwpbHtoVDd_uD$!dg33f zHgQA$OYgv7iX5ccEyZm2i*V!OKXNxSkr!zZ1(D-~;OF}Y(rZx(J>!mJMR5h+_3uj5 zI&n^LOS0S&(l5e!G8SNsnAchfoY2CtF&rjV@u9eDi3WB%7_kOkGOSAee>luR|0|Y;)o$Kw#^x%Ln>P-aQ-w+KVHS95!@J2{yuZH*3dS9acHyfXiMB=DiP>kIl1Y#A3?8AaeyY{3ioD zV*WwKx?6a4?Hvd#UJD*F5~QO}A1xZ5(@)KFA#DCVSamcHLT(Sy^Jm^-{Iy`bA@v)L zPUoPzLoQEWt{M{LOc~n&DXdtTib=R$5bpQ@umAUs^qLrxrn>huKT3_AW3!(oDCcrH zH+gpFe_CvSS|z!?%U_`Hx13#7U{AYpSsJuR4@z$?<<{8d^5Dc0$k-pl<&W%eTEQ4m zNk0TzJRgJO_y-X3K!_RbAmBAxCQ!J37u(mI6nxrb05hfz5xKo7Xc~M8kDtE|-BAp6 zx$=gd|Kd(Q`hA8=x>q5t^bOVg;fVS_9Z7rp1~fa^26Ear!ESLgo;-gaqMzIVx={p- zZvUqD$yw5GSdS^laVX_AS)EB-{vrMj#E&+Ca@#rFwCoyrTs041Lpfaa&li+bbn+WS zId1s44D#&ed%^7>3D&$Q0~(r6W6Ljuq{*I;b5fXTTik^aLdKXkvA#TQwg^ivOvbXJ z5Bvnz_aNav8@oRLq&|<{3)~84VAY$4uwc9_?F}_#_A8!aRGwMGERSUTs3Zj8t^U-> zAQ+^MhSJiuhu}%|Si9`ypdwyOo6G8HhFCOOJt*dP{#Qe0uRD(oh2wA{Bg)QPA%N(2 z$MD9@y)=^ZVA7K|Kxc~_lfvEAabi*qIc*h00_}RyvNDyQ81<2yTv5pJ(^(W-6$J-; zO6imx8}at7pV*_UOD>)-B1?Wu2gPRx;h|a*s+foKV|p7%hLI<1ys(VUk^0LIwwH#b zVJ(m&6-SAC2yPl(h!Q`g@Eh$Q@9IZ^v@B#BIHpiynko4eyA2No@R+Xp*=)qEXqcSG zhk%q!kP%r2!i&eTzA49PQ-~||uOEi(YbSEJWJS=uV~4hjrI5F<418M<ZAX!T#AuXYzem5&rCahYaMw|4j- zl}Nq>IrHCNOUA2bZ0Sbx3|Je$y%!D-sZ`oaV6A#k@}v#hcezban>ST3Lv@J1!p;Qk zzRR%*{iPsfwwz)880hIZS#G#VnJLtkzzhcu*6O7rR*ZK6Kl_X5w|y~n%DIEx_X;3p zI(M$RJC|-Kv7z;PZS>xe54^3;0c_*wCn}NXz~U3ct7C zhBw`+*zspB%qo$@3Rhj?nYCG9)9`@!H&=pLpe|aq+G3(hJUuc~o)OvX2Jyk?xV?84 zd=!5J2BNN@A?Zjn)GhFBu@GMHLOSk^yGK2D3=0ZGov`K9M^d(u>#awRV=`3b zutB_;$aBoSu)m+-q_!qLVWja}QW%7fxEJrHIK+!kZHLgi zyb!$Z`I=;PC6dO;Dd@P<0iV1$MH^ZL{4-n!E>$cA|4KLD9L%KIzk_J-wRvFt^(Jk6 zewxnTyPPO5*aO{b#o76{tuS3#ngoZGVaL#B5R;Z9wKK2d_Vz@*n`4P#=Bf150|$8gUkzVuQ53GP=GYPDA-r=1E=2zGE~xpS0L~)fP_OYo(74u; z>gMpsaL;TQ@xRCUKv#&|(+=<~LF^fbg87A`P=9Vboa*(0qanE%X!452?=8V6D_>y= z*+)N>X|mC#Z;3<)#|+G{p;LYf$WeF>ub;Zo(7Z&nKW0Kx{o?7ak*lRNa3jsB(q~4T zL%`vwK16h%!L>V;pzI8#OH)7Ksjx#ZxKI{ONV&jL|55%&DQ>Q2SK^FVWkJ>NXFM`!hyaGR3_S3I;VANv#ojL11OWIj>9h#nBWbrf{Rs_EtTSza>?&xnHaygFQ@Fp*@x$f3Pu2{@r42o;|GrneJZ&^5ImyoOu2zHBK;415aV zlXY>#)C`8Jzk%J#@BHACv*~bYAQNF}K~f*DCwmIkpzd=4_!`{f&gnY%=9wrn_EDMf z&T~SZp&mrYMB+22Za6gSA&RcbqEVXH1@@C4;2~NFdjpej$M<5q{;7lReeMjoOFeMD zS2Y?9xiYpEZg_@I;3+^Q&zyoym+s#e)jGd`t?ZjKU%J?kBsgrMc2 zjr>nrp7QDMZ7Ah00dcoaL&5Hu%zeosOu2ai{R8%xx6SALBHL0L;`xRIe|NyOx0P|+ zZ4Ks=U^!zEkp|NwB_Ox&G1n6mV`Cf>NKkwXcbCvwOstv0j;{Iv#is{xtm8P;&n$*M z(1n&Sp@NBG8bG$$kfd`8L_NG4>Mze^T{Qa;oZ9dRcgI%Cj%@OJMiEla<;2j~;MP1vRE0LK^HrX*Wl=jBvLSA3`3=R zXpyN51lrp|LQM=meWZX6xE+N(RUOE_=kn6;tKr(`IHEV(2%TlhQS{*i=I_eAn4m6# zOY^MJYtkrOuG@f)o^nh#PmHnL_Y5AYMvzRSI@rD{73DFoOmcH?x&C`+^tJtocOL4o zdalW^s(cEv&m!S?%XA$2mBID10M7ge{_ep+DB<-O%<+Eh$7c-tri4a z%EL`h#js-fS9tAv1%l-tLel06x}>5STplFR%#~AkAuWuclip%wVU{! z8Wjxcz2#4Kdc)s;N`cW*nMThCKBxYM;v^zvKHOM4m1B@CM8DG$pg7x|ctpA5ubBNP z9%qa~7FTJPnFBE^@k8~NF%-Y~fKPt=(8;rhK>W@u0(WPzqX(X0==p2Jp=5*zjjhI* zKowv#6e-EHFE@eNspH_w z>3dkZCXiHdTFD)oG@N;32e3!YxH%L9MXH}*>3Mm6$Rv*WmvaTmtj+PViYcmW?ZMz# zkLfO{J-A5z0eU~-_~)Ax!SK>Y68p;!wSt^zz(@Lf?bQ*|${c^AjjYnT2<&lrTN09fH43 zgul)iP?Wz9HYSDw+n~?t4Zq`fvvIgPP6M_iX!3VaGjg2cVD`&pzzicbG#tJTo)7aN z;baaBeYg&PvXrprwk~$`X|PwlUSLCG6pHCfv+B{VsKyJSxcv)w%LUOu-bQpO`AOa0 z7QvBgviRV%Ja%c%!;2F0(YezL1Mh#rr5vBf_1Y3NW)o2LmoEFG^$f&bsl`s#7L%49 z6l5Nrfpmux-oLR5YBgJ_!Bj;e$kT#i*Xc|~#3i`A+mk;OeFnX?qPU)Bxl0kCk{ahDSSpZGmB{X&7*sg)Wa0d>pI*mESCJ>n=UA(Rm9NPfG*#(m&$0 zb_i=ML|JL4v*fvc7Y6m_P?^w6a6)4)Ef*FdZ-2YP*6x>l|Bnl>^~WI^!~G{E%?=9^ zTjQ~ZHp3IG^C0ECoY)Fw(9(;Ev^xJDIx0?M6~|7J?NeRxy>=1!Jh%%zYo74Cl#i1y zfq|fNItuO%MZ#5K5m5deNv3===PO^h40F?R$k|LaGIa19E*T#Q@dHax(X1Mb$|V`6 z-at5UXD45w$Q?txR4|vU2Jw9#X-3>_cxF|>-T9%-G+to9OYjQB+fI?x86o&Fz73=2 zEa1FG8w{Cx7Igb3Vf;#ArXX@U()a;Tc2xym`AmrV>5Si)D0sGVH>~-#ThQhG6X$PJ z$DI{>aIkb1+gDTq5q7_Eqvu)tdz+;_o%#&kRwoC(GH}QuliCJm!+6;<*s3bcs_gWJ zA&!fzd$|!}i@#yb(MKSmlIYZ`{O&IWA=Fv5En;~w|e^BsfJ`~gJk>+1!5ul&*DM&%d%I5mzl+3(LwPFlHy$ z>)p_fMnA>i_D}9S>nP3L(=h^ev%B#Sm*GF_aUE3_FNg2mp;()FlAzoU5@VN2p9N7g z*m0FKTDMW785c3jMF&Q+ACbTS3$|s;G%~C`kqLVrN}t#<+t7?Z7F7OjM{*0T)(=L(=B8E^2Ba^gE>C4 z;a9gTcgNc(e)_MC#F+{c4b^R+SYC_9G172+g((eGY^FNi=cvBvIFMWD3%S#~Vfb-4 zyqB+poyI}9M#CMZOjV+~uef}p19#_PR4w05)&zq0iLlj<6G(^KJ;7RMIY#@^e`Leg z8zg7i9cuCKILTe|7;Ze84nFI{v19EGT;*fMvZ^sS?V+*Y$NaY_vp^m1nM<>;96Hdw z^AqqKbaC%^)A9v|T7o67xw+9G%M!0#%t+mV4jap%ZrN}Cl{dhCJ{9Y{{EqdaeAuxs2x?{fSLAG4@(^Y+~&g}%in$JDs@ zZXfi_Q)0y5-J{~Z2f=htB-#%yWGBx#3$9%=@VJU6b$IEDU#7(i@{FUvrau++PMR_X zvY)u_@hm!5*&T09h@xJduZiT8c{n-wH67t;5S5#SutZD?O&)lGzUdXz54%Kbnm$2| z+9$5>SOWXjKSv>f2ITFo0t=oA?thvG74D*Ra*qUSyGe$*yzd7&dDa7KEDhmXwht}5 z@sM2H(MdX%UKDI88X=nQCakb@3OuV;mX`?FYI08*$Tp7Dt!ZlP<1@)WP)$zRp&`L)#Zpsbo|9sQVW3 zcQ-=zx__{+E*aW-jmeAAKDx@=l}O!M23h8NP%PvZnQ7q<(s~NGqpBN1Jtu;~QA?~| zIZPva_1KRkzrpyi3aN0dCp-4l!w7fB)7K}`wD6%hhD$h+zANIaQ=~J7@bs7y|E-3- zbHc%L%pB(~e2e2NzoXI2a-3s%S>Vze43e*h;MHIP7{>~*Bj!D|`tzDBRXC5THwO5L zmuY!BYzFt>No-TM9-}+P!r?D)o;#%LfgUY1Q)T@1 z3!%zgk$zv>O{P89Vz$_q@EYzugO;T}kaA`rlV{43%CuQ@;wNQR*;X3ltfB-7GZmP= zv@^gAivYei0LwaM#-gK!vLg>j2m6DDALPM8hpQORl)>W&6-YZhg%uiN@aI8cX0k~= z#@t;?8PR;aG%W;P#R=f$&q(sPS&0k`P)M)-L{4PB!d;7Y)Anx{C~xg@+VXA|Ucb2s z*+GP(A3~|F_7ij(KcDMsovh*yJafl& z(JHw0fj%ycLhETYr8h6ycnT~jZYI-ytOGZm8nbZI1iIj{Jfyx`3(e4iiQ^~Z z)8=vLyv>c2`^A9rTVv*i!UbO6@jKunbO8$cyx>)&G*<0vhM1m8)cjFS3=&e&p!f)G zOt-@AGn`1+Mm-ua5J-PmHIWKQXRzFUp6Z+_19^$T}2gW(fVe=2q z0KG2Gf9ZHacK)j2U!Ke9$^5QLCcfLu@S1RF-?yX?^V=?|dE5~fTD+^2aw}MOeCS3dVDxbN$9PV?P zsNb|h{8cuQ_~V;8@b=n(&bM65E+#Y}BL{y@mBtrBo#jGh!Z6nGi1ydbfTqtQbe+Oq z;%Yt*!r#wD!+j|RAZtuD?#Z)Dp}Ic`Lr5aiIlkzESYg%prCaEClBtS zVGWnbYCmZjdtn-BQ`n4Wl7(oh+g=<|Rc7tJ_w&szNi&XS?#%61N=)nZTO?q(35#Bs zvnP@lLtC^LN|{}Tu7!GR#B@*ky|qzLc*q9&jxU3+*8V7F{|3sO?t=*k1ZI`1;XtP+ zJdyUtjceke@RlNZ|L!TaU-RcV)aG#mNu6=n+e(@~888v=qv^-LW`HsIZ-f<0@YJ%ak@BOU2@Y0q~z!I37^h4L+)2AfOk( zMcbMdwwx!mJVcK_`!G=T7Ex{4ic!g`?4uqzxa?C(URx)E@a;bM$bB`f(@o^um-+!d<$_fyR-;xkF=3Q z$J038oDrBF(Pjn;t4Px2P*8VMXFqrRB*n}#XmcNicSR9kUtCCY@@{hb%wp0Ro5c_9 z$_9z9987eq$IfZO%sMVlVlSLYeNF#I(Rs&n^|o;w86hK)UC77?8R2_h$4Y4{sc0`W zrK0jPLPl03LL!MIg^=;RucKvDnj#H^q)@b_sOLO?`zO7ebKmE>KA-pd4O+CNaqRj_ z#J1Cy>}tP2t4^fiBH0$w)vAZb>UKa%_Cu1ozMk}%*Q3(s8En_-5W2qN2)}jF7Svqp z3juSIKqMsrlgjdmY<>?l4;ip|aw7#B@@6*`Um8`OhoN1Ld=}&vP|Q-*Ptj` zfy_cr$olaD$0nbGfg4AFXnrTDYlh+Xgc!(T=dl|#lprVa7k_e-7+v8XMeN+gc<<&! z`DX=MWitu3F zMogF<0b%uF;5$2&*b3S5W!{$Zdy{}MG~w7x*Dq0*9U0i8a1Ae?X{PhHT*c*y>fC&t zfps6B((ptH3_cXkiyZ%n)f>wZhKx|ts~D#EI^tJlLtekz5Vbv=!Nb&Y+BfwIzB@9J zZf?v&jqh=gmbDfb$;YJX$y-ogy@=+i=8>I3t+aZrEAG`Y!JwK(5Z=Ifz5*6fpFP14 z*6fA5B~wXAlWtA_pMG#o_(wC}4#5%iB6QjkiMuQVsnX50nDVimCOo~5YN5}#S>amN zcVP-0KGM#gd??4JNPITB)W?&e00EBg83#R`YBm<{kAi$eD-N8_fh`B_QIG3oq<6nG zEH=|duia&&)NK;Op;N$Aa}tz%mWS>4-+{gAd|1W#NdCUq!LPtjq~)*C=R_--Sk@7Z z{6>5{!vwB}27%kjcksf<6RWK=_f{HFvs+7$q-~su6dK-BL$%D$@xpA^Qc(qSs0Rj2320aLA5&`4n9r;Q-L2?4@IE& zI33&Pa@|y;pTy(_myu!=$n7vASkjgX9bwh{&;lzqh4YEKOy14?txknHr7{$ce29`m znHaET5RHOmaq26MkI^U0L{Hs<9~_TyTrXk9>x>cWr6$OJjh{%?3%Stxr#d)~=fb>H zN{1Q!pCGQ@5LPO7;&z!hG=EwH`_0c_;H3R{Ug8KOJ5FTNuZeKXkUqNnZyE4DX5sC~ zNHpIwktz&srwecI#?EUs5I`fq@Jl}plodsIbcU{>(fDJ2258I=!l0NH;QCsZImGP; z_EhZU@^Mj^e9@VG#C5w)uP|ifRiEIKDq|eKkO2cv){?R}t#qsCG^kj^Wqu4C;q%im zJin+2TjMyk%fy+mrM(u{Uw>f(=`;B6J)1yW&w}YVp^FJ!Ww`X33EQY30c|lVcs_GK z8s#oS$H_(Dt~?uz1+%FBmRsl?tBa>Nzd_TB8Ianx6JIY|hixGln0Q8<_N%zzk&V?@ z=A1*CE34t)j$ic2kIgVnBbgaDxcQLLOf-v_#`tlW2>+pBSfjQJ{6z2YM_zp9cRoA> zrwpEB|4bPs^|KS3^Rp1At(0SQ4wynxn=@KmOTeb^PU3K2F<$oD3R9P)gGIMBSY-Sr zW^0SU!r}~w){Vi1Z4+2IHB+`k(3iModQj&N+c?(GF}VF@Jrx{oBFda6ncUPNJzVDa zv~U!N1qy-geMem9J%S1c#i7tD3ap-SKCte6_~KYS4hqjE6>Gf7^eIz0XL2Z6B0fO7 zf}G&+xC)cEEAYf+8l}nf2K0O zEEW?l6E4rX+LDRCw1SEX%wR=No&}$*Es)dnjJAGV3bLX9&n7&lfoVS>#`ZAx9({>A z!#|OUx&Z~PlI-8v(vTFN3Oz6Oz`A>?B%EWMnQI(}7~eAdX)g(B9G}MeN-*}75UJe5C=8}_Tf z(<$QY_Xp*qHJ0;io(%-&kkcS-qspWThHP5 zNmp&$wyEIlN?V*V%zc+FO(enB9`jw|O&EhU8zEM11qg6^cR4A}G4tO)8n;9iel}_| z(qlJ)9lVI8(dRH(`6(?5nMxnO~u zoSSs=u2OA1J7%MY?x;s&;EU4x_M zJpNYAcOGIK&P!hb>G4tIL>&|;VCtA@$;|5T|<|# zKhll`CrY5SZ9n8KxrKh_u}BTNVC`l>;^FxO#+Euj<8~E#@5cnFuXQE$)jDu*rXjm? z%0&D$Y>5)jY?wzW7ohC*Q<|hTk?rC8p|qtY<8E~t%089iShfpw^T!J z5mq&BMXO2~7)Kj$-Si{O0U8nk+-jHnFeZ=KvIjwm@uvR4*eTuX}#qA6~LtrVdF+mOQmE>U3 z>P2k(<}M7Ds)3@vrTkXU5B$PAs|k~PgBqJZr5ToD=u^F#W=@KuHh(rk`rj*j2jz0~ zd71?;_g+AmU{RJi&`CTCH*)9UHm(akm4t8egH}aR%o;+Jw5Y&1j++s5r2t=XuJIek zrxH~eLk$1R-J{#Mtd{q2Vr-}eMWO&!kMCl!BF7rA`U?wt9*_f~PjOPKE9ynNfI-Er;PgQR>M@qX!O{S9;%JL zhX(ToqA0fxydM{E-{#F=!7rdL*UVv!^G9fVegWKK1)1x297t1w4*qflVw{qQW{bDu zsi2d5yCO+&SG7gS*Vph$z#E>ldmHbdu{+Q&k=+03IUaKzOO`aDFM-X)1H8&K)VOmNyO2}hO%;}nti_+-2n z@`FF}4b>#t zNFa}A)Y>q0TltIZ96;}nIF`pxV$$>;Q12zLu`l*5E~(}L_iyHtezJHZ=k@@}GB!++%Fd3PovQUPsE8@XO9=K+XY0&c6P zv2LRYutPi%h7+=IpyWHx&)Nlqrclz9ewvCz=A)6BJaD0&} zy5h6i+ex8>RkdTIr%Mq zwX(%L*E6Wg-HrP)%<++lrOl947s>l>OKu)51hc(guu#4ZYnwjPc&T1~x$82t=+k1i zJE&mHEG~}(Ra8fHKK@Hm0Ct@S{8^a<4X1~RrC$Ix{)u1~%M26ohvN8^+tFoPg+bBk z7OMDHpVi%Pk*B?jdw!&RSf|c7@RfDKw4rQ7y$*Oka0A?9%dqqN9U}ht3Os0<$e4en zq{8$DYI(b(VfYFBHt7R?Ix>yaEN?^sA2sT=q8IFkkD{S=I*$5r=k*?8X0=T$EHj9t z#Zd+DY_~lOzNm!jFPzcy>@d&ORgL+aehmbwvhj%ILjIIj#dzi54%``|&U&{w^CN#4 zQC}I3QEp$s3v(0$a+W*)L{oVAmkgOuS1x~Z>n8Z-{6~JZ@51SYvTTk)47{eRXq~`A zl-f*ac0dsCy46OIU$qA!R!?AkgpSigV^>hiMuYvDq{E8)906HfB@ArTA`OOusL_3$ zC?7usFB@Lf2-|FA-DWo-N9Uk6FWwRlqs1_Ex)`UFw-FPWXb4_xKnkN`(CN7`leAF&iPB0;3|KO5?mXUL9l{c}IZ5BxpCk8x*{E-tgs#+UWQH0p~q zRsU|pu9V>NoA3FMF}V+eZm7UeMkv7j^m=boH_=!$p z-1Um!)6ZzSH6fWej&L1RWA1%3WepsE!R6{6&tPx1UO zi0-_zH26vw=c3&K(K9E(w~q_S{jK%r>n~4N*1kZ)mA9$mm;?J&=LIw#(8toC5Y8F* zfgVh0gD#m|EYgaDjkf7DN?e9*n7$cp4y}U6akH>UUzvYZQ-)zr1;SZfD-K3BK;N-S zAa~dmu0Qw!C5$+;v#A4>TodpEOk=tqU*-?Lny~Bc<*F@-pzk# z)Q|$(DQ$)BOg22?<|6Bdn#ud;d9ePAKD$iV3i*G|ld}&E@!RQhsJ6$LQU3H0E2L}D zV~;3^C2!*H;SZ?bHhuQK=Q0xaRRaDwP6R*o5prbXD%SjIGU)Zsfp3>*L1)Gi$Zv3h zRc_;S-*v9bc26D+#~Mkp{ynmN-ZZwW?i+Ya*o1}dlTpW^gg)tvhTCs{;k?TnvQf7K zaxzq4QC&WUa?eM9Y&~t+gv9x<3i03P0GF01^8QY3rhNr*ME!;&Gw%bCzEF48XD<)R zG;ffbtM8M?hS}iPS_`U?f1xL~8^QvMF~93Ib-O*2S$tC)8Xo+jk6JykH$ax%lm7?9 zMGE2N;cC81{yq>8t;Souf{e@YDCoJ#^(yKXg7?lQ+CD9gcd=TQEv4j`ycg>>ZI{R|!`^l?&JXY<8suM{i*j_`>3#Zk**a zoo)Q8!c5YAPvV!l0naoN$6hbu@+-NpyrvfEjz&^-JcVx0wL{s%r_sbml2wg6i}M7I zVn@gda11bj_rId3w!R~~^hpimT2n|0&VVBq6+qEG2&ZI+!H&TR>?aeh_poU@cFV+4 zk7YA)sfGya^l%lv47g6Ls4)m$R7Zh1%X!%~8n9ufD7aETFzEb9hMtdGHE?Ob(wm48k_S}*6soA`FDxzcpJvrtcfLBCMRL^`a-x9djubrUZWyuRxs;PBgy1=JS*+= zm?KNJqq(d;{AzhiBk2>kpW_Cc@nG&W#zAcOwRLlhU!`&0m|;qC{MHUz$Dgu@O54 zr68Q$gxbF+F*R4aaX9Zg=fj!L&s}@-ZWkvI>lH<9RNNBKn5cNwLP8iHa&-6BI znerZ2-<4sm zzoX#xdt2-nv1AA9onY4eLcXV9B`JOoSraF{71XxBCxV&1#3uhdOxt@4=7-BL5mMamSn?;+Yo~OB$f4Njr@F zw7|ThW{j592`X3p1zpaDlUw7ukZM0n(p8`H-%VLV2O1M8$D4+%WJzZI)e7C27 zTx?1C%6WQ%K*#+g>^k2<xU=dV%TFzL@u!tP&-xxY3bXSIdk8;?+N(jZz! z@5b6)O6=wOskr8j3U+nBqAgv*Y)|fFHl$ybT6;{x%S%^6(#}_qc_$6zARa9v#)#(K zLP$_lWhQ-6=h=&20Pj~1ar3^Hv^&+FY+udoC(Z!+Z4$uVK93r?StrqLAelb%tOUm> z74BVin>_ufNi?SV;=NuKTKncFNj%pJZ(}u>M@KBNhFmB23dFEORRZrD-nJPVti<=K z7pQk`7}CrtoOXF8>+(bvPVBveG07~}=jpM`KT~cuhV-w6G*+HDNhV)XVmj8UF!r0R zVfnu%x_Qem)!1H+%hQ0=U7E}mxo}<4X}wTA?K<)Jt;V?u1~Btr0v!Gp#Jjro68QOd zzzS`yUqaj=zHlx0yiIwH_|`WGE9uZZ+JOZ2XE#mF{07(@W4J9qj#-< zHXj3Shhl}T`c-gG-X4YHxC~K4C2i7CXI1m_v2XJ+P!`$_--fLqeaUki_z{aW5fZ4U zk%+p&2l4jG_ta#qG%GYTgPrj&p37CMv9De#GPhN-V0^6@CZ~kbm@~$>&p&}@-#;0n zPlxhN-&8@*(qX!{R2Su%Lr_N}lJvMsv(ujoLuoJPPvhn+=l3>}!P@zBZ*V%bcG}L` zELLHh!n>(C(@T>l*r9#4FWl19XW!2?q1%5hLppq#8h#xnH@3)vIp z|7LJrvJoKw2Xv}de-EffhJly z9;G(J)v#XX1-Tx$mWF$l;_f{&;OCXAM7nnxX(=tldfyTFbnhx|cbH3*KBaKnq07`( zWQvWQYYJ|xuw?|V8WDDSKc>4)gVXIvbo(JmWIjH@SsJ(K-l?+8t`A!9DPJ2*QyoC^ z)gc-ga)&BqQgHrRfpe24u-`NngUv~ffqp}l?BF<(9hu8{p8Au?_*X5SgIfWD4M01W zMS|9!*ejBWe-cD+>-`1v*}mzlpvpX6zz$*bZhejKR}5p^G;ZeV-$)`tFHnud_0Yfj zFD8$VQsD+Mq!;}mWbSu3u%MQmrcailMi73tsHDTT#sq|cuY5&HO_a zUD0CHkE!BAquZeT;Uvm7bH13n=Rh2vU_=@VTXfs`$9@Pgl{4kwA(wrZ@Q5K>`?fGA zMrvr=lL=I|{~)>*Z^w?S+LT%7OYL{&ac7qXW9r*xGfjInxUc4N1{HI7pOXb)B3K7IpFE>c9b@p1 z#9{H{u4@0qTxN%x#jEok(1!bpOf$z(Dak6r^v8*KL9L(owy4w7p^@-!i3ZF1sG;ZF z0yI<3z!5`)^w%qysM_^-p(BbHb`LRNeg&;wA4PL5n@P+zVg4*X9?Y~;Vh*=|qfzTE znSR+J(s^SU)~*u6EnkbEzNe4`ER<$iR!OlN?o8%9F(>is=XFpvH-#T;yBwa*O#_LR zCcG++Sj&Akl1MFY#XN~f(abwCL9A3>LKr>IP;6!D?T>NLsC#kYAgdlEZi1j=C1>@S(6yc z$Zb&Lxd%=JUgDeR2y-6MHn{lqGA{K?#IDd6v|QsNiQv7$w3ss9)b1Xk*;XF#{rElH z)SW_8tPIfA-I-mOD8^jB&A`a#$=IB4L1rF2jSad{&|s#A&Z`RXQC}rr``KG6Bd`?x z`N4#Sh?B22>D1XF5vA(5+)~SWo~KzYec`r{=H+m1`PDxlr+owX)acV+Q?AfRJ8pL2 z%LB7r`RM#jfNjqlq%YcPQLM`V(?jl(2iyAafIvDvo9P9UrzycEKQ**awL|AOT-W~3 z2rkzuLV3X)j2h{~tQ`N&UyUQ5%o#VP0(u+~&<6%rzaX{bguGIH(BIe~AvR);L5c5n8>N7YV zw?IF?uX_lT`zV@^a(OokFKC^mO3Ei*<{h+^#MgV&;YWG`DLEg3aHs`)CrgvdH7hXO z!3rF99fZ{Va{+lXnU*_yVQXLxm6=x!A^Td$M|u#P&Kh_7j)9sMTB!XAf4S4^~ z7!Lmmr^c~-G;wQyc@EoQ;lg`F@Yf+5nTko!^bKK+l?<5d$)MA=T_g(+-bbC+gD9Nf zftySP*uK{yY^#3*RjrDJ3*5awUwt;Cc2XaeQo1^AfDI@f&^Vjin(ArX43neRDgA;`A`GQ#I!!0zeT zc}RonfEQwc@?)wvJdrtNH-MgN5PH6F=R`vY7!>7@Q)mqa<%Z~a=_{^s`$P^JwZOo_ zL}ar2(Q8o?FH(D~axG5<7jizoucq7iCg;tdXL=8tef7&Due zO@*cmS>kr#I2Z~0z?s#F&|Z;@c{?ay(MS@#D$m00G;a_{x<}vV7vfUOc_?xD3F-Q* z#@yL2#fUlEW86$PXgT@;yyPlD0^MLzo)o+2oH2$(*1_IU6A13^B|5*e`B@_|L|vpR8oo=~c8M@0J5|9}a~Zqc%?-@Cy;;=11JEoO89j6f;(qItff>S*5(M10#?Y`EGPTL|-*eH%%u+GLA4F|!^wgdX(#xWsQ zn5mqoiCZ}~kClHYzlJA>zmY`X6|mZGod36mZB9z-)H}E_-qZ z!+wgR*ytvxO>M>1hSS)TDdO;A@g!ES^(GiRI6>_6<=Azxzo>5CQXCJhfrUGFz+vwV z&>y47IDZW!mCM5+t<}A{V$*s&lNABK)(+rlB@ITpJDvC1$AW3-Yeg(ApjpdXK{Kd? zFwUnSPx=GzZ-O3=ckMV?&Mc*CK0PGX`Ria#MJ!lmwm_j~G^^XIhmJjAWZHy{tk5Az zI=|)+I?lQbX7UNJSos_^GY{eRAE%+$#uR#kMR=PO)L2ISD>3O!gw1)ssMM;PcyCY$ zmz@xU6CWjjdyDXIwQYk-O;Oe<_jk})H(6Y~>=K!FU=e;bGv?;M;r!A)3-I}hQaGI6 zj`j~n(DqgaoPIEiG#TbWt=@JFP`Ja_{MZ)|X9B}9A*+u# z!vUT%n^;waUB@oLqnjf@uC#FX4qN=QtCH*=Qzi{PK5*c%G!d1x#lk>mJa=1*B!+vS z`j(TJA3X!LEgXTF`U0e4oG>~&n(Ho$W1GVZeBe0+Y0PBy{x2bDyf4S}IDVqH?d(AP zl{#$DpUt#SO`7hr_VuEBxpngi+Cvu<%Rp zTNBrVspefgvKV*l5U`1w8V4t?vAm++EYupAz>BX#By-PVOfs;AhYN2(#+oKfS9M|f zJ7%%R*H438(IZ%N(I0pJn~fvtY4qaIY1n9y0Re8k5D_lKzK@wqZMO{Bh@Ef7pF4_? z_bs04`pEIdPBqg`tGjf>uNp<(T>+CHL;S^IC$0Mg9zxc?yO48j3Hpfcz^g^v{hZsW z&i#5E#1B_P^AtmH4{72DTrZ&N3fgEg#|x)g=rNW#;}Eq&gpD25#=k8eIR2n3Y)(C1 zv-OiCx}7w_@cv?a+~Z2!j62}s;%=DVEywF?2qhvG?O@Nj(z3HnK{h8Fef&yk)RqpA zn8|UWZj?i_U<@=a-3dg|o4kIria15;q5Ie+npHo8iQs0JFRUJ7a;+&=)SSg5F=qUa z7q}c?cs?1e&}KdjNI~H#OIY=L0hBQbxXykQw%$31Zna)?O+yjN+5N+0`8}+0MjqMv zLkf%5hmijUL%}3Kj$?RF1dk{!)Smi;wm#T`qI1$fxmyB#kG0~GPY1AaDVG_(8OGmm zRfO4mpbiWtPM{iERajs37(f4cM;yhTL6UnDG|1_&*)gO1fOFiwzt(}=6q3RF&*O>W zj=S*qcLeC4)keiT2jG~GCS#<(g8HiM`D1F0bh%PH-OTm)%sO_!?$x_&&OYb5 z&zMi%Z;^x{i%G0kuqNaDG7s~rgvs!S5D*x508`InSQ6C@k(O~p$J+(Nh4#@kbIyb9 ziRlcFdqzUnuVfBN{D&{&3h@3K5lAY(z$@nXh%HN&gZ~XbyezJV32Ntf`#vaO5B~)A z1iYY|7s*5CtQaVR?LeLSsd0rmGvk>ct9_#fKL1@0GIdFqX|e%-41~dZzguXtG!J&K zOovkK*K}i5I=Ha|7*Q_9idT+Mr{u|?UOJ5P4DUe0Wv(v}h&-vsapeA03Dzla0yu3B zL(SI%xGDG?Pvg8P>^&&Ywmdt=^T4Ctwa_F_=V!m7avYUulA&PmGyfaMB5FHkOv)^-QPw~-uQ1?F?@Og7-jOKPbkkF#WqAj`^z z-Sj{hv*MP5>nbhg<=AShJ<$$zGatgh!^LD7hkuEk@(4UCb@7|e1V)fu39r@60CkSj ze@DgHM_gV>;?WDL(>R|upZQ8f&vaquzzei^bcEWxF`mq#*I+gCCZ%Sd z$mrr9HSzU3@#HYKh^_n$qX)1HQ+-MvU682%n@*BB$qu_sQg4@LK!VhBBD z42hM5Z62^8KKtH*g!^^0m>mE&8Z(H}t?f|M(+rE^U(hFaV!>;j6xxLJV9cCCC@vbN z^X)8Ifl74@{IrLCwyKoxe02ltX;;FsaX!{xDlCcnRql){(!qwq6WPQUm#b^yDZcic#`c+dL;1~O`qGv=AME7V_eX5l+jmXb zHt&yMVGx9G^8)D9eWg^VRRg`BfE{#O|= zv}!GUXpuzSJA19~lWWj%c^b5o& zZaEslqNn~)p}!cWADw|ShC{*1`vX|#twHk@!&LHm6RIh+(MVPT8pHR|`5~1wotrrX zje6m~-IJJQmEG8X(FYbySc8E{_N2w1g|XsE%(|xMM6}5e52)UU4yj16jCQAw6E9<$ zY&-7S{*|8N-q}4XmOUukgbha%62yq-*e^Ipi{GWR#6+6m8Fy) zx&Af@6gC0LhHogSYl$I=4J5WE5;D{O(7?ksjBk`GtK{KIa!%Q!-5gmajJXLK-xb)9 zS=PigA`7~+5-_=MKaMwjfC&G&^v_gV_7~@%bJAZ8?CdajnUcm&<5!c>1-kI5rW&>x z>|#RYkD}DNQ!xHvJv1v$0g+CQ33qWdJZi{;0oAYcs>u{EEip#p_x-T$?j)`=T!}wV z3)M_N9*fTBfwD8_VC4%X;`weT9a7~%U;8icIbh8u{qW~|^LvOZZw}$$Df|ik$8Em- z>_-K?-{5nv2%m&2QP0vNz(}y@=vPD2zV(pa&w4nt@h*L^)gRM;*P-*xK6?MQCdzZ) zu0oyXK-<-l$;rwG(OsfUQ}q8q(Nz=J%faWc=F>RYJ3A5wKTc;Wj)%c& zwaGZCe%RChgd=UzLfj?S?dU4Pp`W0G1Wzx) zZ*((SY>5YJ{xM#NRx@<0Z^muk_oMxwC9`TSkQVL^ll*W2x}{YS8{TK^JogPAS*N4A zA5y(sD^|i~E$S4ilG{RJ?Cz(hpz?YyHrh>Q=IEHP#eY^n*|dfH@lBQJFnXBxS|yu| zbKR9ir>tPtt8o~!-VBc~1wf&cD(5I`0%O|-e#9Cp5KmYI8~s}8O1nd_(Y3-J!;O4tl-@1gc{wKxkzYeqvl0n) zD}lVIZ8&+<&D|}V|PNxcqaK89dm@!L@_{J|K-p7-fMb)y*^9{4WW?~$H*dZEQ z)&g3?BV@2L3=<;EVctP!sO%7@?pofQnZN~4FFjo|?VAz*{U0MXZ)zz2w&_IH%(oD| z){3yUJ8gh7IKZr&nKcH-xVshS(K)iV6ty-Sv{4+L$UOX&2RGv{lA5$GGHm_;N(7TR zS4XpT_pS4w(7X^o^A}4AVvvG5fE}OSwJ-%{3O>ckpLgh+vTE6lQDBsVg(xY3kd(JFg z*LMkqzTC!+*v@0CpDQyR=L@J+>nm_-3IlbeKm4RCYZ?7X>3H+jQU0wJku;9oHX65dO@EfvGO}GtL>Gk83uOx|%GM8b)LqpJ5 z0<=1fFst^w;=DW=RQ&b;Zis52(gxCKS;#qCxebf(_orkTr{sw8{7jPFPC)Bs8D4ET z;rVZ9#j!!oFRk$r14Mtp)NjJf@~i>=;eF-UvZ0@E;H!iaEF{^tchos9@*%kRdkHVY zLV@nP*o{85o1livS|2+yKy#A|aG&BMP>FbOEY2m++KZ!m&HW3N7AVhw8yn-sgN5oUSE-g?m?!Me}PQa2ToBWP6)Uf0Agy zqjr$Y4C0&%NDRC++0*4`DYw9muJyqv389m}0jM)jll6 zM3(E9Y%RfsRjw5MYVcX>Oj@L6Mw~tG@YVilLt5=57;32C`+pchpDDF4?fxNR`lSNY z3n#Os5285^e?DAyx5rzCd%%+|L;KE;kT59)>K1YP8T}B{=brJ>;(VA}!?{G(Yjey) zZWocLMI*g**rI_^n&rj^w*pOQZoX(!xb_37GuOu6u1q+r^@F~jD~MxVNK)qK;5m~> z@E^a0S35=7mdXeS+MP~M4|9CVOfS$o=!7w1YiY293gdjN30=!GP<5UT?MZ0B8ucAS zcGL9A>R54Stk}t$~GlIy2>@8vEvqw z4;SIi0Ukc$l|lA3bFALHl=LQ);=bqWcqfD_si&YQqgous+j&M7r)b`F#;+^zQ*VuNj3ooEmM%O(9rma2I=gNyE=1I9T91NKPkzTK9!2UECQ&;8pcwZvQdpPe*WImV& z)sX>nDO9VP1R}eC!O+YWC^Zm*{yUfHkCNRGze${tPw*tcn{Q#@*(wvp~=*YEmNEhBEmX&=jCp?F={o}_vu83dulLp=_yaX;Q-7_ zcuDSa&O)2(y3FzxFVxEtgrdrqyxT>}jF-y`BL7I5@d!7=gLz^&8vKzcKeC~Tb2w+V zP%Bz=E})(qcc@6w9=FZe12Ug7$+IgQQ)z)3+qbfozO!*;G72^TyK^F@X(=+F;{r%_ zvnu8873rVr|KYMQ{H~LVf12i@)iY%bUT_>!_D=@ehbzg488b*$0(W1+S77q%q~Ffcx`-Le;%e^n!v1- zF(Y5$0l#TZB(AHUNq+{-f(~^}M)u!E_$uOuz7O`%#fC4SRZOyaho%B^w?dj>#0~IM z%uM#{*;hpM_ch2kIuCCDkf6;c3Q%hKajLiUKe+8*OHF3#Fh6`#ImgRE#@aCx><8P) z`$Ao?|2Tr9(>zG41b08bl)#hLUr1N2tHzAPAo6L}1a{qlSFpIp4b}IIP~jJIXxcvw zrsB&D81}z})x2c%-g1j?q8C9cOOK&VRV zrw5*$q{bmZAot}Yc1+}UvtLqqzc`jebkAFQP&$cDvDRjV3I$M==8`H!f`f0LL7Cx9 z)`xG30uRinR`Mxq)8E9XFKewaFEXT{XaxUv1vNIUqsIPcNnYFpX3LvVIAL*=9GKk; z>!z2`xwF2I*C$`oQ9*yqT^WLJOr@}^|2uV3P=S$a4(Jga4X^)&!l39bs=xIrh~Ga7 zveWEgfHxn!CGycXBolUSoy=Yy6Qfm!|Ip-+K@?GyVGBG(QQpW5o-uypz}Xg*a67>d zUw9ZA)}2D}o>0hjKMq7W2fyYQ;n4S6xFxq5pNB`m=JZy&?AIQw|EI|ooL&b}w;eIM zPle3iaTLPV4szWH1>F5{5u;Z}N&oz>aPna+1R6Y|qGemrWnC5#zN^T@i|@ru^#M3= zRtKMyo#Y8cyyj;gT*h{ORJO56oJu|lDM7i%5`N>@=*Ucl?G79}M)wScPf}qOVkfhMMlV1|&X)DtpNc7>S}>(VhPfxR308Pc zB`Il1*r~GzZ@#D|DZdPuzy;+HH~lgs_&4xuiyX;VzA?_Nk0vtzrX%}Z1I6}Ep*w{+ zJh0_{Y);~Olpdd8*9JLuo}3YLVecRf5;te{zh9$Kd6~ejQi6Vq^>C4BvnSp!BU@fM z(hkQa(Eccj=LLmWt!=*8{zC%46i1_wVk}1W_=4Z8Qk(XS2h_2T%X-Vka(hX0ny57o zH&*9EcMmY> z5m_CRbqtIH?J+R;2GtT;0O4s9K`~StkK7*T>F!Af!$Bq1Y{7c&eDuZWtY}1n^=xS9B0k>~p-LD$n#*tExw7bGDJ=KQ!ax?JO&vI@KF3HX< zIK)5FXuwFv#ly&YId;*0N#y_a;iT)$jc)} z!Y@dQ%v-2GJ`)TKC$RU$y)f4GD;ZC+r%|K3cmhoe@ZK_x`+jsaT3jl?7bYJ$4*3Hc zuP@wl%-*6a!4K@~p3sVtR%*V&03HQhgOri~5qGB1RK8)nHqTQiBtk_JDO2`yza~dsX7QI$W|cG;5+Rf%#oo{T79vB)R1r-wB$Wo5sLp%NS?9}H=lf}`R^M9N zu=n%4_x<}_*J*N2^&&cHU&V@_A@onO6T6cA2P=!-@eI>~(005#w!AJcb=&d*`tK&g z({)kM7c`eDJoKW$$^AGp^Epwvb{vAM&j_5xmGP!mac8<$F3z8Ij*zk`f_%^MtdRb3 za7ve=UOUQAX4g#OrI-yOJ2YuG|B;|WX(HYC=@J3>ILa}gpIlj!GIB&8J*C{@L znvx?B+_@1KfgD?8V1(N)od){oES`yVVsx6qIe+Il>N?>x&*9IB(qDm{0`Y2dxFA_C zIPyaTZSukd^EV$Sr>w>^y&HNk^}$R$W5EOMrD@b)!fz^GyMdn6oxn=E1%kB9PR1ub z5|j?jf)~%K=dNyB1HXz+~Su;RK8$tw($U+*MAB55n9$Z%Q=rm ztT59k+RM$w4nefUVti#G%^aWjnaeWGp*=n#Z2e_d%rIWUylg4v#e96iGo1gC%#)tM zK6|nrf<5n&S+n25A$L($$FCofE-COlou{%lpM`V$98)&fVllAU2D}`uo4qmj7u}$OLO0z zCQn>G(TuZltbCg}U-OkQ0~y0)Al;2^z0r>C-tM?_Tpk)l))5VheFz7Rq2aqA?v7JP zI|6DT(wuwNtd2n2T~fTpzVW!cI z8NvKtHrJtEF14FPaC!cRce&q{?CEH(_66g<1!8EI3z*F|!00|*wsLA4 z$}f&0R!V88ow6REtbIg|yLv)MMGYA?ScTQ!;yFgn2Eq4c5vFs^dmKr5PoKA{fmY5h z`uWmr4tn$gj7BS{&J%fTlGR|fc8j5Eo(40TG(vtx+Oexo9m9FvbC}*;1yGQiidCcu z8z;VoO{JEQ>QjKVTQ(Ar;uLb;^$6_r+d>PXYIxb*A5imE4@fphG06$H`GYq$qmE`M zvU@{u#^g^Zt@fK(t!_p6Ygb_GYZ9hj?jr*wF}V0M=Z_Rw$mq}SCa2b(B<8tOVCm28 zM1Axt=Wd9^r6#o~G;tGrHZ|)+n1G87rxKNS*-&aB%=#7iLEG0G;BBSI zdTsS&OO850`19#3d_(JRpQ(T!tjN~cb<&6NI-vjO3pG`$p=#!H@yB?5cA)G89=qE> z+9tM;eeIDLJXVfdeTLGvR`cF6Au zmroesuWPtR$y;^yUQ#91oGZcQiK1wS>{6!B{6CPJ6ovga$1ya8%g&v>0>L9Rw)GG?{GLW%{1%}j(vG-WRD<~SZo$7x*FeuDebnA1f`AKheIJTgO z`b@|Hi3!{cSSyJ2AK!#}b~hnF^EA!NTLjDxTXxwmOFS;W9mhuAz<`_-zj8wlDJghI z>i#{439^%Twyuk)utPAs{9{6koTfoBH>)+Oo{3I<#~7|ffCdwOz})g#tjfacAlYpp z==FF;MPkYYU#GkvtM~fiyifi-DS0u_;_lKvj~KwLRnIx^#9S&NaTM<@kwO`bRpg0B z1>ayMgV~A)uq!ZMF!PBf8h3NOtNaEiVSi(ysWkIo@?q}hIGy%*Uc_+M188S&iCK>w z>Bi+V$Wuk*lBblG#5xTSG@j?LS#DNdg!(a{DeH#r{fQXbtGnNC$L!eRZND$HPN zkQ*Z7)7zPFAX5c1{9+(w$dflDpFpC+)-XqQT|(LWC3I134hD|^>Z@pQ`Hus*@a|sl z-yx0L5+mT^k8f!CDIROReu9a&0M$%GF+19TJ-*Zztdk?i1?6K{XFQYkgf9jSM=jn( z)!mS>=r%gkoy6#8!t9z}?mZvSMqd6d6DYqHW?nwKN%LG9Q2l!$2n9VLevvV>Q6nG! zgr;*Gp#;<~tfP^CN-_9AB=BA&<3BBN=wGrHJz`B!&ps9!nkbEOKMCzaO@c_->0n!6 z2@|>fw1}n{(;||IBQbVNH@@L_@Hn;?*XjBDE0w4h%xAxjX!F!3dXk&E0vfF@OVggn zVo7o|dIcUqpTTQ5_8qbQEXTUExImtpo`L6&bonLo9ub>g>p^PLWf1rOCn%0xi{lbd$lD~e=UScdL*FUp-^_Qj5GC7qPYC0Cd?%3(6)Lqnc7eh+aBgbH4ZtsJ}_XtZ9iZv$9FOJkm?0)22Q5_E&>(EJ7~nA0bK6~Pir zSYf3g-ZdQklHb5t+2h!`P#x~A*iGF#FN4DG5Gbe?LrLE@@_D5MTevox{umsAAnGpA z{o)Hh?TkQTiY{ia`AX#z446Y@y~M#~Ij!_xf=4FqhWV9}tm$YU*(TA6E5la6CYL)P zk;gIePu`>F1J^@Fo*Y|oDFmLzNw5lU+)(w)IM_GI8lwwk@zH~;f+{s{)bp7M9Tv;k zX~MIi%{-46)4c&)&%Yv{Iz`}@xE9{)n-AJG`b=-K1zydx1`W3v41aMbb?#P&gi+38 z@8(Zu)W!oz9f9hDRVcagDLixf!W+NIf^I7Q4Ur-_w4ptOqYkK2qeoXY_`=KL?6 zogc8ei{tT#oFztWEp%VEH&F<>L>(=4fxlFj-5((!nnl&<|4f0HZg2!Qv2ie@?gDP5 z*WqcnBIx)XhnA*gI5HB4H=jpiT1ErR73qQ%dK4m;rI0nYm zg z@6H0$-xZ4_^&zI->Zj^C6v{Y;Wn@zougo->tm~NsepmxJlZ+vtJCfHNqs|UamSBpG z7@^9xDzdgij-HwOPp}!CiAB&m!NorA|NCW!hA7OO*INX|55?Hd2Se!nF9q6k%kW0` zaR{^?hC3~l)ZRY^Ox5`CLCJ~ogJ(l`Q4k1El3~Mh_^>!PP0;4oj1C>+sHuH2$}xSs znl*_iCO!|E)?L7#W93x!;uDDC<^=jSk~p^`4_DG}P$c>WQ$C+Sx&94M5~IpgPR+*e z*DjLyJ`rZqmOR13?&0|>X)c3! z*8c^y2syxHgvSb8x96bKqa+;6en;D9T4TSuDEl{*%OY^k&i^>)b(6X!yOFs?D$^E{ ztTpw5{GY!tL*+PS!fY7txrEZ(07i9j7dVa-qQc^>&~iGCLpP3?V3JUqVe=+|LtKj@Bnr}9YpnMW|M z;{Y@Dyb<`9%)(N?LgIcj2$MYHFyNyOD3AL&ck%PPv^uc|Z#+olW=@`@!(c7E?{9|p znZ4j!e}^V}yVG73irpy1BZn{I1iKtsG24Jy%E&WLpL%#vPn?)<^c=j}aFRG1Ol1d? zdg-pGDkQB-A49iIg@%JMR4L&Gtot|D~mIesS(1Rbx zrqX9af1uYRh~&I^L?4AIgZBb)w&chFbPi5u!Uj@7rR*13uh>C6a_{4o?hqQZ;VsUK z5hg+lHDT6|O0L%tLFJaq5)m%D6Mp3lJ+$~LL~^$Fro48TS{DjsTPV*o;W4I$Kg9CV zr#Lgr4~$CBW0-b1t~xIXv39DgUR4zMUkstXAF{yZ?>lgklVf$STF~}@rF3WeYkc)a z4kkAy;EVtyQwQCkO-7bIf3HJu>9h<}X#Je7`4f+;dk?~Z9>_H}m zuzIk3w;wI+I*rw`X7J|Eezaeuis!yXfWt`<=1tQGj6T*yrNgsOzvmKIl^tLcGQ!c- zB_5V_`*8fKdW;-Wz@q(alU}df?q#H{(2FBY&Tv6tV zH}x!WnV|&Jv{98Q5k3m;q!nJ{Jh@#fNDu>10t_l!GdTvK$} zKT{N#GqIZ)2lE5i6wPs_a*I(TUYJ&tar>$cX>R|RLgd3DQLG>G?Zz|GQc@Z0Ma98wfw2$wqz>hz*7&Jr51`YUlVJ;*U8+R@Zs zmhIHG;JhfVIQsb?5h_Td`RO`z+lfn{IzI+%>uh1h->Gz|Rse)O(O@hz{mA`+M2vp_ z5wi3ZfED|P@hQ!?TKEXaM(rk_;%1<5)Ees2vj_T|CerU=7paYhDjUDQ05zw-riG?= z=$ZcxqRwnDB>!sh-jO|6$6Q6vX+=~z{v?K)%VGG}Xgs(l54;bK;J35xnC$i+?_#$+ zo1Afy>J0ahLBA0CXtokNY`qGeR6i%z>k~0zcRWqVd`>H+;<2Lq0&08niP!CF+C5K! zml6972ZbBRH_z?h|FH(6v+dzX*lx^mUrqQwC!vP4{tz_Tb!F zDN;+iP<(!!-V=SnPfev`^GP3(l| z?sifZ>_@{#3W&%+C>dKM0cAhu5}(tFC`Bc#`Au?wGwWA$+j)EIjKi}#)chr4R5NsSI(eKZ|ka7^x))xXh3g+aHFMm$v~ zi@CqolPtP}W>l@Cij^;+KldE87pS7V(p^+tmH;6`FY(Z+e`MO|N?!4F%68w<$0A0%}-q`*-ygBnzxxVuh5S_D{c=>@}2$6)183F>(Vp<`taSlvDgyB-8#qH`DZ{4I}u3;Q5oTmvS|p1>Y! zR3wkysuHK{MR;{mJtS@7o=IN4r(s4iO#i_Euok;Vtv1WR+^3=7Y$eM+HrK_d1{*k& zs?BU3z6bjh!?5VZ2DDlRux$ZjQ733G|~bdJAt(g`d3HZ%6=JMnS7BYu^aVu}2H!Bp)8;$B_~^+IyY9_=nj z&WMNn&%e;V$_PYv4+>u5WVZ5(4sW%SBN5^$3NGq5lk@vlQVZt@f!3>5xY(WyB0rb$ z#)cFb5%q4WcE^-7ggi z`BL97=w_5P9X7;b#YPhG^gB`1`wy;W*pg)m>v`Iro?&plBM3Efdr_6s=qhH%xSnvQ z=KWbPXh^Zq*_X=!Ohl!wHnOcu6~mU@het}s@zb(G8XqRcmh29JK+-_1CeMMevnO~9 zU&@h{z3Ip-I0lQZJO-D@jgY_h0LNtt!S2h(7`C$)c8iEG_E%TJR0|W3o<0K|0-I<~ z)jP;y4XLq%9$1uZ#g-u{T)jz(-ISI}x@~u2hN~l+E%lph5Q@e17mJZyeThx{*Mu+wc2+1170UP2gkw~1)A{V-M9 zCI$;s#2MGqZgAsn7U(+6hBJA?B+G~63e9wc!VPEnB!o|oxT>MA*MAWH%LDdoKM&%a z+vwnn4rpI*M_q;9(1ONWP@Tq|z3mF@amP9k{=EqG4o*frq6;hLrSO)?74dILo?)tH zZ^qKvb+E{88r!;Y0hS&cBxzNbITr9cSdg8MgN>Cm^S2g;4oUK^#=HQ>LhfFr5r7wD z%pk$92!07aLACgPAR+hZfeS*^*k zawDIqY(Y2GG?Zd@Y!;!0vD4A$=?@})>=w!Y7mqXUyn|4A7p!caMLj}{*yF3N;;)Nt zR7jzZHqAOGIQ;Yxy)easTE4jeVjnADTEkTQQ+|R5h|FOMuD4=*NDaC1U6WN>F&F0< zTp&>c$3e+`@StWXAtpbE(#DR?I zOR{CWB&%v=i;8=UNaECDnq5909JUmK{P$$|`ny4J)^35IHS8vq>I@O`q6mauA*}Yi z4%I71@J56o?8MAP03-Mc6|5C%nkgiGpd8?}^IIL!hO< z0X`N?rF!p8a9YVAav5=fT}Tc@7oFogX2NX1n@aq|l^eD-Wx>sjlB9xv1ulC@GFQ%K zk)_h*JS9d6kLYM)sgo^kd8|pZxpQK$_#^Z?WTLcxE)Jh>Ccy*mVgF$y^vq>q*p~~@ zb7F~8TrMqq%ooJ*E`W}29r+VvPff4Ov1@nBvDT}_L5y7swjU3Z`Pu2{cEgg@h}1#n zgOvVVe*psQ3}En`Ffm##%Ieks2Y=ZZ5?%moR@(w=@26a@-r^A4{P}>JeLW!7*ATUw zS(>@#_}tD$dsajX(3|7C4@~2@TsFJmb4VsAM<-#!RC}gAY&;?MPq8c~lu9;7qlm#W z`pjIMRX)szm%o#t-P05FY;SR%MpriV$9-70a06W4BSyQpzMk&*N;GIzV;-mmK*L!h zI4O}y_B~D^lxM(no9NJ;!(*UT!}U?c&2Z#)8$a*Z1gsuZWP}t_v1ev8S+pUHS|@I$ z&lL7T=B1BZpB%AdZ8|D``6K9E`j{B;q^$$W) z-VB^OS(aV;ANQ;i+=a_dS%HZyA4mWG8A#R2)*IIdO>ypFCSF{{?F z<<&F5t2%=o`D26QpFaQx(d#rYw+O72H3Va7kKoPL`?&7bCqY|5A8tIJVEuWHBKqCk z$lj55Ly?VEf=wG%q2t6#ToNaZBcfYz)43r`8B<^!szT5!K?fIVy@T~N8C;g)JlM_@ zMT;ZcjLMqJ^<5HXi@X-%zAx{|UT$`H%~=kfTS#M7E$2O%6b@UbCE}6&dvW>uAdK8A z!k8B>Vz%FLDHS7})AjmMY~^~oZsIc-&lN-D9IXZSntQy)MWZktK>?^37O+b8kuKF#}j9y%I9?lEHT09B|)s1M6EGc!^w(Z^AS~ zjIuOCo9Ne8uk*f;DQkZU*2(5mQSTFElTIA?z1<9p7cT^6r5l<~eh-o}7H~6iPb}1| z#O?REv(EYk6v$@NSK8cjjUX3y&#r{r&78Y!vnzz8<)GC*X*|beH|*!=u)Sx^Av1WF zAgFmBcklaS9W=Rc}3`P+>X-IPSn{aU6XS0k%Pl7_&eh zpp#Y+70sm}mc@BqRyEM&5*y&&-zhkuLWqelOQh@f=Fp#4y76YSF)lse20!ASQyb3P z;!qR}xBtz7h!H8e@yiG`o0$dQB)^bi>(9i_ONWG=y27=r20`BNIL>TY!?O_&6u5R) zk)QTW!2E55>uZ(pi&q|E>d2l&n{i+G3Bn-lc#AdY8Q$ttRfAnlz5<0v!!7;3( zsb4}!3(SIrmxuYX+peH@7{}5)91E_^@2KVj2VC4_3nEso%pDOK#^KZ^l90Isykm2D zVfxR>sO2d3e@{WvJevGLYYMItz>iMG7@N=MvErqkgk^t>F^AWYp-Yp zZRgb>UTE)J)@cgRklCz@zJPaOW*}Y&7iXrVj$`??{$yT3A$W4k)_Ha+%-!co_$IG{ zXvH>DhnIf^c@1)4dZ`f;Bi|B3?}?0Wk0vYHcZe;~*2ODoJbG~G99^GukTE}SjeIWe z=DUtfNAbx;yp$+s`q_FlQQn;j{WY@evA42Je|+097B}ziq#a5Z9(Zq6 z&OxBqMO;>5M&@1SgS(bF+gi2^59F^!oqJK>6u{+CKdz!b7WxpbCl8L?9W!r{C}Z~L z2c~{##r)ou_A`)x$z04FmKWw4>TNPQ2_g%1tcL_q0g|T$veo)WmpJf+vh%Mm3@l79tpfaopEqM z`v<>u%5}W0RZaHHR0ms|!_53Y;jGQnGt`f}k9E~9BZizmF5f{FBKG-_*Xf41DNmlf zk%+)4)gSQAk_ssJ=nC4gO4vDZOz^279$iH&Fz)S4+GpKMCbcvZPqvIF^yN8~-9Hn1 zA1u%zn!QVvjYA==EQizWq}cVheM2^@l(xJ3PjFajqV!g?2;A zk^|KFvWNhBXR^l@7SjriJkAqBKw~QRY^jL28*)ImYyf2V*YSMJDJrtL2PFf}kjUo@ zGk@q5EPA~H^tRRDo8VhmddLVO%((sy9wt{VPX%!+0!xDcOSr&W-l<8LCEOL6r8-lWsE9yaJ8l?e%=y@$%$Ma{ADl&501&yeFWy6Zh(fZ z%h`vr@=QkY4H&I7B4Z;ztq<+Wf$b5^V77KKEj7$0&&GtAkO?d~Hn$S?WuL&eV!~j9 zK3K1R7^IG!C%)gWQ(CKuDuE~HB>DvGPMrs{Sw^JbM-~2)mcic8lf0%}QC8me3W|B2 z$E>;5yw|>BaA)+y+~a#6L2;ll8m*2*_lnK9vfcu0zs({`{-$F3xv$tR%cA#| z`GP@r2J%OSNJ#7x+9G;akHc2NswDR5c237%9XITKx5LvZS& zsqi(koVJD=qn^k^Y+00xKk~$wefmeRarQ-A{;vgQT(JPTr3bM?DH07=JMrvCUegE# z5mvtFJSaTYCQcGoDA$`#!tZjwYk7JkJj0oFxUR-T{+1=GQ(g&V0=Rju_Brw_O@h4M zl?gKSfVJ8je@N&nU0~(SKG^ymQkwMf{Ju%>>DFobbk8|bTQAQBhMK@JD5E=B2cuy4e+tpNgQQI`?_dkcNT&DiEI_&LpSp zpobGi$rP6XE+gd*-b#;g=$Sq%Qa%e+HDd5*{&rr;8C~YgMb3$`N|Eks$)mgdl&R{g z^;ok{k9|2~0K6^!3WO}1QKtAA&Vo0BQ{yhf9P4bnB5#NRZ2_po<&mi686x*ZnU!>P zh5gw%B$htrU-iO-c3W${B{8wLz`Lxbvk z^t;jtA^RsYyT8~l3J0fvx{3+v7hQr_y=;j3a*TM)`@}h?c-X?R#m(Xv$jV#G-kX+( z>Dt^mlu9w`V6@pzq*W>23 zTliZQenQ2zX2Fm2br5#M02fJK=eW0(yklz~z~?0w!9ZjZ1CpDd;n8{6G})c{iuUne zC=C;d+nSJ_$|KwVXP^0oBHOKCjSYDb++J=4yY!|6tM0iJQs+Fz)lpgCynY(}>2Zs5 z&RmCzvrcGq&XZDOG3M*qvsCQp1^7Of%XA5Ge&fE~7<{)D79A0wzpoqa>DTAH3+vJC z=R~qr>N=>Y-=GB&b=GS2-@!$U%iY?tcwxr`X1{Yg{s|lt{3%)tKXxhMUv~r6U7vfe zeZB=7+ov%D?-baKHGK57cJE8rue1^IYISQlDT!*)v?t8ljWV{?fdUy)fw}*H@DlCe?>p;n(#DGEmeFQ5y^3 zR!=JL#i@Q>BiTp==DJX)iQ5! zj>HKlQu~^Bk(+t?Uid>_uPVc6WhwM{90b)#Iv{dY8-Jy{qlx1%8d>&{#%682k~$GX zR~5la!+vP3H->v6-q7lzjS9PWgNV;|;&Q2tp4(iDIcCQ&holMWcT;W+WTaAyQMR5>oI4h5oFJB9{e2SpUp|$DMX7sfmpm$x;@XxIhFuT}DRWmn$!ZWT%S@#u7Jj3yiUJ?lAD`7_Q zcIatcMr*zcqt&Yktj-ZJF#abF4(al&-nX5o)^`}!Yg{1>f5x+KxxL1jX~p=b0>Mt5 zfy7;b=-8Kwp0msttuGVUsjlORxASvSS5%1)j~+$I+8Z=|!#EteEW!KKF%b&A6)|eA z2>Uvz4*WLTFug4~aQWX^=$p2O3MDvl-46+-n#(NO_f7+%F3lVf5dr_zX^`Wn&y>B{ zKo{^fiHnEzKo7EQP)C>;Au1BB#Zl3Ex(o4%FY2;!J52Tb6<{fh7) zO^1v=PUoTHKf2(79cJ$hgpZ>6Wc~Kvbjt>9R=F{jYVH`qN>w%DtX3va+#5+cmzY87 zqd*?4FoJH9L9@R%3dV2SNLMNX*NYw_s$Grn&0rh+`EJ3k5ME3_E;kpDh0n-^x0GJ| zpavZq|AP6frw(iB=`PojI!3y9*f^NC3oFoElCeHJM}oozozskIhM+WS!`|1#9d>(cz$ZSZ+x z6mgCW!Nb;~__9Zx=dzP??pCQmmwP?78vDZegR_`To9}|#s1=$804;cN9)%SwA#@Xq z;qt1?3!6J+g{Us$Rpkf+LGyVtq&!Hi{wQ|8wiql=E=xLy8vK*+@_iuQ_Nd|f0y8kWXb+5TDWZmOmv`ar zWz_16#g|-`IkMmpgd8=ZA2)G3Lyv{%@ns@Ah3V&Xy&HlG~**7#Vj0vnS0Vqw^la%SE~%D(VA4dMmiTav6r#t)!3l-@)O&x3r|t z92H+QA^UYJ7#oF<{>&mwPPvQq?^AJ)Nf=6Z%HU}K3#e2*j;ixT@ZP%H*#B!j;L&9A z(KHr!s-K2BgC1Hb=0TZN`(VNFbyzTK8u*8A0FM_*#3?2Ls&~sXX=AgQO^K)QsE921 z{MLqer8}UF_o(f0Y37xb99vcNi@MmqqUz%w(I4sK*?{YNz+Y9B4VMTeeya1C&Uym)`Cj$KLen;=UJI z^kIiP3=5KJ<~R*TuXX~=v8_jUMUGF}sle?2$OyhJ?8XbmtFUP17VMwcLmcFHFuqP| zjPJgK^se(c()m~)*0d_ozh#6B-amuZpH~SwEZo@j{nvH zUU7w}XZeMf>}ShHhD+e)@jGbo&I$0tt%=+@_64)f9fLM41F(Ddi{@^tCoiR(|7RZo z`5~NpevvZViH>IG< zkwLO@a{~yi4aGQ1S++|z2JX$%XWl603yO*_Q%OaZu1vT^zJeX?ysyc8*5ta7wguLo zdW&(LHp8aGio$`N6uvoZp;jMaakB6gFtT`#k*Xq)f4d*d^?7*K(GG9Q-6bEu1Ft?z zf>|m5VBX1G$lvKq?Rxm=oqe0er$#}7-aYg(QD-v@qG0^}F&cNE2n}t{QiTFZCU|Ft zfORcF;P+s$~~>wB@ZRAEI|Zj zy2MgB%Or?U6UTABkbVprY;=a#WDtsE6VYXSi@-rf z4kP}%!SCN5i7iVzQF6954X_eo-&M(@vd#>=F+~UB*PVs*1yO__?gCw%?{RTz69~mD z0nxex2~yRk7#@A0w4RF3tCelK8NbkLE9yD)rRD8kXVg2p}8D3kUG?_6yrS_!j= zW=|?=NQ$vqrW!2F<3q`wF_PGHgc0I))d#+w#m)9TsCD8Dp6eAtrxj9g=cqDDcoqWc zmB6oWE!O2@2cSv#J{Xof1=IJN8JSlVSajNt7Cc>Iyu$)WIEpNVLL}T->GsQJsi{rJI~*vb)qjZ*~6ST&Dp{JX)LD>l0uCB39h4V zdI5xlb}1otL2?rK#z+*6U>8++vx6*aQDO_?5+G+$ z68_UmhjUYEkjx1M87)2)I$I>zBm30aN4*#q?BJMo-}RX-17YyZ<|gl(v^U`8H{kf= zF8#%wA5V=k$+LZLVbjy;tpAWQ>TBM_Lz>zEvl!y%ag--p>CHT{Glfat&7t$@N$zf% zh->FXVcwT)o^ntH(NCKK7e7scDW2!Z!z>=QsY?nTVc?=H3fPW|G1 z5qVY_1cwcdq7*-xf8bZ8;P9ak$aGD?f#Um+J#U!D&r?N>(N-)FPvrf1#N|OV`;a+t ziB4E}nT~nhg31Uj)YvY}x=%jA`!`pe9XqoS3!SI45%%Bd)T5G6bgTm1qR+s}4TBup zY5{1h2m`q{YXy6xx6-7UB8+=hB3!A=!(0g!h^#jPsS~mAdb${E@>&h0`+;3a83_Gv z2`Gn1a&|dZE*)Jw&Mf zg|(WW2x}a_49*)}hkaUCh)B+4`ZHrcol$m+zK|6_*Y<2UJAEe#cek69BU#3XoCK=z=N&=^DftmgrV7`wQ<89SWFZf4r_brZR@bwPvKYxq9xjahd z{(g$%{W|ge=qTk(puAMmFW5C|fJ<^t)33L+KzFex?(3cmGi+1@`U{q0Y1SkR7Ysti z`){z~S^{M{svytf5>y>jC&FGMoabyY`N8!Xj;JcJ{0~P^%_0`XubQFlu_z2Skz$B4+U*&E@}bx8$3+totho(M&fEuo`CD+WuZH*LwImMh`vfQ8 zD83lr5s}@qX;O6zb{J(~1wCPnvnl-G_Fp3PChVzbZC2ySC*I}7r6*{3q$WHRvcja8EI7SB4)mw3#BcnSOhuOh+bB+PYr7az zTyvX-?XqBktc4g2k5TIEwiq5b_<;5Pi#X-`Z(MD)7@Io7VA{^u?+wEeIRWs4?$mf4dlU0Hn1QXq^z~@MC&)`n3hf4K6{a(YAt3z}M&;lmai8oDNpq{yLE zKWeZp`SR?Krr((J>njX>xDCg83gOX)GME+~kDn#Y@L6{!cGoUq&)!mDJsyeS9sfoo zol4~KiGLt2GX_r+uEA=lHaKE8LX=8W*s1y)6K7Eb_~xtwRg?47BH{`brz%qYrv_m8 zl@;*E@=U1sh&m8{=YcBWIy~lA_B_=N!Xv=hsQWgN*8Xg0yqZhqJvBwYUn0zmBb|^~V9N|}x$=mye44)1nRZnbL3~C%xq0t{ zwXFi@S%`j!&ia3;OA;cV^CItEA&T;^=WyIL&bzZdl;nS(LS?Tv0cl?ezSbg;`b~_@ zy6DDJJYo%EBaV=CPKQ+-dx^E-O9fm@1m|4p2K%q4vGo880}H3{`gd4?vSBe6dAIUX zJH!~agq!0ZdQF}cKcvb1i|~L9AC8!7@oW1-pxm+*+WuXK>!QD?RW55S9!=@iu{h{4 z5@#*{UIZzT2$=TTiXC{JM0~is@7Ir-NTxRmOfv*nl%&o?7Qet{ugbZu<^Tz3G$4}> zuEoUV`nYcYZRDFiC%O)yn5YxVo7VA2a5~~Re0*6!r_51j9fn`ywc{7zdcGfYyIzHL z+%8>Hz8q2{YKV-{2{vQ*Gr{Cd6PWt=XLQPmzqF@s8h4KlhN&m-qfbX8H2Rdlx3L!9 zvyLisop~I?j18zGk2{2-CWEeZAa9!TXRy7V3GcnK>Be!T@bRu1JlC59bCxMpH)r zSSU)?9WT|&S^;Johe5f=5$vXYrNyUvq2p{6h&y(Xf7UW=z)NW=LT7_d_bW2vs<4!f zlCZ7j{(T;P0bc(5c5mNjvctsGWS8HT*(Qg!`*`ozzbr&pNGN3f|IfcOGXD3=)cAkC zOiv$=fdBO(h5o<(nQ_BiN=M3_`)=y}-!Ic+|9(G{ty})rKmX^O>Mr!48Jw37!JBYH zm_JSt=d^J<&ZDBl&Fw4stK3BY^jBh1W+ue_u%{Q!EI_;B7c_MKPWrcJKFl@yL{x99 zGhLU{K+(zoj$Pk3sa&Q_#Pu1HC65X1A;iV}@h|f->WHu*wpHY@I2L)p$KzX#0z< zcxaC6ZNi}A_fjx@`H$Ke--e=}jRLn{>F{r#vcNfqVOyjMz`1ZkDV08sCoNQ2?|~<% zWE%ldtCbiVxft4M+e!*&r$DaVH}c2+J^hgV4G-sT=Z&m-CQ$ou0i5Txfrh6&f0~sw zC?3p$!-uwzLpLqS=dq`#wE7;EeYKdDSPoln7y3w;iKB*AmU;^NL zrr%t>VZ6V2%#b0345dP8qVl<~T`EILQ7NPWDQTi;@J%IS$dFkm6hcv<_}tevRSFrR zqJdN-8kAHj>g;pQTIa!8>-T&5d-D4OED!ej?0w(Y^?tuzkM_{(x0;E@ynGU_ z-r%)0*W%R)lj+r*mypz<&RcPkdqn{b5xhy-k(!>FvzQx9ORi@cz+Al)gqj~hxbKoY#_u^uo!)%H#M|rn>l?4qZT~d@20NqZA1zbq zV)| z)K78-@8{A6{cL(Zi@z+_cswWFZW=rXfg@541ise^ueb0EY+VN zYTBYy0jpL!lH>TDE+AjM`EN&Cgj5V`*O_XBkQ?uzBq$t6;MFM1F~Wlu+@GY94C+K zt?tsHTV~y-jUq)*S-S@AvxziF?*qDfM<8#7CY=|hPW`;KncjrM{$&!h~IJ){Z_Mcja^|RZdhPyAT?2tp>l~vGs z#ffiQCxOByvM8(-fi1I_G2y0`%x1}RAbWcbngotg;rAE$FQPIDIZ{lg56xk>SlOW2 zrywZQuLQTnjeL#pOK8rsV4M$Il8j5@tf&noY0_z^Fq0)pKUG-yMI0NRCj%nam(x?e zVc;N}43|A9zjAj5taVupzCJee?EXG#+@66C_q?TNgP+o4Mn`Z$R|@TLlOc^ZC-F_u zWE5Af!~98eU~bM12#nzvfzGD9y-Dw=Y0!2mSy2zhf)mNNwS>NrS_t0J5$JoWgajU$ ziL(~H$ASagPEPay|FB6t)@HY2-DWAC%Fr|Z3NC|w`g0o{515BuLhrzPqc0p-rojf; z{)17irOc&7$#TmzPjG!h2&`E2nVPOo#Kd}zd0{)7tyb1&Y_{~0TaMgWv;QcZy!{_E z1~Bk%rUXdG-NK!hhWMteD}J-TOmhS8!Ss`%^slNo6n^laPwM0u=ggPHkb7T)z*yHv9*CeIH!hF1Q*~H z;Y+Cb+7ESETMQMq0hyeo;;bv|d$Jzyv&rO()FIF)utXfB z#LxRQzjt0aRU5AYSKVy>9j8PRn5l{}%r*GV<(YDwWY|ji`5-<;4dQ(|QPy_{E_BwX zG0Xih!k7iWpf%7h9ZlEa89G}dh&^#gALce}q9Z2*Xc2!KgdVk~W78^0Y@P(G^wE)j zU~&mHtXD-^{}6_gP3amN9SE`94hyFnu`72Zax>~P;8*J0j$KlB^e&L2ae=*tup4vOAyi2QS{IY4-NG zg`LiR%Qy|>2@gJV{T_ENU;gR6B7a|_8XTHcNcA_U5{J)yq)itfa`Fh@{@nxGzg_`+ zMn~v@%L9ass;)RX`T}JeBGK9F325y31W{Ib5ZZr;Cflj=hWb4bYBM0e+76|4F44uy zgh-CoC-7G5qc$9HxMqbu`}g8Sya934Ao2yxDNRLv!y0sy*oQ;X(@}15EbKDsqC)jv zplx1Aygs|Ypl~M@jje^{x6Y8CN?vr%vwwJZb2oGu36MySYt(ygJ~TZQ!)cjQu{l$e z^}4VONQDb*J(mRg>c(-%u9FwooJ~qzaUpmy9bCF!8UA@D6Z27fxL+^92%b6yBjty< zZ}vo{z#tVu3K!wxElr@W^9pBmZ(`-nmceT&SG@3Ejg9Aeg*Ti88RJ7&aV__EF%7xc z>Cg!3GQCt&_W+G5&V~96X?9QfPhQ8m1X@$($cpn@i1b2nCik2;_>wy~WHk{6XL4DC ztr5^sF2c_7cz}mCy@OY?6QFqb9N~DY4~VVqp!9h*ajdc+@`ZL}0k_w5bT|%w z9U6(%4+qTqun9!{2B=eBDrziK=JsNPd<8WnJRI~CtZ!G))r+U%wm0YD*L4w;GkIj1 zZ8#HJ+a&O$mk(%5=s>)pCurH7hSb>m*)U z$VuY#MT~0fFQIMO!myxM3{i}7cWGVlI--GJnt!5Xax0|Qp9fWwcW^RYl{{NAty zJvfEx@N&^>MFSDI)rfC)388fxcPyOqmnhZiVL|^4$R3d7znI4Hs>d(DkgWmvyu%I} zS0<97`i1=BsmXk^+!45Re3VL6-sKPQg)!RxKD`j6%RDVf$JJ%?AmQ#kGHuaJx=pVe zUP^z%vF+k)O~7%!){qi-K8?rEr{>)@Ty+m^<0YA&<+JheF-xY~OatTQ=79f;A!z8x z;dU4uz`?{pYC4~+UoXXoc~(&Q-z#a@*G-FR!Z@z){!Q#q^(A`wXE$$|9Oq-mya=_& zn(3d>=g@UpfzdKmtB}=5roK@gxKbkvmC9P_?zgd!{ks+Sed@;cAw6i}+)uNg%wi7} zPX~F=cHpFm{4u`~c(%@kN_#XDza45cLe`xLpP7b-gZBX)t0OzlOT#tR47?r6`2Fj1 z&}cglH>Cmo5j|b(4EM+UDnr`%O%6QQ6DW1rP3JDTOaun%Y5lNc#j3vzJbivA zy*pc!X>$$6R-qvn2tH4^k{eE1rAym2|A9bi2z;8!-EpowqlfzM@H>n?!qoai=&}ej zZ8#yo{(4eRE<9QR>=zHLi7h}gF1KJ)?0_dezQXWTf}Dp-n^$eOo6`4-AjCHcz3z{I zuHsbojnEm$+4~3{U+spOn{}Ai;|ECYjgO}FR*^JjX&kzoorRIc?x=k}hS(K+z-fU? zsof1DUR9Vl$9caEA2)|FS+XqY8aV)p=RI)4S2KLUu@t?9TjO&-qHa z9fV%xSUZ1$cp9Sj>3z-_lD}4hUBdb1R;zSFWcUSmVr9zgZTZ04wMiX4x>RZCzbFVk zUx>RdNU#o0K2Wy(HyTBp1NrdBWaRJ=xbBJK9QR{Td?gYO_i{6f#dA>k)+A6dG-5aH z)Mbk#lfZA`B2w&pnb)*e0>XZYfpLK{duYoCSSO)I#jOmPOo3Vu@0kqM`Iay`QUOkt z1w?7yY`)r4E*o7x1}PJLQT=f&RIaRnC*ynI$22Lt`*{YIoI8g4zHZ#is0WCr3rY3a z3(ap*xSq;P5|LyFV_h9oMffj@4}PQz4$cJsB#v=<$sP~Idx53>I4&u>jz2XiM{^mE?9H%jpCoHKGLuQvbH?V>LLB%TOvbYZ zu=B!dy6Kt#BRnOA%axrc?+RzosJ?sKAWWOHDsfx4DHgHTwt#!1U<0B2-y-f;*zLBD}{_x`7Tt=g@o@aEj z2+8bT>M&Al>Y-9h&D7qLl$1@-c=R~L3#`ZJo15X8VHFA$2GNIR-+2~Oi{V$B3CQof z4;7LlJhrk z-|$mdvPzn1`&0tgww&dhv%h&EaYoebXc{hCY=XmYKSKE|f6SP+9!)N_ktd%n5Q|wR z%<@;E__C@BCMPB0mk%mTm4_huAGt~k`$XB}Yo+04#4^&jZy2UHKf=PiB%J0x5%X>@ zCcO_Ip|SWK5IdGkMEWj~?znt1vt|M#t5?bU{qpmr3;tKa`8iJVPzg z>*2nTD3h9%jWa%`L-N(TL_f}oWUt^nftME14w*TqJb(IKFTVsTa-Vf6)&H^<3P8VOy znsY(=M3bLm}7m06mzfw}*#9V0Hglf^bCA%1BOjBnr0uhXJo}%2 ztHeM1MFR9Mw9^-TEGZg(PK`suv2|SCoZhrl8bx62vQaqO*Ys6#r}DdoW8t=FS|@txv^nxkorzLV{^^;8BOTH?(@n z5->Z{MVE}Uj-eJ10e9?BgwnIhaKbIPvK9T;mRaUSoI?s%;XH9x3k&w z@y6LKzV5;X8y49@Q^+iq#NsvMpl~J{8=i2EbGp33LkB_Z%{9175Wj4Qp<3rcKvdD3 znB5j)#M7>jH_7eP>(&+2rllm9Y+^el8tAH$SW+%og2CKydGxFi3?ywO0Sz{gsJNRb z>K?_41st=cp&P?_`3|O-w(68K0 zM(8ZISl|mkkn{eW?0<;CZIdv0z8EATvji_ z;ZK>U?plE_Gb}NU^IN_?=X`5t679md&JVys^Z*IgI)0Q=u!%<((H^xBk(SlBGa+J_iJt4}X)=bgi_XwM`v zr9c4MpdV<10CUpd7YG&#VQl|z@@#1!`8nw<>~vtr+J_rpPM;kvJug9Jf5@O>7|ai%@=cR5=n;R zVqARn0jys1216rmq0n_HHt%!^EbQZY+~#9c%6bHKe)sZvI5)S|s|S3iZ-Yd4SP>)M zH=fv~wuZb+GI( z4`w_xhq>1#vf7Q(wEpuj^j&S|`G&}`4$~93PF^!UQHY?#sT{-&8@YGO1$-xY5#-Bt zSgHDMT+-}KT%W#&EV1_3S?R^G4xuN(?=K~a*ECe!Sz_G$Q(ZYp= zaGA+W_A!q4@2kM<^WTctqRQwWDMY(ViX_@Fs17re0#H)Z4I+#s$dVq4e_TwM(|hh?;(y9^z3?IK z=@4P&`-_8`=y_0|pik!?zs>VjXGvaWHttQ@j{!IT!_`NY63ui$M#1JRB-rezShA!N zuk$L1jEWV=t@Z#-kq2N@b`oap;d)*H5Aim~A`{r;f?Iaoh5iW}$(MT^f5vuiu{;9)?yf5Xl<^`07AEV|;o$y*Oh*T=^ImbX2udpc(9oQJ? z+_#i^W<}w8ku%6%oQ#T{U*LM?HYmLqLa$EMVIZ#x%sVI3uB%e~xEKd!^5v;G(OC;# znrxxrw{z)+R!W{vRb+dU<}y1c>_OqVnoJ~rB`!^zz-;$B%O9Ao%X&Apjd-<%Sd6EiwZ>?Fp<{Zd(h3W8uW?lmk84p^O4;l1wI@z{a~8V$!Z0(sk=J z9LO)gh0iBqqpm+D>4gFw>7y4+r$F)rL+sj6jHWN9vIn;4QP08*YW~oe@#1ngO5g6& zmXsB6DTd3UiJSwAB@58t$9(uTy%?(H3iu6*`%q6`kEqU4W{P(m$C1KK+`KM|lw=m+ zjIFof<|cC#*`kLecqbm^W@%)Uhn=hCAnou5(o;}H1`nx0vWEvb0#h>A_+r$xX{-5snmXVC&q;VmIj?AS1)!F8F@*@LvjProL@{$KOBUIg*|BS@+1H7 zo?Rd~fH)Fh$az$5f&GRK`ektqgw(%+RZmnc z7_Uv~1udAJjO(q|fXzuP4K>tR85^t3LvWvT-tpFB8feT!Z&e}^i^&ER)qzePiz>hj@Hq2l*I60od+$$~g+1WvU?Dn&Gd*MZnn^Ho2xh&#wxsPZ%sf61L z9D@Z@Gx3jwKdO8>i7Mw^!0D|cug0YcGgRln=yDrSo>T_jc0u6f>y8}`v+0W(&aWV~ zlHal|kw~8m2HO{x$#Kach!OFin*77)-DHe6xqJV&_kxUe#VcZ7nM|b>*Rt|Hwh-S; zK-a1Oo#r2a@OC?>JsUzr9!(@mwlzZS++3Kt>oX3&R%gR5ujPDVw_qsd71UW=1LX~W z(d!X+PH>ik7b!15J!BIVD(@vNUpTGL&}}^GBg~5Z{X?BDOv1xVC%^93G7uRkhk^bY z;(9I?h4sUMPW%l^*oVaS&r{BmTV$HX&1$~8jl#R+EpS|UCyohc@y9nc(qZpdBJH~b zj#WF+t| z40>T@VDr%)cSbvcr+yC=(z0VdYlyLNUkPq|E034B?7y+nGA>`$L3O(pK*&5Vnjprx z)#quj4ptMG>~H`0Jr1pKP`JJ8#)$&~1V!>qjBeM8X8(+$E zISH)w#*DWU=-!m|xbxy85--$;b5{R?D7hS{79R)qqp=uWGDy`1X7SrDaK47yHALC& z3;DAz6`Qt2;qhnoyc6GoiQ%#VUerr5wr2Z7>^QE2<)wA}6^dDyu;C0jUzGu>Gr1k{ zcMnuaKVy3Rpcw66G6qCUmT3u8;21m&72Vo%SiNc8B*M%Gs=02Z-$o0n+;WrF?QWxP z!WFbn`~w{Gjzr1q<)~DGFImkbTp;m z&nzC~1U;rVW&d#JJ04T^vJ`gxUX9uQT`)g12&ODHXO};G#`8!Uhr6Q+Y^D1(e0@9t zzqLjX;r)M!>>>?*-{!0Ed2tnS)fmEAPhOz(hkLxPZz*8$n~xu2rl40zCVkH3^@`Uf zKx$aa-G46NH{+-1 zHzNhQ?{PVnacho0Sq%GE@Nuh)Fx>b7xhPiu;L} z?)V4ey6a)9{C9k|A_04h*Dwqw(&X)V{G^^N(0nC;m!F71+J_cc^R=Qv=&mmYPYB?c zhO3E=JLgq?l7`+(K0?Mk4KhP~8!LJ98LE0pLehL?cKOwCRK5%GtK)tKni@d@ZIVz{*0};G%m~*EHg@R zV_PfuD}3UIi~R%n(G+@i*AU3{$6?{&ha5vZ9_oJF!P|mMp=G2IUx+u*6!$U6TG)?A zu3dzLDXHXX#uS=iH=B9--4I>Pm%@^%zL@uI2PAoO%;|7SdXx%CYyT~dSs=o?^=_lG z(o*cN=x8FNa0Ybk?vie?VPX+iM*GeBy5#|VR=3cF)xKa=X@;8_(>3B2KwEtupYz#R9LhPv~!Xyq;o)^MvPEAK4H zL@qFfjyOYT?^y}g-+TqHg9-G@6d4-eK8hWlPq0c(1DDr4B2K%FD3E_-{KyqpGU^3> zW>26`z8yOoxU=HM7VMn+9iHci<43=KyqP}{Vy_v|NOvvpFGf7=c7)esRR)!z`+4_- zrs44gi}^pJ)5sM&ZRV%50G?j>7xx}qj=N+`nexhs7(M+8bgQ0(ivG_)?o`2hr-{sE zeiMm4eE{|y^(KL?*SPbz1gm*X8J3#|!J!E~sIuo5KP~YlSnWT=X!>WtBC)CTlg?S* z;Db(ZiC&4Xxg7iE;t)7t_>yF2Uxs;e&tZ177g-pdPu>~zaXBkVrecZ@m6;@qbMMPS zG&#aAJR<~hwaaMefnl68sh{dQaB~}*>r~T862!k9#>j$e0AsV*`(YzstA2>gU6%~T z-qCnua|+Bfh^CKEMZ$9KyXLF;)AX2UCD=9iW7y}bFc3eVM&#{bqq7@%j8n8}vduP# zD3Ar2&OpdodID-fv2n{o_*=1)Dh(Y6f3bV;aTn(bi)x@*=fBcL zN~`FXw_|YV`Z#=^m&%XjxYJFL=a68UwbM(wS49hxfu^{gl=iSlaoaC0c|7jK7d_@b3 z9Jo2fsVro|V{z>50(Sq=6)nPHv zwuy!5$5BXR1OM~E04@V_h?nPKTIn>OV)&z~z;{`jj@BjN{6VKQ*tjACn=G%8 zG&?`IZgi4yHZH_93Kewd%?kP>=pq>0(Li(`BJpPSaPF}J%jcLYu0an`x%mlMerFv| zyyq=?Y%L}iW^+uas{ynpbp~A57iaXQD>B~`b`wFnHF#Rr13aFbfu5(Cc&y?Hg@=22 zF}sS$yOp94b@3cH-wcL?ZNYryD^r+O!!RiPod^LdE`r7WB8>MbTvX$@26L6aK-rD| zFwC+R7baf^OHCtEvWD~Gc=>c&Lf9ksCAYvwskGkIzB=Tz2AV+k8oh^0O@)C8ONTaSNu5~ zN2f;%vI|N^==xhT8Kr}dK|ZCQXY*r#oF^*Kc`F^>zMBAxR*SHT|0U8d!s*I^?I{&BgY6wknfgU(ZJ(hLw_T-CFw@FTL%+!%Lt>Y}A zs&Qzu@dTICx8tp<+X+jKo z3u{QZp9|{c36e(NE_@d531h)I&?>G6Nh9fHH-!p#nUc7U;ZoQXw2u!!;W-KFmYMKgwK(~ zABqvMBH0`6|CB%rV>gZ;J5Hfo2oLwQ;XrRbO8qSZ+2D(Or!ge8?=F&@3=bHf!Myo= z9v%E4#k{jUOB)?E@q^1jC_gEKBRNMfhF{3#+TR_1CpRPA8~{1mU+BJNKWTxd9b>Zp0bFZ5 zkH6-sLCMS<)DK^PJ0o39XII}bP4fvr{|N5>Ti-%!9kRGR{8ExXo`o)XTI4o&j(dGW z5bo_LLbJfY&4bWv%brl(K?gtSG`zqX0k4so6|sg z^f}H(nVM8HKk;UI}E{6QBP)C<@uer>o1qdt&2Y2O*D0sh*s><^LgIZ|mPEFI*UnseqF%=#A zuA+I*O`>DkiHlFX;kw7O*j_h^N6s6f`cV~TQL_VicJLO2Eu@suxQ3TK=O?tJS`0a;ehk_dR!AeI}0$^|6U`OmNK|4z?moNI|Eu3^r@n7 zD0nv4^IR63gMI%6z?0kvxM7n7h2yiCq!$vbRCx#f@d@EF?{d)n^flNXmVnzgc{nuvWVHu73^`D zo0z$og=5Q7k(VLCShXjCuY?h^*1Lwdo4w^a1*5!o7k=Z-0Cfk4v&E4sb!s3cQ5N6Ph!u}jX z+)JGiC~JZ-wOM#*+?exNWkJxNERO%cb!G2cF-4A7iOIQIj*qdB{j?MqtCi;HG@=3h zU4ZVkXK1Rrte^yJR?!`hZ8$z!3xM}E+=$$0I!+v zL);SPK@0~Kew?Pv*p@axxMd{}nII4f7vPV=bDHpv%h;~V1@RJMezc;L~wvbw~snCfYKzBO8Y5wn1vbRSew_izky7)BIz`e8rv{_&sk7-yL&@ zL%-BvujL9%eH4X$x!E<~p6|n~tCpvjcq3716~pzSyv97@H>PQq!}ckWw|3 z!lpztyjW`bPGA-N!JW_Mn+rp{!we|DSx-CyVzF^SD3QA%gUc+E=+RtNa&!~NOcIlV zXNvR4`D$ULg%!Bv@B!Y>7bfJ2D=^2-4yk{{87mBcgJ3Y>564~#r5}gdP_{UVs%+WFV+Oro z`$0MOp>;6dM*kTM49p^74~H>WSsFqNw}a`i8pu9igS(V0SV{F@_#*oNlChb7sk(;w zH>%Ox?;!@dMxxANiaEt?->^RINMlu5%~o1gO9 zpE<+6V{f73x+s*sFu-39W{l^J*(k2S&HkRhBt3sLF!8Gy@qe7j`A{|>>RaH=yo+Fa zp$vyG2wM~^apdJxRNL##^~R&o@xmHN`B6i^&A*6`SMCF^goEH}UPB7+)Y5MjH!=C@ zNBG_zK?lu$(qp^l<0&~M@CaL3F(_4yU!pTHQ#zC5l9@8`u7O}Bn25^j9`MbdOu>dS zS!nAsqOU#oQ-$6OxM5x~7J2Vxj4M>}ME)XZnjlO?Q5ryk@hsSyQRuA8`Of9 z|6=*Bbw-e{pwBw@ZifviQXKAU`1RD6N6t8zoqqV=vF}vt}H>H&O6Ayt}wM!_z(R?XR^8JIkXah&4}(qOOa3*5{yRQ{O`Crbsk)L zr$`d4TJeNt6B-{}0Omo-c)Mx>^z)*yF1Vh`>RPe^5f5qik9$}aJBUKd+ZRi@Ze@?F z5O7^uNmcygq3MhY{Ueq|H(DOVo1W2J!eTZrzdQ)Pecq12l48u72dYfz%K4n%hvSE5 z)`QWy(_l1nBh27t@!x|OG*)1zd`d|Ecz|j00XksJPREyc1vp+k?|11+V;C>@SS57i$#+8Ph6qrtPZs}(t?-i z4A#k2n?Gg$G2Zei9FMR1BY*ltZ7>n=1CjXi;FeidQLT9cstt;9r$rpJMq6Xt*I#6O zsEA4pAg@qsF^I`ILx$ZEsMFVEe{FC9>o=Zc{k=TXhdgJTvFtw9E_qL=;5_8}3liC} zDWI^!8}`fccG$3 zQAJGEY)a(jGau~TnJuF4!2F&$c{@Ht#5Wv+VM9wapSYCPdN0Hx$rgCMYz&u`{q?T@xw& z#C3El+90m(GR!xQrE}Qtyijh&Lrt`Bf$CId!>w&hT}uv>9s0G{Y;`;}v~2_3;udNZ zyBUQ!5?|=~$;?{yGBC3Dg8FA}06yuU+trPKb8U&HOF14DIg7PQR%8!&0+}Sje13Zo zXp0auTTZ4ft$}#x*<0%U)RZ@J&&J>SIgXlZLBl>*%x~(# zc2@M9@5 za+b&1D13$5+FabCCeEDx-37`L%ennV7WTTPVbPRh=*gSR#0!7M>^;9p?u;>*(36T$ zxpOeHdk#~Ve};a3IFG#BcZOFI&N1@yZo#ex60mK~Q=Y0*0T>G`z`V_iNUG{@yfgfr z9RB;0zkHDMgWh_F#fE42CG+cGhfpR>Rgq>wtGF3~FvpNfe-Cd|#hLiI94qGdbTE1+ z0GW?3ko%nrp@1WoO-z`8x$jrgh>G{5aM2kUvmXUHb3F`mQ>lnEXNmGx5e!K5$AaVz zXgGEljCY@gvBD^b%D+sRw~4T5To<6=2~1R$VUnj+levDrSbn$=74??W(*7#UIq|F9 z((^oggV$ihh_iMM-dGYK#TIIu$MA`JaK%SI2=Ggy-fhb4R^RKmb&5EX_ID!8-{8qH z>Q0lKUSxSIQTAnGT~NyPW`#V3EcxX!^%>yc?3xI{GrLd*A`f6V5|# z$#zKSD&pB#h~m|UL2zlg5UY@J5QL=l8K_#z`N<|R0JZff5pb zLzI4CUg8_PKyoKWgKLb+_h zA-=AJEGRZRV%e@rtQTF6J=@(u{JT2cIzZXB2a()R z0quDuWJ2}_Bw=-?qaMHtU2?(YKPhhc(+4xsqp(};TJax zjEStk1H_WV?CT~yRdrcH8)EVg}BpHWW53w2aJMWObX0a z&Vdf;le~U|A*_)YLibczY)A9$o!>F96$DR4$GCG-@S=!_-_o0jmof8EgKyN zwlOkI**x$12JByV4(ER`#@b68d1*=4VAt#{aJ~HyWiwWxxBGUw-6#SVJr`!C?`F{5 zZ6)5bW=Zk!3VK=U3{U2CDyF(R;#&V!s&Ib|yKRpmzGP+4jLWu$Ii=6~3@t_FGj5ph<14oQPKJg^?l;>f&-ffv#`7yeaeuTTt>?{SD-LNe zY_KG2V)+Q}91$QL8t-9_mNh<2oPi{~5EtK_3@5h$v^Q7tb>{>?V9{A#(URM|3qPh{ z!iy3T8=MV?c$UnujRQEwE+7%|zwoKtWMW@X1k+D!!wpOy-g%n_<9ig@bswVOu}LG& z$R0+8`=TU3EEbMhyvE4mGOYg-QFbtC6-IP+Lw>V7JKZun*fbDG0(=j=r|xl@O#YA=IPgFC2n@Dw;~o{OJSCNla4&nU5xB!7kH zK}EeOJcNxz?TI6a?8zt7zNpZM9~W_FRtVW|C(he?QGl{Tn9U~xVOc@Mb&O^1lCG7E&`bd8dpb>UJ%$6k3eYzGW{7l7S zvq0E)%@$>+bdvHVp3wbGAKULd1-JNrL^=4s`<5gM&TZG9}IV4A_BkX-2TYf~ux#Gm;%0 z{KJhY81}D}KE6~&?W#AzYlT)4@bnt~(ii0k>Fk7#<=rqiM~1CEF##{E(MH{Oy<~Ba zDUO=hz`gJKY?{|YN{R&-ua*Lyka{)$5Z2*wxdOPPco=RgcJn9Lj$^@F3!EbE4!;&% z0rSW?j81AYZJH-d4B}2wpT!Sg&O}Q*YF&dbh6_#G%BG@1@&_Q6JpPZ5F?4I(M5HZ- zR6M(i9@yyuB^58|HlY^MW#S2&mL}2I<9#smhc45vuoa>c)bK9MB@Thy`@ZuAMjL7~ z$EOVQ-yIpLSUnPrVso_^{a?qyLCzBs>b7H4r2tI*69~@lxQxaLM@*TO2YyLgO&>Zs zkq^wi3Z~tTCg8R44i=r(!u|S|D3G0v5sA6z zo~(pt&uxJOMKwm;X)!#5bP{fB0#m#9Q9-NUIAi*B{1qU{9RK|Y%K~r1!V6q>uzwQ9 zILounqFU_9*oV9YMcZ(W{V^>3w1v*8bY%Byaoy+oT4>+r1{r1PsNNz7wcP%DXLSSb zm4FQK+3JiZB^HD2Wg&iz6F}gwBXcjR0P@f7q8(GC=}WOS%zgSB!-9ipq{JCWNSzCM zTZI|_>P?K&z6IQG_!`(+TJS2bumqn2(O3omep-PAk-ta8aNbQzY&(ZxI9e!&u~keg2Z4oG7E&R~A#k4?Cdo27qgH-p;^TPvLN+%W1$5_mlw zK<#7mVD=^v47@R!E!-J~k7W}fGQNZ3zsa+2Rt*rlfWu(M#otfdS_Q|j9NcDdKE@Mg zU=x`Lr!_q)943yDZF8-05BmVzRi;6mb08#?e5NwLFL7=RJ!a^AIJ_DCM?wYL`AxnW zuxkGo$P@m_tG$&@T6`A~1zRupymU7N^q%3V-#tXL>SlqfG9UErJcf|U?acC9x>$Is zkG|nD!TpDo**Fm;#_zQa)yO2PCiOum#|m&xyoiQ6cfjD= zZA|6%3F!y#V%h&-?@iySe8azOWJpTBQi#eFkyNHw=Wz)YQHDw(LzI%{qCrY#B~ylw zA;}ObLSdc9B@{|UNvWhLB~64v<9+>}`_;2;&+`}D_j??di?`3vGFucdIqsyCEQhdJ8*m2HB zytDB%iF$)rSg?rgFmxtM44P4M?H*icDTM1*AEG4(CZgC_Jq#Q$#BK{A_Wij{*mWiY z13$dr-zd6BO!pYz71uLxn#+8!Pv&6IpNWjwId$@xanomE zW?bze*A>VlwHlw`f4b2N_fG(C8iav{D!4Xg4qfq3m1cIGh3`Ihae=xFNe@-xTb&nY zPirnkDTt+>bC#gO>{--QAQTJNg+Td(Q9QN#Cd6@P;_u(9Nr>7TOz3(+iynkf0n$wL zr1Bs-EfKHZ`C6^B^AjH1_5}*KvykS}Lj0{)$g$!Z(a$3V`ncEb_%cG)e&{6M<0pg2 z>{F0s;Z5Uet(YqNBQQJIq&ogrId1|80Y0-P(bTkfS_K911Ux zfGJ~SAwLwo{woGE+61h$FuDgA;PhYq_@sR;SrOlg8u|I?%R2^kM^4jzjzu36JJUE* zHU_sueIvrg4)}iJB<6suKlGek3CEw$W@AJ(Aze6%h#h;u`^1wV6{ooK@r6{#2~&o4 z?=D`aS}PVFJ%k6AJS16yndoArhd1P+u-{P-FLN%d@7C6I_wQ*SxQB9_r$~ ziY@%p6MsVMCKs@9HO7E}6yW+WjB}g@o7Z}uw8?~UESp)FWE(?{aIV(Ak{K`@t;>u* z*TS8r<7xI;H74OlF4lffM*sTz{C7f5jDc$#X4UH8fxIlR)|(DXBE%R+<2kq_pc724 zuq0-76FsJ>#vT*8OQcP5(A(aD-*8177pkwPl9~n7KlKzcmDAZ~QO+Cg7fiE;9)snn zrDXPOX%e_T8mgE1vej$!8Fv|ZW}Bxv?EHQWY^OS4YFsp^^@c-!s}m@`xdg^PF4FUl zWVqQUFmHrgz~{whY}u>_2i`t}Ukg;Q^=WSPw#ac>o@`2L6?n`ZZf5#kFoE2tjlw3M z#dxFmH1yIO^f2RGXp=j!ZSoB+w|9V5Ir9BF6Yq4w2H3Pb zlMHw#Q;SwHY`zo+pSK@@(jTXx)YcPY{qr&WCf5@yN1lA75C-nL41>~Y#Hf+u3tdnq zhYDrk)ujwP)EUAa-xLkU)EjY~cmkQGC`Kce@Nh_@oi{w&1%g6DP)Y9~zWXo>f*(x6 zMvl+7;t-5c$f*zLRoN(42DvaBO+2o6Ql z>piG7;>WAgDj_eAEQ7EM5p>3ka4@?#5BDh?<;%#w#BHIo;h29tj@}Ytb5q3Fp)dcj z?EOgY4-rEYB;1MD1~K>?#dQl*dg=Vz~BYMH7tDpyiz#g>^k_2>|Q%M#cimj>&z zd$^tO96H7BJQ!92g+dZ?QJex@Y)#Hlj*ucu*0-EDaHGZtjtZoy6_1}a))*%e08D2e%1RgGGlqD(B_FlsQQ#(gz4_CopNF1gm#n z1Vd_PLSxniy2l^|KKURBG?mah{Zp9>we{Sd&kBC*I>UQ7xRRTL_3$U(@FS_#+zjO2 zR|wD-;v9hyL}JVnr+;y#b{sogpd%WiqodHpXEvKr$+_Q>cXFJ7E<9$|09*h4Br3C6 ze%XN}oTl=UZ@Sn6EB7v^F=vICTKA72{NxGDA4dZRhddHpdJ>q?pS*wN5!7(=1avs)O7(n} z!cdedm^@ZM{ioe=WD@@?&2@T+eK!N`zzHFf*tWfY^Kyha2c%vpR1Z^d}TErpDIi1d)uI(XqepH zs)F0b)o`11A9a~23BPVWgl8x0VXLt|oGnRwgFUZ7;8iex@9zfOpsB*- zYgJ-RFm7k;eaTQu=M56@y)p0i#XEj;@jGZK~|c_+tOXyk(0z)R{AI!>@p zjjZDOM-nkDupT!(aTr2IaVc+%9}GFYr>WKYY_^CkZ%(W@j++V7^|Qa>qPgKH(s2+?h7a&s3NKKD z9V=O%%k|{TesN;|+!P}dzTw6Jd5&M2PaEdkBd4~9kWMdmlsNX6KhUPf1aJEcqHDer z({@*ko$LwnD`c=G@&}~<6k~ZV=kWTaIlKe;f^7f9Ld<-p%8DJB&z?9J0)cOf_>~o_ z*jb0pVQ#NH+1w&QTZyUxRDQ%Chjnzkpl|{R^pcv>Tvtm7K}1J z#auihgTsk0aklXch;cST`JdsaW4WE*Bjswm^Ogv!yKx32&gduQ@ex=d7>;dnTqo-} z*Mr_>ifiV@65H)@*gJe0hwd3Nd-Hb@X+?L;+*ZLKSCV2)S`wji-V3OyE21x5BT(~B zGm3OigSDC?)nS3N*rW#&@vfvM-Ls*RJ}=urdMBnq>w!V)Ke8ED6jm7rEEQ+pI0rHW*$-urYs<@yNY{g<=N!kv$bt4iJEB$c2%2e9l5lTXm zORz~M2rvH><|}mf(#QMW;*pQmB=Jl#{xjknr*B?k&WIiC=NLavRnKlO^}b^cIC0CqVbe!{jf*JHM5znnFdsVd(RzfXr{iC*McwCBglEw1jya1 zNCWc@68us)zR;C z!1Fb!84l&`TFRa6H|@nwhWAh;R0sV96G@nd7@KZ1jthCW`FA&$gXP~gOny>9$HU|4 zh+Ymk@m-Q>D&9@rCdGn_XBDkI+y;MC4XZ_}o*9|sA4K;(Xq49J zR$qw6;Zy6`yERW>b@#LCyjv3Nj5s->YmyK791lEWZ8bFg`hXQXTXEd?8eV91g{|+C zL1gtxFdx)GwWS{9k7@3e|~_?uV*tgqMz~Zq)O&Vc#f zG46bsRei&LGWl9R#?#8`q3ndYIN_B|3AN9x&HI8T-p!$QepmEt}V*l3)o7Sb$#uPh{Q+dx9 zEK@@93GYCDfgm$-s|a*b`arWW1BJ~DVDr&M@Fq?i6}I$~DN*87d}KGu7L*r77xn`V#MSp{NGp9xLv_N-Y1w$*~e0LVD-C4)K>pBfD7EETJ9o<88-R#l6 zA_JD}iz30{9vGLp2AbLzk>yv5smWed95`D_%%u9EuSXdQ#hSq(ei|N5s^Ir`CsD_| zXp&&Q4eN5a-S$a6uyVZ$jX}dCe9KX=nHz(}?;ZFjrbmLQRU$vX{TZrd-lkJ~m+^k3 zj?yl)r>2J%@*~1IUf&lbQkcCK6c@UH%1k$ub6pAJlj4vysYUsR>!DKP6-bp1Qr?|e z^fgTc{BaQvZ4E-H7Xi5P=Q;4ca{+_hx02PB4(xMFai;z$$6-wuWz2sFvNsFoFtf5A zfE%NZ^##7jrrn3M<(W_)u#qjS0Lb`nDzOcChRMfyXrB9)>Jv%Evwazr{M0}d=B_~> zt;06}Zh8s50g#92n-aOz$RJP7#Gjj&%klR!N&3-?yg%I& zurTE$9@uu99Bm2Z7_gG;7~aHV`CaJ2O0ZAHy!qDBI^^Y|=j3qtUiiJw2ELu14Iy$% z**%#eOogHV{+c0(sV6>y>f(uDn!cD+KX#&C-8L{9oJ75?T(H(plRk5@#mZUVN&izj zo=uDrsH+Y_VEjBF_XHUY?(O+1t;SxhH{hy2in~Q3!FYkT@s!+Z5}?{ZZfH#ekE=$E za)37Ea2(!{EFG>FR!KYG8lkanBJ{oCJOy)mfu^=nD0zwXq1|ZkX%ik+lSWs~mzdXe z3)N8wwQP%FSI8*;0%Squ)A_8=qXbg9vkYq{8q(IkU#UfT6m^n649RhA5OARwJ`0Dy z`y8a&Kax0}%uhJ7h7b2_POCW-Uh7G8ze zDii8?u@Z+0AS|omPjm4N9dYHoP8J(lWuwr-x6Lt%5+s?~isul{XgKVKE^bZwr z-Ui1d_9XV!Chq@W3Wh?&RGDgjYOOx6}84$I&Xqsfe~(n}&%9)SUWrepnm zEwnrQj7((G@lR9*+_-TOOO}Rnj)ey#-O-HE7Tp4|OPok%lsFUeN(`;n+r#n)id0-H z9B({MfW{>plSkqKF>{}d58Z#zBBv_U2%JaK={;nZi?NQa&ml%El{#}Y<@tB+VB8p8kOt(`PR@KA07wwvuz;ohJ}>(FEEp1c_vopqScTbTiU~MBd#fv<;gYlqhtuS ztRLZxm^kCs%bif1C&%ps!pVH;vnaIQg?71RV;7T(uKh^%#`n-A#i2;sC2+6d0bE!n z1g}n{5^df%-i%GgCfQ!Hw3+MG;Vjyo@PpvZUgFz*n=j2VrpbX0^gcQZYg$y8UhNdr zAG}8nSi{*?H{SZZ2~cw$!+)Wf5392V*Nw3@QYLAY*kt)v!p#)64eb<`4f+ z+&&u8$8?C)?QX(2bc5K|7C6l<@MT?gNDUkU5LLoNdU^M&vE&SHca|GZ@qKRiyjlBeb`P#l6P_nayw#`^X?& zWEBL3?meVes}Z^;i82M7qoD1V8tbu!2j*?-piyZBE_`zfJZ!3=THmO;{^eaN-ooX- zoF!PNr8CIxle(Z3U`KRz`?BLfjbvYoGCFqrh9{XVsQA&GR#%$wH?LifuO#!qFD43S zC#41=WitSs6OkQc?J((9K)Z7!%6c`ecswaj?sSHl8IEvg(;r|u|)JX z$8Srg5fNUn!*_Q*N(mM@_nH%>xhM=pb1qQ+;{4}z+81X{hQrgfS-jA2m-dT6df zk**Dx@wS`DTQ5d!V_{INS%`aS7d`ge3d~blG0njY_l~KveQBRbT;wAxyL^O5sBw9g zJ7cKBb!pX|4e*~p3f9|xplQ>hu;kby&}do=D@PP?z5fL03ekfnQfqOlRwA$W>{~eA zHx1=ie8O!GlFX8td9cbip6ZQNVe4`klqe1b$%f}}v?U9Cy0X#1_!b$^2qYWi7VxHq zeFp{M>wF>4LNGqxK}GM2K&JCG2rPX=?vBlZ)8f{2SoJZg%e!KN$Y*pgSOhe83}1Gu z@_N?a!+5hCd{$G2nc1hn`@&hw)ct|S3f_`F)6Xz+at3H$`ohyMz5!A%7t;8U1Tf4K zfJXwGF#uk@0Ir3vU+pNj)N`|+T`esoVO0NH>e^kH`{?N}?o)~3V} zgIA8=-Q0kTT36Nloj>?pi*Ml+>;IrAat|8)+)C@5)!@&bbM&+FCLB5wKFn&t*1i7e2WtV#}<%fHh=Lc%Q5RXMtEdH5EjKx#1k&0>cRS%QE@8*R1kfGs;eV|CLKX3vIIu*=5}|L*b! z-+=|}h{X$#B#YSIhI^b}+7atEIK%6fi&W$1emw5w4_|lIQ_u2JI_y#a2L+tz(0`$* zx6%ja%Y4JTn_R)++a3(wyNf2DSdE0fhOaeOIF7&&JU_e^yat}qxi#Nt<%+0 zf)n{$KhI~BWty?+%_VL|uFi}zVt~rJDD=sgp0k%j-L!OCXqjys3EprwB^RxqxA7WM z+@PU&BPe%X#7sNRxos~^x4Qg?YAT_m(qSV0^1O%(dCw&2%Wn=*}m|QwV`#P29h2v}X9Iad?z>aw3 zQN70-F(~5-QmuOUD%=Kxdw%h2g6in$bGmF%TMggr_ze=_&tY5BMrlaINlX}B$izp! z=RQ+ZP~t&08Q?1Gr?bD)Ag8xD8}}RzyhB^S(drgAL*cP2u3RB+BD`^-rYW1BbqQCk-pII@9E54RWtg*e ziflun1e@)i3}W1FFO9pbcHO#$e-xx(PErst5YI-hoMJ*gUIlsP7_{8Kgsu-7P&B>{ zzb6R5!e0x}(W9Ryx7rK>9!;yBHhKxhN;LRan446fz8E@sEbwH`Y}|b9UbWhA41PH@ z8v>dpvA6uAiQT;u_%kGq4n+>ozCbTBGgu0qtnL7#9bI_Zn-W&QmDF>bsl-+@GGf;Y zHmQQl=nfamjQfnwF8Yvnf)2Q$AP*cq=3?b?MYwt{idLCy!Q&4dp)6L85$M(ix=$Rs z)~+La&SwzOp%7kQd=^&SuHfdYPvAjO9a-}JDh(c0V=J{s`O?d|--~-b$#?gGUq5#? z)T|;U3o3#3rBd;N9y)1(9?S{>zVFv6p7`uI>{%Fs&r1Gc<6{Rj3E4#Irmx4A;lZO%u!)iy?Bm$eRkp% znJH+kJ&Yp~uAni`7GpCOQMaGNxYK(zc*|cR&I&=S&O{rSUEM;S9+QIl+l8=q_yN7k z&B_b?IG&|vF#lnEBkVlr3elteu-7D=7xq<*c_DBHMxAX~!Q>tI(pvzkbSxl4)e&~Q z?!=~*A*gGnjSCHePF5cu#3;BnyUaf=o>@ZENS4BblCp?Q=4`Fjqj|0qH^k*o@lK8rx3h=uw0jXoc=JKzMj6k|FbXP5Z?eRkIt2wB7 zaVDs2utWF3A!yM{LuZ#p=xqFn^67uEb;~Dmq5mo^H8>AzuW@@@&vtUV?g5>eb`_b# zaO(6}0s7j_z;NG5_}J4!W2gOtsFLR-H;p>G7oMf{|A%S5qi@h znTF@iKyBfz_+jn@_I;E)KcRU!`@)K^re19?ODNe)LO9n~p zu_?G{dIjg^`NW-@^60*DP!63K$MQ84L188{Y8U|B>Hx$-F$eb;<~fSr}J z^0_DYU0cdCe;mu5X^q)eeYR9#nFMImy@rLd6k|rFqU(pZ@L(59q>nj*V)-SIN&Sse z1XNMXv=lZ7%dj@m;@EKR6V~T-!QRSkpgZvlT3wsLM7=!0tG{Ex=DZSPJp~pqVYi=< z{yUf9JGZy!=h%f(yJA4<>u*T-unsDB4rDR?tBVUgqDM;#sI9+XyVyri?hR$4_0e>iu^PoIljm1q9 z7{~D;@-bm0*RKl0_=Q~OPtg!OjW5A}ahw}xSv5U)FNQVSqe3&R%$X838_bY+470vx z00i5!5;ZIE$g0K6Zn6pw&UlDP4~5W7`UPlS)1?CYMaa619dt>^5G^oo0>Q~=@mXCn zjri{&mtAb8njA~;S#=#bc;OKlY)mA!CW|=_{52RlF39}7Ig9amJ;pP+=L@nKYnd5d zVQ@8j9*hgNk=_Xm*>-q236q`2{1}?XDsvp?czq`}YR7YE-x~?XvL;cb57KbhVJ0yV zl?JCIX}rCfJ6BB2!@+Z3xvtTCcIpNpR`HG-TwVMR+8!5DtJhO-XTAa5@#7%$)Vo9O zCRx^bt_0>klVW$Anor)hw9(rIu~c48gob-x!pn3MwvPWIVu8W*ozr=&;7hW0hFL_@ z=sG^EE&$inKglW)b!Kew2h6+R%5f3}**98e@z1C3YSZMS5b=0Aoo}5=A}(CTb<>VO z{>yPfdF=A7Y__(f{E!}Q2FQs0{(e)_c2TM)SCbJr_{MD zMc^P`BfSIU0*CP4(^9_DrxWzm+D_hGQ7(E}nMmz#-J;-mA7i!h;D-N2{u5hQ7>-Zi z_%ZWneW44OjDDux4JW};|0c$%g%N}1SH=&HNH985ny3_~%s%I`2QIqH(b3Wi0-~f? zeaiWD%3fjWZSMOnmnJzk*WrPW`-%VUHsS}X$zEmx-F4#+I6WL7YWf^o@|PJ=F_L6b zbj46y_A@z@nqd6B@G!b?Y;&IjMPL+aLsft8ClZ!EAYa@{yl2>;(^^aBajH1=EjJ{^ zzHQvkGbD0GYp7fp$Cdeb!PxQg4iHLu3VrW=;Pu&Hwn(cP{B8Y+-I3K``i|o^-2Fo0 z0%E~(r6zpg>)`%}XW-R?JscZl2t8^~^1Uueu_H(P!E33#(TeN4hzTa);yo#N*=zu! zeucr4eUIr#*Ef*Xt)yG^IF~?z01>m!B1zhIROIJ%w6-zB48bojb!8-8ExkvC)&&xW z@OE-{f&!DRS_b}0E`np*d`9vK*AF zB+V?7Jr0sx%}}5ahQF*PFndc+qRVIv{yuaScJjy2uHh(J$EQ%{l^RXBB8vK&?!>0P z5?#3Sso~Aj{7-!X)u}6vgWh&8(A!i7GUHR2dz~F1{plqxKi&t&E}VkD;zv-%m5*jPzV=PT7vN8O6<<5 zqcfJOLga~hJbjvD3ih>lg?zJ%e#)BsquYY<6=_8*FVaCw)q< zDHjB#u7k0tXXwj7a5#><9!IO^h%XN;Mz6g++)YG}sV=D@mV+lzH1{^{`zXQKd#@mO z`*yJHSJ&W=U2=@yNtViN<6?a4HJI)xl^Att80y7O)8ePEad+5CaI(HjZ(Wn5Ui~ZK zqyGt1l^5iL*>>unu?sy*{L#AJ9!zVeklVJJP}Fk>t@FqDA!l+(#J30Z=?CulPb;RC zPA-r*aRocuFAS`x8T;KO9+vN)54GP^px$OSTv@FSozFhQdh6YD}1~m z$0MwsmlsQ3>Be{DeZ)LR&oFq>!hy#gmxeSwJDC`d`?99x88znEU2nJ>7Rs*??k zPI5CgKN*Y}aw%j%djYLTSBHwLLy+}Mo(86_!qK57koyr2pBts=0&nf=(X7{~H(G#O zcf3W?)J?}{+ycG3Q=z3G3r~mqq1J}l%-y6Iw2{+8qp0O@lDnr>s&aX@DoHpz_aS|L zD4X_sDl+?SQnb+5W&bqi;_t7==y1_<5SsD}SHwJr6+sEq_IoYVU%EzZB@T1%ufW(c zJF)Qd3*>@|>|jU+3cV=C3u|J@>_s;C>Q6W3xObyp#}4rN?;IqVs!+E@ilksbj|o@1 zLCVtYvBGr%lnSQs&#G#JU6nNRa5N5cLWd@N#^#{Ncw7*I>#WAWWVaDgPd0z+Pmz> zy=){173>2SCI)-1eC8{(rgD4%CFuBEOb*uTLG8^g?_5J< z?|S20Wew)igb;FM>p>VTnhw$2+0s;?16zYR&T;YSYB!l?n3~xLQ-gogB>V07wN=eH zl!@ZHjJ{Y|5lurP6S4cwQF7{c53l`vE%v@^!NR^Z=$w_ux1VKBw#9PU>zM*fP3vi( z{Xx)jH-ncwc@I<7_6Bpl?ZEjVX85X4nm&~0{O8Sc=&`^{Q1eyB zy!{o7=gwmctFOQ)*F6kM$U~E5Khffa5$xWdO9W?eE@0S2YLZMCJ4=oSVXuGztycKp z;f?C06Q<(ciUlmWJBMfHF&*FT7DRcInQ*mUgPL2E;*N1!SoCNJ1=V-K)xA>)Zd**+;cK0Lk-7V2Jpc=w) zUgnXVXV3CmSOKj5#&tKBR91gGJ{6}DB`(A0j`e?5d6elUZU)}@dYk3G=T^gj$M4Cd~6?HFm8i<_#0==-Xj=zr7#rC0u> z-VO=$`{fm^zIzgCb8`+C`h=)I9T9LnM2YFr4FU$pvNLl+973{P;z} z6``qoWf*&}dW6!F7pUho$X~C%33c?&;5y6GxO0mGz6&ovQ*O7B{U_8oYq*pS%==9G zB<%R@3q+ZP|1$&d??j6=2`IKD6s`*>;=MIao&!ENcC>x z7m9EszjqT^rirvW(+Kc;&$Q zp8H^>u$57|D@lFFl3<OJ*@13%WDK!`KwYWBJ%^sALrm!OhsFzQ6y=l7(6Ko zT%5Vgi)9vU=#0UA8Lg=HL!JtoO@eKaQ#c-wIr?Y32hj*eu6vyc`TStMZ}uSdS{IF% z*IuL+E4E@(l@;t$?IH)SH&fAX!wSegGb_( zn&<|%0xBHn404=K*|m7yR|aFb42YWy=Q174 z!vm9A@t4M3V$rTj->;7$d-`NqC5>1zD8xgdNLjYMzZ$Mj;Bw7-`cN)lHQ3f!P+zI- z=&OXVEmi>pO2!QRv?FQ84t4f)zyLmJyaD?FY)xFFw8nxLbtBpj$zsp@qN-1 zVxp|Ze8gOcDs(`-r@5%89?XOfsI$zBcoePiq*A;(+C1wl7#nyT#(s#HM%7!vg!`>5}WP5m}de$H{uETB}bHI^CE^`wg!dC4b zr+Zf=P`msxviIdpqPQdyMrCe+gr_m}UUU(BmW6Yjm`QByZBuw6e2!Ome*+!7l!p0i z2?-3E1$Q}CtbN2L@}MT0(C0ItzIF;od)d&qhA@a(#A8IIc2RqoU0Ari2e(}(a5d)x zI!KJr1AbBv@go~%zpX`asU-X~^%7j&@rOU!Ujuh3jEAV}FR{^oCRFFn zg}>hnz;eP1?tSJW^~@guw?Gl*edP$GO6kL9hub`l21698f4d++5G?~>=OT=o4SCHXkN3RYfQ_t8Qe7-jYg_rTz z=amiEf9oCwi^Oxe-L-tBRifZx%i|m}wtR`FCXB6(BS~HChm{>y_*W0~6M;+ju+#Gl z$QhN>;+j(Y^Zg8M>9Yp8#>3T0(cL^Zxf)U_tHXMG+yL)`XK?d&S>p=N(Q1X^NwmI9 znHl|W2D@Xm8(#V)46Pfhc*n|zfr(y)QQB>|G++W_mL6i9^-UEoUrB=)okl$7+6k$h zl+^5dPkyW1#4uB1X2C`Y#6rT)iZ5kS%^@MaUiK~69DLk3({>ItetpSHG2vcU4Nm0j z>$+nub~z1l+dyE1Kb$6j8p2TGG_T}!0+T$2&}k5zJ5=GFzq`aEpQpn zE2p8Ni52d;sK}URxp8yre(ZLf3r_1}a8LO)8heJYf6sc7;;bTG_W~A^>!OIurXk{C zn+aO({q#5Yx~bn)K>D7}gII5Q@Tj+^1^l<@v-lNWSe1kO_T3{zPhHrM_(j~E;|6Wr z5>l;Z$^DJy2%ro%xBjhn9Nn#JNr9m@>ocj3=Tpgj_QwKbMK$90`ZnIGgZZTFf9^BB zky!h(nVhOR#2=1*1b58E*t?#)u$sQYvPeDlrhXKTB;Uc2XqGk`_mYhLU3hQq4D4Ao zf?Jd)qSh5PIK;8|k4N?qNgWNWm={eV{d_P?B?EK(kJGYzBe0ny&%9aP&8rVsN}n18 zf%gIu*(~iKD@?ojTa<-RF#qbxsSA;<6IlQf~;G=JqGuP!_z&5bgb+d zo#Glo6$BiZRtzO=E`-Nlfba;a4;3Q}aZpULQ=7KiI7;u}T zgdc7DxidL8!|hmsN?r}P_(eA9`%%QtzNA9-+`oo4sUhsMLo7nSKN%PC0!>k8R1sQ6 z%YR$4E9Hc7Y{w(svw-`gX)qje@EUhkxlY!p-v`Z`bD2+N86@uLIPvsw1c~Q$Fl~nb zbHK!x*p=^u)gnI$ad5)`ZD&0G%DQ^(ns@x9+Eln7V8iOQZ^Y};KVf~*7Iet;p%E$z z@%^!Ah|sNp#4&9)=$QpSd!YyvTWin*zuto8#K3V3~4utG(Paof5ZQVw~b=foq>?AnRh z^A_;4*F`|k20=DWHyK+mj1v9p$xzR-AaubO9htcp5^V&FHu)n_6J+N)EjF5Ba%V#Yjsrfq8Wxar+^CvGMhyhfW@H{z{&@z)dmH$_ZyD0M zYEiag^c{)Pet|lLqqLms_|7utWAg7nS{1gD9mw5{Q5WxXjAwJcjsp;XhI95!dyME_ zjX}$W8AnG&T=8oO-}y)=9Zu9>%r?HJ$|FWF$&mBFK5wFx1KMoB&YNV>X<_`OAPM45 zGHif)Jy;3qAlWhsuT7^i;>O*4g+oF>Gu1#zcMQrVRe_zEA-N`yLkiCw#sH0Me5ay~ zT`!jKh~GvKmfAvUm)c{x!6e52+YPXCU(7r#>Y~`S2$gqBFuxYZK=Xb_Am0Y4{EwZe zRUrJoVF9!Le_;Xtn*=HRe_@ROXFmb0s~lP4f8Y`vSwi{$`=90ig`x}+Ofm|k?M<#k z=ioFZUF-z8uJeV2t~)`b*Tuqhc_W@pg9=kQaRx(0=faddCeV9YmiaFHh7#Y^zny&pOc^=_Z#0|R};dHTtGqhZc0-wz{`anz)^5IqZl-3^vv%$$7?!^C~^gV zbsga8oGXEC)^@N;c_!nj+zr#DGNC{29={-Z55{as!pv`7f8yW`r~Sd z2$f?R`m(}yoOo!Qe?GR@XfS!qRu;Q2;N$Luv znc^X^$p0BWtdnCrZ`~u?Or|i>sjb*LF@(!ME@r#03bLQ_uJTO3q>_RIJQ#Sk8i%76 zFpdk1Sv?^)95K$uaNB8Ye3LMi+Xi8~-bp;>uhAgYc>8x ztuO7O$Mhp`Vo(-z9ooQ*3Np+B3mzWQoPm}qX50=h9^8v_sPekasFZUWEq+=+W!Y^Q zQPF^3nvcm`^Ayw#_ygixUvXhvDdz$^4dk5!Xfr#Z-cX3CiI8C@8;+6Rma~|a`}J7a z>Ico62WY-*FLY0-gC^ahbW!soqI0bRb9+kh_QQ#g%H`XK_;K`&fih{WG~;^>T2# zvJN`mMx*7W2>7BDi_d~sT=jkxzFV;z2F<3hHYS|+JLWD3CX3R7;R-Mq6oY#zLFiW^ z%^2UgOF9;0Q)Q#`xNVgh1kCq@Y%Y(|IH?z>=^Y~X4E|Dq%XiT1O_A|!GeLZ4Ttn)l zROkYaD0JgDV-Y(JIhzA;$mR!nv~)qz!WCF6p~cS#&?k4D8_@X42&P;S=Vz>|hWsgy zz{}H^vG^K958pXbeK*{TbcnnL(#$#cQoS*3UmA_l?;v_d!?A100#HnBrh9q{Dg{Xr} zKmyivX%MN*HMn4mPurzb@p;B^oOWB6brADpqOMLN%1NQ9us#x`w!fm+AK8GDjycXA zT?RD+^NDPB44EHg4PLW1bKTl?)Mh9fDyCfos~t~ps3I7z@6Ljvu~dAlYD;w$_%QM6 zbSC1`eUcNaf(AwdRPfL`Xw9p|y|wCCr}c_9j%@^&Yd2xmVlhVk#XS0JSw7(7wQ%ZO z2D%#d)BI`6pfRBepZLY{drw%?SMl#qNW=pJ>Sv=1*YWN2nZW3*Jx`)^8_B{XL8#kk z1&<%MV~vgtnyYJ4_k=aLb7((J)bHb!gqPA@9ZhE6(tTXMu$+H=oby=wiL%sD0j_+R z%VZnI(ZVc6Vy+;8I=2Nl?T7(1rZ~~s6^+KdlW)=vNl_R}zKeSnw?KbxC?)H6V1;%o zJaJmdHixbw4ICRGK46?TnezafMJ`00^nbLo;1z#cNf@Y{KMWRib~q+I4^$huP#E9b*N!n$GNn~> zzCnlWBbs-rf!eJ552h{Tbb%TH)F|y8H7V?cJ^9!0`HaUfU+^7`){VeBA7WwVof$Og zl{~!TIWa-A>QJS#71eDmsi$QFdMy69@>pScAy>2aW@8Ay%MnKo5{!| ze!)!sbd1Fz>a3oN%eg&p=Q%xgg3}-4)6a$3qLD`0uXuxFINT(O>Gw(SqKWKIZ#g!z zQwpBhOh8qe&m=5kK0f3+l>YWSbUHCeeZ{%1!BrX3u=gkEbF*K)b4i$0vJr;;;<2uI z8()2X5a_$kWOc@WaQ%4!W<|de)N3?@Z{jbK7Ssy69);t0h6Wqh;>xC87sC7XPq6Gt z4xRii@_$ivrtwt0Q5!ZzW*L&dF&RQ3CC+))u2d2YN<~VOLZeDbrNNkxh)gM=5E>+f z^Q>J+B$d)2k&p<5&>+&g-}l@5$D@<9lLpcoo((N~pToS5 z${2262)Ek<@nf(QgoM2o4BOZe{~a4J+qxCaf(JR%kB!*w532GDiMhEd+_Dy7PFad^jtVclNN&TEmS2$aO^LDmR!N<< z1(1xBA+*SX($46goND_Un3QHiZ_+U|*Qg_(tv$eCG#_m9M~K(=WYBEh#!DYc!4Yp8 zp0a5qHjJcTk-IK6ymOfD;V)$tZ0Uka=7u~UrHgPc)rifE%%SNoccN441TgKAM#&#> z9Pe^~H2<6cPl_E`m0%|V52Vn@B)3FC%^xzfOGrv*G$xflre4t}@n*Rro+5=*zC0A4 zzV5;K!AAtK=PvNB)=!3%JxSzm^>tcxHVFQTQ(CCXv6HXxu*EEgiru=5DOVyPa7+h! zMhDU5@?v~qw}p5aim|@p3Pf_W1%2!;z*w0$m?+y2#~Ip?Gl z3-ZTR!0dy!Aa?K_=r7n$(4M=Kdj@`I~8>&v!855Mi=?kKmoC z5ZiHG1&k7Ia(w^kjL0P&`b@V9bN^5Qp3KA-%H8zXf_bp$+9{MUBn*t$1fk*ObkDPE-n2BlbC2w`}G_=HZDf> zLu*a~``oKb{MU~>)a@TR#6%FX#o z3>!q~JITMOe7%r9y1Rf`#B2nMN-cC%w#8qoZW0Zjg^Y%HA?@of7Njh8Vfv!2vC4kB zV1wQruza45XGS)oxWZXz9DN40VlzSQ&Ly}NUx8~yD&b6?E*ka!0_!_@aFc%yg{)^Y zn*5)rTVPK2CIq5E5$8!(dXM5!^2`D2d$7^-3YnE3$lZghaGv`w9;?-YGX=x^;a@Ct zbd+L=@THRQ@&A#?No5?bp@Z~a`3tQ&IhZOcz?^DnMpA4&m~iay!Ra6A;!Wc0)2vY3 z(BnW|TXgZ&Aq#E?rht3&&u|RcObqZ@1Wz+!QA_qd966B;yBvpMRO~G+O`C}F9j)-~ zMjn6SaXn_4_gN(85AdZ!_h6lH9u0e=!2FyRhx#10&(%5-T32ot#59DUb>1P^Jlw}Q zb{4aBn>g;}k7nrU5kZ;H-KZ!rAO4QJBxt}OC^)xN7?zR-Lb=XDblp3e~aK?^u5Dn{;R_9ZIcilXt1LmUB9`Ex;iVia2bs5KFc+iNPud2GmdJtqt=OLT<*P%x*N!V_1U|qyi<KtxpHQ}2DF-t0!lF)z&%-3^& zsoCGT%!19`*qXWxx|d((^&EW)rt(+F>Y-%n=P1Rzne`kTIWAS(y4O(mbUS2?K|Ykjt~)QD?PP zWZ7v7`*6*IGE(=&nMslSjShwFxM+18h8@sl3Xhjk>5~%d`1OhOSMx3G+U^52hpS<} z&qF%dei&SGLg+quWkyFl3?3Q{(;J?VL>i=E!UIhd`LiGXmi@IZ$sl@c1H5Q$?Lk2Ex!Pk98?D;4jxVR?Z!w(5yWpey ztHlu(p&C;ZCV<42sf2yNJuBTkO)`BwG3`ziO`I6cpEYorr*k1$U}7zbUG6GSKlK)g zw3|$%>@@_1c^7esmKrVnVh%pzm$9Cr9pKba&);CgopI5@@H<$QO+N4dZ*6-*3n%c& z2~A^mqw-8@u}_7)lKz{o>X{Gry9ulnI}Bv75LY}qN>6SKMYo`8d{jRdu7rOmh4Fc@7l}EL%(n@j!$t&hp5xmVP!uu){O7b2|E=cW z&G8tQ`%b}Pt<#YIK$xvQTaF3e!qHV$4o(ft#ATagu~Rk`q>Dep-pWAez5NGSH_+IcPn5;_fLe#(5o*G^E9y{hQ9VlEZH91;`}||dUebSJB8nH} z050{%tUYNcX&%pI11jN*Un-rQtw?5b`IeR5ifHIH$o0}h*y86(^w&Q-vZGU-e!Lxm z4Nm%a$3TI(xr|!-TWEna;HsQ((;qaoPI;=MSXm9+K3bZa94OERJ(BW|Jk1@bJ}b zAj2Mn%mZ1pCmjVh_wC2J0V|dL7CQRY>lP-r3!GGG9tuHnh(fkEQjP0#O z%wEk_4X&z}9LA6OU>w>Atli;G_T56NPaWT*QhqDItnT?{_;5BO-Z%&~oCAAV@ z@I{Pno)wF+A`=B^msx(y;U-XWen1rTIkpae8~z*=Cxv@-7$cP@*h7WLt_2ZT>F3Ux znsE2a=co9?Y9iy#?c5%t1pb<~3LHi6W9LCteyp`Hd-mce1T~Dnh{h;9`c{Xg$|Pl2;WJ1*Tkvu3p6iTMX8dEo*zB^NAK&{iFtzho-R25e20GTnlj- zh$U%}Db!6z9n855wr}Qd8h1bmyreeagRSGiDJq1@ZZm?U-bXm5?u^GSa?Y!+D4hAj z$a?g~5ybeb(9r6O9g1i1tkM{s8=eIAY@%Sa@HKtZmP7-yZ9%bGkx{7K4C<2W*uW&t z`P6xy?D)+wNe;|Il?h7JLG6H`;NCryPZ5LjvrHhyQy2Z!%wfKmHcpA|6R<5&n0?lp z&i9`T#s`ZpSNr{)@G!V=;x%R*L~8iJc&)G94#GUm^D&zJq6Rc~rVhh>_l% z%X$7(IW|Bru{gb$t@fy+KQi~BTF^T1s47Ms%NHE)Ed*Sn3gPwjH)PgyF87&L0EZo) z!Di)!sM(Z4autrjg(h9{YE>7O{<(_zYv!O;=^Zrr`55Ov(kD69ui$mfK2UniVn$5~ z+*H*K$H9tYABY$ICvyMM zR}OKI7`B-8>za&b#gc%@z7F=k(!kWW2n$szxlHajrh3pEGMtRjo3F**xt0J1f&%LE zxYvfMBHC(f|AEMm!OQbuGk|mBtOn7}KY6d96gjzKmiI8A= z-&G=`DGC;243;gpN_&NtqQB~v%Pnn)ad`!*8F(}m$eQ7mmfrOgV3%ixBTGIR;M zj3X}cOp9(C-wKw|RbM8{~n&Jd=g02T?L^{ zP*P%+3ZnlV5oqZiN1f-dkl!9kcUPVuLaXGN3a=X4yLLS@zA%LUe8nx8mj42`Ip+|w z&KjCfuYvlevq89f3WVQFLfJ=(`~y|bX~qc0Qk=JfhAmbF%bHStGL1u%;)!gF+j*$U z&Vxa*x?-=Tl2B?;C#W*5;$OW|NL_budFnl<%OMW#VSef%8iQMDAJFHE8PijNmveQb$B z!7=1cbODk$OW1Jr3Pvf&ut|oFkod2Fs9ftsjlLy}WkWgc?j8^OPUKL@$s*9Opwl|* z*AHANtie|La`{>3-vH+AXxP0O;tgzI$%#(dlUyYjZ4_o#ukOLH^3eF)Wg0!$aa(5KFJXWck`?g$ zw=+Bp-Hl>)+#W(c5B2>k&_FvJmUDa2^St+%_4h07SNuhKlDVDcsv?qK{|ddYUqX$0 zrKHVGn#QI6Bx+USI3~OwU0+qxkc1bsbHzs7yS<$FNE8XiBtyY{s|YsA^h5W9UX+^A zfZJ`gnYN7`=yb)LhWORfwYuc2ObHHMO+qch zK^k&1p+q6Pk2)L^VSoEEP!nVd2ANHQ!q1$?vR9k&ko<%r*~O52N1ly$aA#JC=TUo& zPvqupbBLbf%;#CVV8aq?aGrLPPZTSm$*7eK7%qmGEC2W(kI6Hs^M?3ku?w+2`XR}c zI}a9XH{RKk0+Qpm>~Azbo6hWSyx6y2uW7Hm;D2{-%gG3iVn z9M(UHp69($1F9h5V=kO57Gq*WP4L#j3Fu^U6LSh=8HEj6NIqP{yZ^q>Qo|vBCl$9ctm+15u7QU`=N+S+$YlTI|w*Af0VkbiI%6{^o}d#>unmj;7GCcRI}9 zNO?Bjq6#kfC^Of;kCo)D@5Ys(9BZNP9$CCAoh&Q9jd@E$A%V-%J5NfdSqTNOx8pv) zpxP2XZSoPU-98=U);t5cUKw1+s!Fop{yP;@LDCYg#kHT|~ zFWshxg*?cfR0lyO2MaOF_z8~&zo2!F7~S_z1U=W}L0b0< zI{8~Nls2W2W@Tw6reGesQS};VDZPUOc$8m$dM7ShA;}g6hruof5eSaCgyKtW@vaV` zd0)M;H2V+VP397Q-S7bwzcs>&uOiH%##M~resLIy)8p^v4M0no8hFO>nEsR^k~}v6 zOy8)`xyMDZtgVoip4I`y^ItIMv@OScuHf)V|s(SAVhH)954m8eds@8D0B(Cnq26wT1O`CSsPzGz6%mJHRI;65U9}B zqG;2AiLE+puW=a>s&Yk>6Cx)sv= z;qbqI<53U|Jq{U{3>o3`O^{tA%f4Cqn}oex%LbH+Fh2&=p_B4hBEx1A`@)-UYb& z(?h=d${XAab~Ab69t$?cyO^7jri=_XgGpc713w>6#Ney@z_=hDluO-UjypmY*vms}DxPtqWPS(QgoJ&OgOS z$4TS^*HfCl_PoGnS}zG*s80P}X3*nV2cfgm1`?SBJiG5bZMK+6EM=F|>v8L$Ch8{r zyS@>sw2GjnX%xHq!lA$N0ra-FpjsdwHtfsgI}7AsOZP&-1ojmF@|!fwH8-MFr|;2y zT;IhfDw$dzAEEP{gqf0g&G1WffV!NJXZ?O11Iby7vEIsn9!_Y68wavTU$j1ZF>@)( zKU^TlJNtoux_lyQv}8QFE_MtbInDsbusHHO+XC1DV>IkHA%k^^ShZ#!1nj(sHF=pJ zUn0de9Fha)Z#rl#TMXf$XSiJW8qg_`q8h(;QY~FouJ>m^J5)^BwN>3zWJViR_@)On z`L}VK$2r_NG6$_^DZ~7)#bnV-KBybcCszAkW4(tvdH(1>{8sKD$X{>;a?YKhpDRA% zDycoh{(h05?)fvQ$fP(FJB^v069QL+qp18BeRR2h6c>?wsPL{Ao-CY0HcI@W;-5kU z7V$6mq1u9y@C<3DY}GOzjyxri<6?-HcQIH@xs6&j1f^T7z)8jl)HSc;p6%CAaWoZw z{F?>-7ydxrWNmyWctkxdAL3fW*%14g!jVh&(cJ7k4K3aZ*Ik`qG;235U%+*a7o@`F zeqEIFS7JJj>e1qqfSKkNL2L%WQFZQxorp-2Znx&0%KHRgYZojadjP$8To;R@tHdL@BtTCB#^$M6*@h zlhHe4FVrsY6ljb(K#lJ*bmZ>3J3f_UTC_jfaC-r_!WtSSJy~#M=1(f|UJfK8ZHd>% z6!a-8qO!bFV))=PSl{eL;q9?7XCREo58DH1?juiU4O&T_$;U!5ai(XkB*#jzf&64o z5Fecev%KGsr2H|^-FgiQoxH$pAIB;Do(q~epV9V<3l%x~Sm5_5fP^@`#_F5nm|fR) z;6M3L`e8;Wd{7)F7WqP;#d}2JIL}{iuN>0xECvkc)9bDuiOPBpDjy?_!bhes#XKw6 zR2_?Q-Ko6lzEE(EnG8=(9iuMCpOhGhPhs9BPh$=aY=N*_pU4u=@9>VdA4E@coj$#K zy7Q1HM%z5418=iP(BW8ogHP#bd>Bt!UkH@vPh)QtNf7;m!GaI_oS}IuxA)g@gqv+G z82n`tek$He%XzXK6L&rA)3aypKJdeaz?qCy`)QtA!)$P>FN9|0)wr+v5X5H)lUJuN zV)#!lD0%!JhiqAhC+6{q^np2O@iz#P6PIx8u;Un|oB_AYreT)o8(d{pft!{|LgFlr zOJ?VaH!eRWQjhn;>kk>68}xb*NNeTO)yJ8pLW4v zP!7He^DDZ*r!s-82RDw>`AHzL<}fOLF-GZhAOUY~9G3wiPkZPAlg5i8Mh8X4%k1OZ+c_TW+z99gf9$5+XIq@_`Fdmh0DV}Pn z#3u&ZDDx{1SAGn`=?{>8xUP@KzOTTs$x|5dE5`UN>oGBiNg=kv-c&8q2!A*~#8z`n z{I5F@LuIbO$3&WpIV%>BG;PL7(vfFt({q3?56=uJ} z`jy+^pvN3YUtY}d1Sa5)5)+JD5)9D}u{6p&hPH9NS|N)$4K_a(fjguY5_9q4q zW5mLNgX)ZH<^}$wNy^yvO9S_>l);O2-TaQWP?YDXLf_B39JjO(+W&H_=wJip)LAuR zk@SxWxh-V2C2gg1;(gKcUMUr+T}(x)(urk%JgnZi6y$Wu1&JTz*dDhhVA%FW;CKH5 zXlOQoY0Yjr>pwldjZq(JZC7AN4?O}cT!gPJ-vh7V5oCr+!rxc>@WvEi{@s!m{4G0y z&t3}1LFYN-zQjCIZu=f}t}hc1jeN9w=M8r23iv`_bQs%!W)k!M65W}$8x2~$p&&_w zk#ki+_dEB{bo*`)OMEDJvN;tJ9%=BIGb}j}C(LFPPKUq8I6h6bI(@+l0Qf?4IEi4OXEk)Q^z5Oswq#4DwR`YKCvxMU82e?m0fOcLA%)%*+ zkbLqwbrpSzn?F9Hk<*vJ3Ncq;{m@l?b?O`>vEuW>;e2RbOp1nT^L;Ui<>33BGTPmrXK<;X*u`#sFc3sZ9VuEyQL~ovT-?juZ>38zYetW>|biQUmMQOZ>I_t z^O*97x9Qx6V$7Tse6si46;zv`iy8N2!D^!fTPbx4=2_LySLVWOz^522GU~!_{VG`a z^A9dx^#OL~*+b3A*$}x~ou&Q4Otq>Sym*_66|)u5=gk(vOSffxY|ew^+aT08a>5$3 zX!s@j0M{M}fsSpyVAUB62OnI<8n60jnR z@UCBkOVYu7A>+A>uk#%E<0Oh-<9FlaMF(mAhnaAoWIFW4reGt--fUxL)78g$gw3$V zC{1BTS5F>>PejtO?hlxC_6Z4Gc>ucWy67~TNJ@75pxWPlnr&~1<#`%-%kCxajOQz8L>Tqq|?D91n@ z;k)TRIUaQ+&U+xk_PDNs{!YNJqm3ly_$Mg56OQv1ub@0TN`!7Gvi6=y zbf(QgYfm9jsI~hl$jqP2*8a9+BHD~WHII*1qq2$K?+euOc^he;k7$&_z29dX!5^P? zfL;F#s!^;=qGiU>%B7wl);5h%`Q(On*AEb$&Jrv=wGrgyQt~i+7hu!eBm^+g*DZ9{%db z!e24;?X(cm8#s}6*V&PD33D{L7XWLGeu6|>8srA*Fw*C&@T>bT*fFOLa1Djb3+F(& zC4`%={6p`|!(ileg1()2iimHBhbynf{W$v`17M#s2+VuZ59j>k=fE@qE|ZK zJ=_zNvNr4C zQsZz5=77CfA?K?0pz{aSsawQO7(Lnt!lDZ>i*{1Gq$1>%mh;4m!f{`yC~LGS9n6pN z2u$|I+SUG`<|Ke1?)+)KzYYBiN}z1p0z9FW0^cV}5nr7&f#jX-=56` z(|XRGRD2m8`Cr8?p&N+7(hVRk^Bde#Od0HNqV`iM=Yrwq0I6~pn0+o0Wlki})N~6xEf<1gD{g|Z^=YWE6otL(McL{1 zBZ$suIqj*~M2YSfzSz9iU})?|10?psYney*^Me@Mrk_K^eJ-KV3=?K~MJhx#h%ocy zQemgLBSaX?ME6DVU~RLH&J14!V<%crMK~QgKKPR0WX|*bP>c0S8^Do|$4h2AbA6hg zeXyueL-6yI7Czj*9pqDwV*XGSCdA9J7wf*0Pa^vuZb~*BeyWa7ey=0hXIs!)tbsc1 z&I5mL7U__vNsOv=u)@v?hE29|*?5Loe{ClYoY8cIq0p82 zc(gbiRllFZNn4MjxYkpy*ZTk(Z*L_2RcbUe=LkXV@_dOmU+L*B0dT^y4~u6aR#k-2>(8Tb z%rOV%%__ntk|nr2$_f@8mStR9xS7i0D%>_?LmCY|K`Uh?-qXrLk81AlKDh`(u9XvI z*~y#^C<$lzufj!jR!}SX4TdZivpbWmuzJmPL8SL|F!&hG&Tr?#Ga(IFEwUN5(hDdw zmLlk^X`p`I@ua-$IZ0k(1MO>yV5lh^pNnpQ4Zlqx?m`;mmz7ZKDiut2h=t}aOPOqC zId;p{VQ9Ox8v5@0U_pT#=4`ctEsvj&*VCkEAK6W`Hj1HU?Mw7Y{6yW7tBCL_4JO$r zgVIR@_{wc9#+e)8s-#V5pQXvpYggqszZ0PSqX=VL9t6Ew4aA{sKir&V#+c@xzvd*OVx6aUi1LF#6;nhlGb zgjU8X=&61d^R4c{Tvl;F@3mC}B*# z!;Vlf*5k=s)asLEn~DJ>oZkq>Dr%vl+mf7A3IX4WICz#W$sTZW;5yk;v3bS=I8&#L zF{jT#`+{I>(KbWdW?!fdO2n*!@Dd-s6;4;s$F+wGVC3=`rcTg@&h8wrUR??|-f_N~ zi+ns@7=m62gD~mv0op1%kD7rp)cI~(vhnMw0Wv*V9_@S-NKT>}6jn}zdHY9+h(<6Pf2gH{_=i7obt-_) zAq;wP1=r_Fm4-_#{#C=rh#1x*onkJ{oIWgG;`$ z%*GeF7{Aa9qmHPt^GsHP@~up8KQbb)e|QmRM<~J1R7I#W)&j?PYuI6vh$F>1Y*oW9 z$a}V!ri$s|*Mslq!Kd56sneII|0)9JC33E%H@i{y*JUbnC6la=DSk3 zDhmhx=EhfBD5 z*C;kn)NhG0q#kXjfjflR%!YS7@3fN; zT96FBeX4MW7fb@DHE=r-8+@>564Bf6mY&}cjYk?vKyJYhC>|3+ALC&vdh0*BqsoFO zwEQ{Q`%xCQiSyuKkps#GmO?F;HMyq|hq1lJtg3h_x~_GFI8h^3>e*3zWDx{~CHi>c zHaAOlz7KNy1t8Nffn9R(JZ#`H8H|fPv>i-^-mh7(;<1Dl+pN7&WS_z3y0S%=O z)bURgjP*sLLT&{5+_J^L6D665vUlYCMOo&0!7gw+o&@fKXlN4K4$3Pn7+YT+^J?P< zpsF1>s8S3W>1lW-Z4i7Hg`rs6c@&8Zz`-LPY~G)V?B!)=V6CDHh>h{+oV6?jia+2# zkJ(L^-CF{Rnd4A(zZ9zO3WtV0H(}vnebQYWguiw>;k&4K>Mxqfxjvp#&ovpCrtB}! zpDZ9#6o2!j9Ys;KU4?VDuECwv325;4B%B(&giE4x@ptM*uxkwFMO@3KCBH|hQ{!_u za?y-=Wjlc9U%SI15(oFBvvIw(A`E$$z=~V7c&Sc--LpW39=n_ie?MBl)h(M5B7P8! zbuYoYFBdmo&}N@+jigJfx6q{T>)`5z4ze*bqh#)%2_X8cA2nv}MOM*@IWDyhC)x^Q z{c}H}QE$R5K6`~ac)f?)eR8;aX#j>x7optkK!KBL6R%Pxp0D$;l>9pBLwzelI4=J; zu8*U{+>aOs+g9A8MT*G3yIO>!D{(HC5=C_Kd=K?b8Mwrv9{oG{4M-RJ9v$ z=aO4VQ@a^S37yIDW}ZSuz9RpKvE7sf0JSU&*IeQ(%2{7Z|v?z`oT!FqE~QB=S$AP|Yyj-@A?H zhi!2GAcHz0yP;)MrGU3cNT6Xc3WJHscqjA^aeFv|Dh?r7s_}q-=k*;32uz1FX8qJr zGm>uFpDK6|9|uWdR-C6-mU$I5p3(f33t^X=`74?#@zwiGJotSNzWB!7&nn4u^;BbU zyf6*#UR?(t)*2A)x5A*|t3qy_`^JA^)r9vCap%I;a$IQn9vdS#CfY?EP+u2@8;A?TEB zrVY<-30y7ilV{J1uuICC$@I#^9M^|5RwjVDM7%`xBvF(yzA{oo0!PUL?R$56I{oBxA?FW6`rZ(qzx| zXx8Q6GwIVH_sx%tt1IWP7;NJ|x$(7l{Pjufr_nE982A$nbLL}lbq|pk3&c^8+mKRq zlg#FFR7~vxGO;lgzniGRvVRkp<5vZ=Tvwj)-Be#PE2|1a##wOBa^0wQQH@G_rIA|q z8#t5MNSwnbF*V2UV~4X8RB4~W5IrX#J2vsTXgfWAs+;E>d5sRPyoBKqTQS>n4o%4o z#>ojUIlnQ-xcV}cZZHlZiuTuuqWB+tc{YZO%)bgPzmX0eYlJ3Yb2zVPOG5%5fY|}A zgLmM?v#8E7+U4jcn9&fdCHy#~R;6Gu8>i zvNv{gzIrm~)-1*Q@!5hqd=(6~^~JBQujsVEDeOZ(ZaYLE2aW zILIk@6rNVpxtWwh2+(XsU=5c=JkRoLkS z^IT)Wv3U>%KF;N9^*qC%V-fgoVi=t`6pJqX4wSA*qDDJ%aM|?>T$xXmxgj?dwAP1^ zs)+$`^tT-BoHh$v4*W-Sj;f&g!pmSX<26a%APjvnTyG-hE$P1c8k7sKcfsFa3dUkA|4L=2{vYq13vV&Pe{6&Q%rk%JdcqlH=;6vztUzJL8>VYU%U z`8Nv0ZY!`qZwX@pJAsLmZY3{{M?i~XIQiR~jt=}}$gh|KuXHSNk6SRukRGKkX4K%` zawDSq@-1CadXv}7v0giDhPn4R9{V9uiP(=_$CQtO==Q=2Jklw=_S40vw`HX4um|cM zwjgpMvfML}HWnUU!o2o1!}x#IuP6k{43E|bDHemDZ}2Bd&d_k)`0?zR@`783&uA# zkJlN$nd1fM{(oM;4AU*XzTT!=H~rraKaU+=+kG7ZHZPYkL6x;VB+`8ardNode?=W8 z%PWJecs1WnFo}k^h_NMJGEDVTBUYMeLBDgcbT+HNZYjQiVh>E9UhN@x=cIEUwvGH- z+r41d18Zz4$_3v}J{~!qKQXMuM==@dvu3BMA|=KA=TG_$epkPIU-{TaVF_Zk0{ ziaT-rJc04f@}*ymxNKaz45qzYj9Rb4;mK(~xKle>(4~Ew><+#{_xesJHO}kE2FYWYmdRX}(oVq#<#OO~MGFAf)t{U9(*i@9iK$Pb(m3`2*Kps8guzF<3W_AzO8 zey1_M%$fuSN7JeGuF;bAjoL8iJQvh{aBR?Js-)shHuZSwf$uUr(7(n33OLVkP01Z< zQg;XcUb%xgpBF&p!+P-X5@W?DOoK&UoX23&OX4wf2CYsiveu^&3(g6^PcEDrU@io? z8B>UaV>GvI(s@{~ zzaG-Q7W3EVufRO51z4lxhiTWtU_?xxwe4AiS}$6lsOvH|{x_cKU7IIhyWPq3ZQ zW4*{%($A-q(026!d}R0me;rv4O*t=MhiN^&`~E@@kZsS(C^ZnXKtE8dW1!*iF&dxo z3kGH!hql2|{=#JrnEUjR0G@892~?Wx%$mS-&Xp|5Ii^I%8Hft>R}JI&p?uh}cnJH= ztl9a-;ba-ttF63|i!!+h*u}9Qvr~4_7Llc(+^_=IOpL|-*OUd0M}A{MY9i}1jXMjZ zC^~&F1g*$i;;k_#Ncr)KXH?LTaPDX2RTaON7@Z<`1=q?3eRUv46_7^;_O~GTI5i`Et$Ki3=gu^*w zc2F_tozX|y-TwN{d>?<K|l3%=|a_9MWW;bBgsxhjZ>jUq92GQesHh6HwTs(1&;>F%!jt#AcW+xxx4$)E! z5||R+Yb84TzzjZgSJEl(Ct$Q(7=)~1p?$;@jdJ+7$lnGJOfW;6)J+(*UKw@GCjoE4 zSrm6O#^v2HaMkf0EHOC%3(e;+N((vm@!dJ-G;$C|u86uH>8EK(TqfiStfw~#s^?b<8f3kR&!I0gEOr8voXCTjcV{z)l}gadoqMel za7~AA>NF$%AHQQkHoo6=7R0V?Wb?&S_>5F;`|mS#OJVYmfPB@S0MZ_D=y_a`-3v1r7uyL8Q+=ECwMF7Wj|QUU z`T(wdmd3{Gi&6Ej8UDD~D_EO)4nrQ9Lx8;xOt-ASK9%vzHpMAW=xB*WoA^{wfpcBg zWI)^JazWqeaS$@8hN`^~;6vdqIN+CqQp;B2rTZF8gJK=-{q!7{iuEIQE#SHla;%)+ zb?W)Gk-94mLAp&orj_esZMQQ&<@++yY9-8eW^AGTw&UTKPB*Q(v6Gfre-qqvDgm$C z5unvV*ysCCLnPOKlD~Zyq(_Tz`KDSdO)nynGP2Chp~>c_86;!Bs69kU zR&vbubl$FO1K8>OKXjexJ63Plw#_o8%#o>(WJn74buLLorBc%LLxV(8QIs?y6jDM_ zrbwiTM7XbWNhOlz6q2ISq@*Yb^<2+~_rv>c+xrLHw#_}Pb*=L}j(uN@RpP6_!*Bvx zlS6o5%x=79Gm9UY&V@n|Aqtu|}WJ zc_<$XFOGwPvw0AG^%&M`xMJ8AV^}aH8jWVQuzu%~K{D+AA%^)+{c4BLlEFNtbkIv6IY`?b_wJH;Edx|qvzx|M2m9)kN&#MBZ zU%|9x=p)@2(;&p_br|q1TWIv*lhAdb5yK~>p!I)CS*Fqyz8(?9JN{?DLi_+9Ao3R1 z`%fm@X2(Lt>x*D^SszRV5yD+AS+v&n31~Hn<7LftKaKE-C_a-W0djDj^m)I7XC){T zYe5N{IUs5A3d#pHu<}_SxR+n2&l29zid{X>z9$U~%6714Z)39d*fts(?aVt`9l;fo zgM zPB_zy&ACljKQtnbzL}Fpy~-u|>k}2wsj!22bhYl{ z&HWVfKcltbUv9+N6)c4<>DR$({c~Kg<38T8 z(WDKw+JN^TfS7G5j=we%Y)WopN@@dn`N0^&tr&!4-8ky^Nsjkh!TiT7Rzqf^9yj;P zeBuy08EtOgg1T@ue0HM>{)X(c>)Cw{mTATS{7nSYKprEbc))xMket?!8^teUc)c4t zqgRrrN^MZfSZz6{TkwmLw?JfsfX}K*f%o4J<9xM4Lanwjyoh@XiNE8BTWLxO_iKR?LLY z-9>iRtW)`PYZL1IUB;^TI!Q-g$2DZiH9~Ne;u7 zKwjin*lU+XZYDO-cvELcKUWVoioMB8M-5sa7YA}ikLgII9uP}sKIfbI{9qBwb$-1g zuqZ48iSx{79W29FFW2L`>yi54y_T#U`)=3H&27UMXc z!^*~dY{^Q3pF>u>n;_cGAV8A$uM*|^rX~pI*8ZR$eu?t&Y!7uYJ`;3``%zoc7<0cG zU|nw;9?#g$&s%>3`#u=(r!VND-EqWclR9Y8tB;HuEDj4TUf{~cXgsPv3cf5%M5mKQ zc(gqPPkc$ilt4*-%Fr5gj5Y?j#UiY$_!%QJ@?dw#Ul6g&#;&rt;1!+*6~8xOxSAqb zeAUO+x2C+$I7@h7y&OZko)Dv>+8}Y_7@75AF)t~y9Y2*waM99+C^uW2zdJmQ`KKP! zd9iA+&wU$)#XJ-~JJ*a?cNt+v+#}LurH;cvjd0=6VsdTVX-IUN0dG!R#+1Ya!7N&X z&f7)#MPJO(>8~+9pPr3hznbGx6C3Pq-wNv=eM8Ilr{MChS`vKONZ90d6<;+}lMkQ7 zg`%fY*l#Kxe$Rgg1?R>JO@voj-d=!}-2=pYt0{ClzJ`8Z)?qyNg{(0>2r%Iix}-GW zjgcy-d4DEo9p6HK8hRp8*hOPpH$vluo8*0I4vsV~gaCdSc;?GtRPZCPoH&6@RGWrV zVqRm~e~Nr+zZMdMXHXWU1#6P4Ny580oKd(Bw=FW~6(hyD6wfm>($fvpwj3hW^Tc@N zKXXw2TPK9-{GoM0)|~1NKbmQIR^a&ewLmOQ24lL@D{@;!889fjTv8j!x)6q0r8Ak|!iw`zL=+zBn%d4#!dH|Sxq!e6Mo zAwzOB-NEOWhj3`a9?U=5N+UxeiLBBY9KSspXD9{6Bm-$?7Vf92a-j&Thk6t|t{cB#4g@fNofb%n!gD8bCoj>5WaXZ#OmFAQc z_R*GI&NO{mGOn4vUXbg00Si})a;;m_psw3Q*xg`2?PFRXH=?Pc#8(DeGLx{}nlV@w zEAX*1Ct$(kQM}=&wz;6RoVN{{#C^E6+OAO#iPx%NB6n#7{E>rqhKJqoI) z`U{TAEP!P$EX&faNH4lAB%4Q@LcC}dT2C6prx&BZ`E(Pw?9IVv%%>i_0p?Rukx@aF9Ww{5H&VK52 zWC*s2O9+K2qhR;W6u8mwRq*Ch2JKn33v#cY!P@hIL?Z7raI>AkHgyuwYW;z&Y=%&< zqZw?)p-S=! zRa$)$r07B-{W%x>LR0X_*CD~$hJ4t`7=?as7~@n`8e6y+SkFhJZ*2~K6dnT01XEo4 z&XeU!Qt?L3Y_Q*K#T}_jNByIcV5jg3l+_36>5xL4-x&&iw(Vr0QW!Y>I|Sj^OE9-X z0WJQ##J(m;FtuAtwsh_%+n$&(PqY*#{z#eYS)Isu0PZwEUzwk&u0ZmW50gDlm-4C3 zlSt)%X=GPRBo>FNaF6CsAgdJ9NXCZ|v`+H~%H*aA$)@*2>qH7vwY{WObE{C+<|7;) z7Xk~^1YkTID){#!3uUZLImd}p`QQO360w1~XP)t>^sj^tM)kv@7cBRXB7|+pmtc5l zBJBR`z}U(QAY|1FIJoO9k!x+oB_*|F>hZs5R}hPFCvCZ+-S=R3ZEAW5(9hDb{+u66C$Ii5gMCl&u=`XEE z2`LloF}zS&9;b~;NdmK z;YfhD_Bo`$XWg`N{*A>`jwUT3!+p0z3<<@vD~v+f5? z_EF}__dkQWWo3fP%DXUU+7Xy4UILL_jF0&*Ot^jz;nSzt3MXpq<1;4_4D4LZA3nH5 z*!Ex_Y3Mivc1a-^F!KfVQLD9+d|(Bix^y|~U2A}9zCg$4Q>pw(#wckl5L)#3fy>tz zfyiPlzM19nHonP3**~su&{Lc@N;oDQIgHSB%#3^Aa-KL1-iP;lcH&ymJb08N#wAx< zL4ju-(G=~YuRb4vHwB+qw(=gh6>5T^TLYLntso5}Gca5+m*Qhh_;8!CfCfEj&-&j) z-?|I(XUXG6=P|hFNdR{JDuhr+IsErJ5z_T-q4o7sx^kxhzDPX?Pu8np!c`}j*Y=i( z?OFs78_uEV0KQ)T47_W;* zL;&a7x`17!E*JVql)EiInKw0xpxfgY!iV8@kT#M*&-Ri0$uHihI@T2*t+fI5$ zRUHkkH=)tzYG}|sDK6z+P~{fRmVYCEh1|Gb2F@eOu;<|qd^6dF`!-GtE=bM=<&djF z)z}*9GRcTfeLDevex5~UJavUGwi~>?Qw7yr#$(W$Ol&*bOHcosht@mp(R%Mwn4cX7 zB^{}_U2-u5=cmHK>H!S<)QgYjAA`D4_u-ZD72=~M4;qP|pl$VgT-K<<=k=bz0dr5Z z`M!#rRoYDDTXLcOUJ=WLE`!pWV=(`Tw;rg;AM6TAHW zGMK7{=zjDEE_-}{{ zIHF40wf1oc zH+WoOXf_w~Piz2CdJU>;8z^g(+D*OIKw5OaV}s3alKgEx?2Wd;=m}S8+5=NiwQZ!2 zmRR9ti3$9cG53k4jq{L6Y2>8Qxg;5W|Rf!$*U&`d_3ckhf5C@x+`ntj^w z(f1hLg;4sZKMj30V3mVG@`Ie600&xgS2w6@UM<^fJgQp3;ZrVAoYTA;x_Wmxic9KLx! z2sdxs6`p3lqpLs9!LnT@mD$AoTSEIR@((fi#5oGIq_J$ zRfVhRZA8%vpXfUCk2o|Z7o|SCkV_(p!qj##&Mqkef>z6-{U=@Sefuz zl3}Q)=+8TUYow&l592lapeXM=5liZ!A5!1q#L9Y_vFbSyF%Kb1t7f71>@zsQNSfC) zyaN}O-WEi^enWR#OhC6nU4G>;b8chcXnxVQWpH--MEb2MlK5_99uVPuveZI>mz%bT zJJ2b`cDonI>eC{XpMKtf9q~%g-_0^3Y_A&Q9f-xuMJefC4#U+kqb|(hi`p-b~8YwKuS%+#KN5Lh(9hQBP;+C=*k=;=%*urN*LRU3iF7zSuMbfb9 z=Xr=tFQ!$JrSN4_fWUo`2xheJB5CNzeTY%w{re3BQkGV@XPCp5j-yb@dhnGq+A9a% z+!B7ZQ=$cv5SFC_8qa$Ygnu+dBJTiv24*S(1PYwX1~NLi5S^Xr3sJxhN2y9R}TV z*sO@L?|-{jqu;_7+#-Syqj3PFN)KSHFLS3D%^?l+9Cm2LLxAT#FiEn8s^DTMbUcQa z-YG(DLp1E!%d#R`bJ_nl59N;~!*ZE};Ho_iFRL7fPU;JLH3us^%}3Dgo{z-yTs~dy zZ3gCN)xk$K9M=x?3Rw0Da<5#VgSWPji|x#(J^nR1wwKa~LQl9PsYH%1I1cgp)1dWk z5vg0W3#NEm;?2dPyyvmYsHe{IziK^H%x4SG%lAo=STcDky#ZQ$7^}ok3bH0d;(>WA zXG@+6X~#qeFL%U1)1T1#P7F*cM{(_X>U5uQ70QX$qP6)i`atHsokz=f_}g~`gmNJcGNx1I7cJtw?JU0GWckSQeVv;@8fY9mOcknPDW7$bEFZQfL*@Bc zfAAh{g)wCFBpc{|eGMZkUI)AmJIeamQ-hC((F!#`6u_&+@ z{4R(;YY$C-AJD{G33y@r7rPqQxA1qyZ>r-QMpk{h49yZ{=-OEzyilRS%Y12~51DTs z|L`RLh!LC&e~no|5_nR#0``q!Ogsx+?z6`s{AT=}7HV9f;ROc-9dmbKRzwlpHJ$>E z~H987A}rTLBnkfnPHey@IxiHm%xbL|*(czS|)3+~{G z6`Me^DFRn5Is>`!LxR}HDzIbPC`gs8gn;oZtDWr5>jg=0GUt;~_UnE4wB;l=2R@+5 zonm~rofD=6Sm6T3=C+bs32sGZdPvj$I|Ty6 zzHdt;UT4iYva}Aav}@Ch?SptQ+#Qq-t>D)G*#c$*W0`BB9-~V?P$yl z+hM<^3jUD2i?-`};N69ItO=-ryTV5h{QCtNGBD>0grj-$b7~y<4+YY_hk>a6K;P6x zw56jkb@6NAB*(RO>P{`>naXtx-fsc)wJ2;GvS9Z)55V6~z@K&X_85=jYS<3tV15~S zFIj{6cNF*}gSX&aZ^*6j*XFTuCG8E&1)FIGyfxb!(F1WPz1s=$+4oTQT?&>z9?A8U zxCqqF9j7Djy@8b6U$o}%65;kg3VfiHALkO=fj8qLaeSByDY-HN{%TayocX#SrqY1L z5k~~$`FK(|7z92oN>tk69wv@^Or`E1-dp$#@&;GJNr`6sF*O;scrZ`K*F6=vU*#(A zF>gacBqZxiG){7j_Pv z!I9s?F){rCta5Y3pK+_nkN={{Ho*pZbJ=9NZSNmyR1*uEAKikaQ~uzavdPX`T^R~5 z1qp5su|0EID(>}}OYB)c+=a~>EJJtWuW2I8<*`6`(byjrB}P%}5;il5s{-3%H7N0j zCrUOZ+{vhiApJ)c7EKAFt#4iF$zluk41E!IK6y;8#UFr*@sY$n?JU?ywAl^DoD;lZ zGq5CiHO|hD?GTSi!r;0z=uubV>eghk^OQai33HmZ#g$@YE0oo~CUSc}Q{pjG;PA(q zdr+kg|Dw}T&ohvoemox~$E?J0jk3JX9RrU*}2iLv$4cwZ(v>^m>%OF2hYZRfqEH zx1wvAH0kw_%Xz2hvxhq{+Ug2@rS%M8lRqA{`Hz3)l!q+^x1eD2CE{zm9Lki0 zf{i^qF>SmEvOY-uE-9xT=3=;f6-(| zkuN+<$0!gqjKYPy4@56AG5ByHrbJ_Rv{svlbjOEUs z+F>dRp)zv>^NU-dQK3-aI5mt`8|sjyqvv3un=;w4PXY2QvhbefLC~vb{=Y>sc<}55 zdcrAzbZS4rr^VN>dYU4JZuI-1g;TU{#Q-C?j{#>MFDlGB}MV)ie_~F$~K}~oH zo>-TMfg5@F7{EMNFOyLw>k?HeHyeYdvD%?CjS8XEGia; zdZ%*aT%AyH{7wG(BuReLk}k-;r~#YqjKS_pCr~|A1+JN0AfH3r@KsR~N|h}nXS{Y` z#g=1GwaOJTK3t=!O9g0eM(ILCDY~njM-#?ritj#(Hh&i3tLO6|_qGg?^!SfEU#Gyu zsAQ3oar5{G-p_=ucBt|}B!x!||0zI79FbXm%{4ybMUbxCfW~?~=MBSy-H<&RbuM1&5_(Kxm<`Qn8$BE|=ts2Dd`4dvZl} z(QT5Dk!QF2zzf0lvC1%I@hs9(BF22ztWbMt0oo}9L(Y02qp*B4Whz{<*TdxANcyovC%zl8kS zRs?b%U($$6UTkK14SN4wfqCKyP^GF4DxpazGWQk6=Ndro;YLUd(&dyle4@^i0tIH% zuVRj6I$db)#-E?$1s!MfsqyD>uy(vlN(M)uB+If*yrjqa0{`n5{Qva@{x89xp3B3_ zg4{D<7B+W0LVoX2MoY{d8 z_Dq>yXgvu{ek=qr)}2m2ug0tVh!xCU>j|Dt^|WDq1cX`4;5?7LBp#i$kcT6%t4D`d z9?3E@QE?#6m{%$4hv1`;3h(r(mh2t&;M6Bwf`j&X(6dU7gj)3q76|VU561qCZ_2lg z9djPD7b;^;!y|ZFVF$_|ys^4Ixl%t?9UAhDaIoko3g{Gh~pIxucOEY4O(uPj4) zV4f1^?y!$8iyTYT_jF;{%T8im{>Uz@K#Cjal;u>VO7YhvRla-aR#@v*0OCKkQQ1dJ zNucYHP$qXgoG!Zq+k>`4ybI$k>&?dPTF24qnjY+H2I3VzliT(9Bqr%jhx7IY0t5GG z!M{~;^kK?M@NH{iEShrkDi0>pLN)nE2ab~>-*?6DRKRG{CFu%I4t%U zXN!L&50?}PR&JO{U1o|i?yZ1cwqF7@q8Zp2o`ju#KK!5Wk=SWFh$>&Ag(^MMxZW61 zFse+%%@HZIbIE9K=iIM$-`C3uX2&orpi>{}$|7vP2(HPrNGXNnis zY2oC83~Ww~#lb zH4t5T5ncsY3I0hb@Ge|EM9#T@&YuM+zbylPw94SdNJmmSS{Zvci;#ql!(e=tahjOJ zUj0N9d=jfCr?>mDGea;m&t>dTqle_ct$#!=SC;Dxl7#oeH-vN8`7L141t1Q~1qTMd zz?P(JSk_vAiK8}=KGv}g-oSFWCZ(9Ue+t<2=VQRi=}_6!4{1)PU|CRHFYtictCG7zk!vN|g*Ba6gkRe3B`{@?x5(k{v<3 z)sNub?+F6W-P18+{Tx{4DTftR3t{}PS5)tcAF0<=;69!EN7}M0h*Zr^Dq(wz23XtT zd(T5Kvg0NVHZ#SALrwT;Z7Y>BI|v4MvvHO90I}3`5Q3;IRZL32=-GDIG|3!f18YHh z(FERR_yW5xY=J`NX3X504oZ6$Vw2?&EOqMw!*4ZEY@*5A;3Ak>$L66oifP*8N0iCJ zNNqzHJk*e-TdgvLg?|pCV%2YICMQA7Uz`H@!54Hvtu9xyVKL75Uqz4jTP`uE z2vmor@J@lx@LIVgx%p27r+wALpkn4FUpx+VZ0xZ4d^s^WkOt-Fo3U}cDh8a*Aq7t# zK%p$lKG-Y(iLyteYTid`TFbg6tM=kj(hnQYTA^?3AL0HqQFO@M1eLZ!5O8w`|K8yc zM#d#!x@b9VTl!7-X5M*vXjCNG9pj0^ckLjrG6t;XuA+0#&-tvI+~C<8v0kr4O&F#C*8VVLh6ti5_EAQayT!P3M1_-eEgPF)DR&ESQvY zysPRtSoOS*4&8Al1AmUIBSBec1fiQe$>rz^ z(9|r89UIH(2m{tjb!cN8@_NYhoJPN8-Nfc+@?>yxJm~$+#Lay*kW^dEvNXFvzE_rV zm;OLnOD85iehWui;_#8#S{A2MhP{jT6W!nhveafO9j*6~9Mqo$1u|oWEhb}$o>(81 zU)hB{XV|scI+_^WJ_`9UY=6Yr5i6TnpfPa|+{JeIl^Ov*BQ{}`-w_bG-iI-bCj8su zqsSvsKUi{+!v*zYx&1D&bW8F@Fnn1-!?tIWhZVAT-#iujLy{nN%qZ?ej|rSsNum`c z$H{N4i*)ncbm3pIJrMpQ7QJr0BbD-x;P${GNGte>R$pteW6CoqTKpLOT-Er1o0B2* zvKF=1wxBl>Ph;kUt0*S^QSj}X5)C_L4xan!?cRGWAT_3z{Dpm%^g`wte32xE+^)-@ zn|?;v@R7it;a?c^`v63myQB3zMe5?G51Xw=@XlC^T~C{ZYt9()b!*i*6UR4#xVk0qZ%(U~U3q!Ix%2 z3>S{O#n(Ypa5!^)`eRS?VnJH(NxY{m$yE%m11=}0(lE^!B0 zVH*bnF$=lFx3W<(U;(xj+TcnnC4SE_*4z8Ez)s^uKQ%ocW2b)c4D`C$@S`OVxBuP^ z^GEqZ<&z;|{qHjRjcFz`UB^J z?+h8+d1)#dZ1-$HAZ!)T{!3TiB;Gvm|2H=0V-H<0+C`hzSu>UO9LOI>Om-olIGeR6KO8z zN0$x8qhIb!%rqtFbyJG?6~|Gz&DZb@-GwN$L4UOtYAZT~PAi;1;X)+@Ru0gCLz|%J z=P%MIVTeJapFzIqWV{!Z0Q0=(@k){JK*!7x6<#*Lz7QvlyLlMS3YmjQ^)A^x_A7=+ z?Z=#D2S~t0ZEp3K(I|Jq9D`I=!@lGW+Oz!#4b7ZGE%jA7g}704OOqEae^Sc!+U2Ne zS%JMlK784_+aP?Sg&lM|JlNg>+rR!H8y$SXqxl2*W)#lY%X+wAgdK!v`;qc+S;pgf z4EOtF(RW=Is>a`?B1P#?8E(lhxtoshnK>BlB@W}`O6lJ3*XZGY2LP;1H;H10OSF4265WuB@P)CDF`4ZQht5)59?qXSj)+@!_**xAK$BP$u_x8?>k z-JD6sPkSz?A0uKn>7N`g`Kb;%%vG<0%K8# zfsx}*v~pWZb~qWs+MY9T!ukr)mr2KJH8(7jenZZeIO4TKBB1Rz2hW73k%EkOq(bMM zkP6Pw^)JujCjUohksb=>yK>2-twDlZOAW@XS%LGHq>$?G2k?`J6CD#Y{PFS$&n^j2W_Nolxj@I-L`wm3gKd@R~-t`|7Hs3efR0pFVW!&t)t z8c{qFj!TXsx~MK_AZ*V$!W}g2M?;kh#l*967&7X%@O@N;-B){6ta`+F=kjf2-?wjq z4~@>1J(b5GBm6q*X|T_xVFaJ&6OR8yg_F}i9tjh;Tja*QGU_nFoMXfsEcJW@rUwFW z)gNEh2Vagi=8oY0y0g8gVKsEei}0LUIh>ew8a&FfQL8Cj*nkT$(PA=eHJ9g0Ur*q> z|NDjuiX=JZn>)c`&rdkgb%a(wNyN`hzo@n8GwhtQ7)1l0qt=N^^7yP8J)o3H`$cmZ zlY|iON0YdRSMQ)$;|w^s@hR#*m?IGI;i!6PGgZ0sAI_2)&5fpwxPH}Y40@C!Fz|dt z25*)lZ(jvOL7Z_iRryoOC(-NIe!+>M^WZJ*!FtzW5ZNQaOT04SBcc|P6Z+?{wWgbt z_4H$pygYYO&jnv6=m!pRUR1Ct8Y_o@7d zE=zxd{;3hX(QO_4WvK_#67t~BKRsBg_5!|+;RO#=R^#BLa+oXUDI7ob3fL^=F!Wp% z**g9>NpG7A)rx0n%M&9sn_-7{yJq8V&+E8vlM!!Vx|ce42C`ib`+UMf;Ys&i{Hf?c z+{P3U|AR-cik;00881AJG{X2KW9&BEk6zdhp35>xUAZ-PR@xU;w=SV`=8N(Cs|0rb z7Kf8w-ypP63#!g+Lz%`*`V=R_HE?H$RjIB&}z2kOn#_i>Kk>wU91$=&tVR7rv|b)PY#qGC6gQ3RzxfEp>W9B3*4^} zu0C=eJHMH7Tb4y&aX)+AosDq1(ws^u@}TuQAL`{sz(uJ@ICxN#(3`$Uoua9cu#EapgY+w*B$|eT7 z|1V)+^QIsM81P+Z{(tZS;i8ffX55eBzU72Ujf&*KQF+j0&lC?Cw|v0n0Lj|-6+CpSg&SXc zl9hjDInV3yn6otunmrlg>5L)Qt$$OfwB-=~@bZBSMH%`sw2-=2PNik~YwYl2H@TEl zN*B0h!Q0LI@!5zYH2k(M_fmN$>crNtd(<)1oPG+af(3kjx=j#zDFk=#+64s~6KPx6 zG1#;AH_q52iytm`GiJ;R&ULs40)}gGV3s^oZpJ zwAi}{wh206)a4`+m%zNLZtOi%eoudgTmabzOF(^u0^Rm%0JE6KYuegO!I}$VxWV-V zt|!`L`MyJ-TdqW=UiBjUhjP}z=h;r*f{(rYkk~D-$xUv=~< zGqf}5_dQ8i+>=6{?dD+ZMKL;XG7k57)uQc@k^D{@O>DBdEcmmJF^&qoVIMCJZKh8# zvvnQrw3&$o0|$vqT^u~kK1aJvOYzcSgeCjk=$;>%+`7&T;480@VP2IlEz+d9t|&AQ z$i(5D{vc%^0jgvM_D!6MM>-dutY zjGnusY|d@yeY1R7WZ zK^S2nj zcbvl?9J7eeRn+1hx=kf}w$u`x)*viA@PR%)5shmmy`W&#a$UGWQa#@4{3)lpc$vuB%Ak`Pxl$nKj{;p8?EPFiFHclOtOuwN67@meyK z-(EP-u@_|c!r*w68L!VhC^<=2DCEKK5r(i_r&r({_YE8hr*ctZHk7Wq1=W3QKYsT; z)fig_8_yrY%Kutv&A?rVpJqZG#q9Wjp(U_;+A8em$gk9s%mb~|cW|lv9KLtCLfcOz zkcuM4CGtrJpW6>{>5FRSqo2;rSg}qZwP`Vl?NbN8S?^(F!aC&AQXnU+t@7`$3{;a> zC$B7GXhlyf%SD}s$eXd`l?%(U4u8kAt$V17Pb^$2t|V5BjZw5UT(Bs$i7e`V3*!fJ zp+CKi;&sBf{Za5)vlMk&$MCiD_mYcOhn1H2of5I0&kTeEbQ1b&bp~vtN*+)ot>vK@9L$Ai;uq*p9*g>Sd9o!Tk`bm$j=HniZ}L$_en znx(LNk}=m5kc|E@(*#;c=G+hSNP$(!9{RRQi+=*QvCiQf%;>od3x8aP$q{2Q{LxEc zyTU!P#>|jW2sEL8$xCYgqJpNv?;gl+Ee!(0COgi%p`19jY^o}8Qs*eZrF`k%c z*^0#nW%>Bauc=@D9@IOq0nZ!MldDU{bFccHsFp@H-F6cof6h6)8>h#qryqb`^I|%B ztPBJfE`#(HK)yO?^F8a!AuMbGx3o!ye-|{5PV{xhu&!kg>zB>mn-zA39|SBKRy1L*5%$+zqYh12^>sBESeB(+JlmCdk=nC z=V6LX0Qd0cEn!V?k>GMj96CO(gP!mzc%MW`()>pJ@qQ-w_RR#J9qi2A8HZW+ckuOM z5x)L6%ROI_Cbf5^_!aAZ3fx~GMo)IHm~~-3axo(~6~-2Rlw6JqzK7uajTUk|b`Zy= zjO2aTcTzVq2I9}h(HR?;qV%b25L-4xV{C6Cj_we2#%;!Lqr33sY$K3a`2+Tj45iQ0 z3Q%;#KBAub4@4Bo1a_-ua%$@@Le`3N!sCl-1alX~ljB>hz%9@gERb=o$2Ah=74dNO z{VaU$V!=y2)Z-OuQUskht{{ytK*N8G;jnfxo%hXx4;P5>_95B0@O2=*Jj~|UWslHF z_Am5Xy~NUoqWr8VLtNv^JU0p}aHPltkkow*?h-NNLvu5JE?GwRcqEcK(Jz9fZC~l{ zuwhuZrGtpS_{^9E<{0eNP8=f#+5W2<_P*`Fj)blWL_NL08DF$zws=Tnnq5 z!>NCh6VZ)G!2o@0{&s>RS@rP+8%1|R+|Df6TQCiO&wLI!IUHQxH$dw2f79QZK3L%( z!Y@)}o|eM~-0_Dmv9<5AAacW6l)5UW%}t)=a|>H?A^o z@I>VHZ^dJS`gk{?10X_rFIi+$-xWu}OMw)5yySMP5I)Y^_zn#LN_HV@IqZC>; zvQB^EUE0|Yfqv?3BrR_T`Q@o^>pdd^qAfp?yR{QBGvusb{@r+t9rX^%RmGuY|1eeZ zYZT;}sqro&<5BOdC|`8?Gu`RcQ$cjv;DN&``0#H$j25k+5ALtWSr@Nh?tjz+1OvuNV!)F@Y$i8#DC`_NKIn7Jabh(#PmEJtW~kA+Q`l+ zZLM~8CQ{rSPg!oY-m(A9{L^uY(x`9(;KA55y5P75cjT%fem$RvvYm-U^wur7`6-k3 zeTm02QCX08K7kr-F++UcfJSC1plo7Je$SbRm-m)4j;1(_v}z`n(@#UwN4DRQyGV)| z?|1Xm!_C;b)ha}gvdq2E5JcgG#Sxs#c z#F0ktC(@#FD zeM-gTa05uLzXBqQRQYXYv+b;%vq;vS3VgnM48QfwDH3j>59T*^z>|)pOD>(kN=BKGSi`Rel1C?7U#B4 zT}SsZe&W>8r*TOZ+wFBH!++Wau)l}-s)G&C{u0Y|p1Os1ErkNb1D{E}y##f0`$vyj zZN|uZ`yt}_b9m>Of|W-%(ov!(@mt$&yt(=@mC+7?G2Ka(Sw&;XrqBT}9;bo92T$OH zUz+gO^aqhIT8VP;mxxo{O&Ix~BD{<1B<1bLur>dPKy*Sgk-R>w;+^jq;S8ly*f-f3 zLO+Fr|D#L7`qe+l>+|Bc@#R#A%ACaa{+JHhGIR0p{*R=$aybq7>*wg_1;? zlqP*Os>nPh6+%)7g+hjKp0!Idq&X@Y6bgybph2Q{zdyd$_5K5{>o{lcXFd12@6SE@ zF&jRnZ3MOPI@Ad+#fAh?Hly|w26xT9(ITO??0fitt=gUr)qUheJ<9@ zk5h^MIy}^MjY@Scp}`!Rz&d6NssA0rf1l8ff5*@9b&p(yUn=)u!(4>^x53c%B_2i3 zSd#(exp;r9g*Vp!AKm@j3&a|fp{Zgn%1;xuI5i{)=f?}MH8O_(dZsIQnn*!=L?Cgm zISS&}k3*5260>f>I1Ichu$*%F2-h(qfk9!%;g!%RMB8uYp5s6m*&hc7tLu1M6P)Sm z={&|(#e&N;m(tJWK}2GD9qem$A%ouIcvmMB1Z}Uwo*9)8cy22Ztmpcu<+q_;dmR>C zX`z3)b30Ey71)hWAn2nJI13eF=R%GnF}wp7H;iNdS$i(O{SAu#RYAI8B~~wc&+WGi ziK%ikiIv`iJI*WeXLn1JX07Lx5{XDIHliIZk8t=DKiKDXF-O)oXaZ|J%iTYc^KcC0alY&QC5r7%1#P$*{m$` zENCG<-ZcUbFEnGuXdvpah_c%Z*tfYts1%@u`@~+7+Go@F67TL)Y4brc)F;TeO%wy4 zxJ?MFAEVlxMKx(JxZL+)2b|HY$qvtnK{`Q(4c;wHW0x|V&uAvjtb72!8DIKAFoT!7 zj}Mn#8=$}MXM8MNML%C?2lFulMq*A9jEssy>91U*nu$=dA{T{MUIFP3TX{Ye%Q+5p zIqnmAN31W`V|U~bYOU~rFE=Sv7`0-8mWPUS7`4xw>HQ(MJzmuZ3*44zw~%A%(_Q$+yIG67`S8>c3a9-gy$c z#Mue&x<&BizJG%qLuQ~a^#Rmd#_7oS8zi7u4B51g;3%LB8I~59v~Dr`c+`wGooMGv zji$hE#mBgn;e&-v4l;L(_$%H`2dyJ{w6f_p%I@#StG?yrbSgqK*Xbp1+Jce&wm9#! z0eY^UiwaT?K!bM|Dm-)HeFb1(AIHWd=BVh@2Rlr{NHwRU#D!Tgq7u;@RR1gPeDQ<6 zoSF@fMi93zn-47KH#xc05{&ZSqDDJQub0KZo#Ak#ts?BMeR;IwErnWOOUq$u^NkH?EEvzvyVQhE;Xt9UI0+m^0eDW=%XHv1? zT`LTC=0h0`$2!eqaBtCMl8SGj-?VJ>boqt@{BM+N?L^y0$MN0a3v{QaD|q*x!{vWw zva5z)gZ{A*jul-`+b=yPce(xX*!hWsrzC*uHaRj(P^aai!<>HUFUlHle(IYqgz21X zVbtc~YnC_bhqD-NV#b|IH#COKm>{O^b*8AD2+Z zdc;K@tE#Tg-drUfUO!F!%8M{E;Z+<{g4<{md%M$VzNU;W`CD0z61!{rL zIIKJg3hZo9qhTc}EEwbRuyI6w{y#e3Zvw0ueMrjQ<%37NGJ7ge8VbCRkmUxMRA;LO zdvX6)kcj&N*3B}kj)VaF>7^#?R-nU*S!+>&)Mij~()8&0cLfz z;@_rs#L#6qB_Wl3r8}`C!NL@roNDo*uqA%caYM&?ajO$jnGhHG69N=&@z=;nGrQhS zhob3RwtNDG`MommGw&)HfAo~xc1iS(;0mD zt{CeKPtag6Vlr|i*fI0f@cYqi>`{G5&a8>1u3g>OG!l#Bla`?HfHJeEIt|XdtR{+K zmc*Zkfvcb&KDFgh7msEz@0mo^K7f?t(0ax0{;zs z^!?F=r^6nRIJ+jQf4GddRr-}x!=E>J$>I==AG%1NYV{Mg*AjaU=VMLD8GM(Oi0)@D z@`I{n;r#D96ny1OHvGFrP9M&KD9vgti#39~R|=r`GRMmdI1OFRH89b*lGr+*pjAQn z(9%gz%jN{FjyGWTjS90T#?FAwBXN+k)F(;%Q*rBsll1Pw8d@9hoCu$(#AnSf;Pu}M z`q1hTDf#u396#BLC%Ij5PIx-HzdB}hez+gq!t3Dxm0{&fUZVcSEx7-(4=l-K(O#Co zUyC3rt2Io5mwlsk^cY&tGlaDVa;VhUJj}kfl-B(o1&wcGG-gjSCi2J0`jvS)WHa6g#S0*GKnwS3lY=~O;?S~QTRG8$PihDM3Sy{Vv5ct&+tu7ou+2wiI zaF2(YGX|{Imy0u6$7|8qG77Rw{79(NKfYJtH_oFg%rtLHB42n(m?r(ZMt6ZMD*Su` z@!WlHwktwkU@CZeNaD@K)0n^?jx=-9Ct`ZQ47_!xvA<;v@nhyf`d-@(T1r$I^$MC+QFrRL(uTx1n6jU?{uwyG@&<@XxgyoS1rnR_$CmW#f4xX zvmV;7>EhJfaOSD~CSK*O^|bOI*8{Rj3uRCHLxUJWd#Oymm+C9JwBRmp(Thb;@nR8w zB;As=8m=SPFPX4H0$s3b=3TJ0xkEC|`^YD;Y^W?>fIYLPFpq|8@X)235YayeB7VxF z(V|>x`qvcGo-C#Jcc`*~V)ytm`$i#Oegfk^UXE-2djw66(^(hUHypozBA%;%3~Q#o z;@Qsp&ELBEHC;ZZ0dL4g;7JEN{Bg}3jhvpth*};`pYG(D*QHu@@oFJC@)<4m>LO8% z!f;GVoK-8nMofJLS;25oIQrdzDD*U=y+srnFIvY6TC4y8pGN4zGf?p59h}*!0iM>E ztt5kvB0XJ7TZ0aQ?%X^4nl+KQEj$ahggk-f5*6C{REIG(pNLmBXAp_lW@uu>Sk1gA z(7$;<|InXJm}h$zos%oD%rFL;^xoBM4YFV}zfR%!q+_sj!4zKblLO$qnx);(RT!1N zrTpIUS`1jcj0wp}Mv3hrpr*DAj@Fm}d~Bm?3TL28Mh|~mTtl&G9M`zh1*iXRhuKgB zYgBV^-ZlZMI2cZP9;}6mf_mKB`xWlJt%Qme*X zvs-cy^skdO_cnt_;BQR2x1Aw_X*iZM8{Z#%!h8C;mRRz-&`Rqq?3+}Ej^SGP?INd* zyjhA5f7*jlM-A+=_)HwGw&9ikLb>iHE@RgB7~T&_vx*&E;O=`FLTwIX@K;@k9N)>f zHHP59RYjP%bODUFH_*9$iuk&($;!Fdj&7c~8BFKAf?*MHHp6R-w63or3k64D{%U=? zC~7O-x_BEKJLTA(r(bZ2ju`4sGNpT+en6Q+8s+kV*+OB|s_Aq>H9nO3F_8EWUp9ko@MF-m_>4M5JMXG41#Ps)z zV9qXk)EKVf{Ny3DWw9-OUX#GDHJHZpQVAjHqZN1>INw(2cKGoK@Wr)Y3|u#g?`C|2 z`lOTm0EY$`n_mpxe`8T>&sUV*{ENPD;?5k^Ssa)LNhn*`0-nz%GYQQX>8;pABtllG zC9MuRLrZYJwmAj~C-UEze}-90&Vb3O*_ub#A8_UJb4~)^@%K$x-qORZTvmKC<5snn?>HkJ&SoWJTaX=i{7ORYvJ^b+ zJ_SNvg_0pAh6X%)1{-X`=>n5@swQRxYt&U?zJ?Aazb8mkMfgh>Hqn`jHqlGl1z^I{ z0F0NM#Tbu-z>zESY3JgPyiQSTtnG-w=DaYN@^b??PU)v3Z2`F1+zFob#6bGZchGSv z0B3%chsu&n;v)Z)UpnN8w?4$c$lP_98eob}(N6H|1J~cWXbGddB9Hn%tj1ttRnqgr zl)K-EvLW(k@RnRJQQga`llVJS6J|xqh)Z$%`3qjmMcl{IYkhiyp;dIr3tmR zbi>4myZC0i4&QX08l*nUB`apB@=Z8Tj;VVMjWwrOKI=8rwqVd;zdtYgitBYhpqdpmex7e@k3Tg zGQAB~N#SlYvfWP?))2ou3U-h9s|>V*$2mctX2kAk9<}XH=&K(dMt-pe@B+=Ezr2 zzMBg*lg6<4%TWxg^TKgEdpNX`<1zHeu!Xi2yce>!!Lep7EKOgEdvXhiiOWfN8on1a z_a;HklQHUadIs!k|4#n2WMNSKASr6QNrU<{KvAm{3|@8t+bP9}ZVASp;aW`aqA3`) zg!8e?K95^ge#PodHu&xFUE(OQlUc57#F(BnWe&-d@~0&1z~I4)R8L(DBl9R8xhzf? zLGIom^q59}?Ep+4q4{3X)c4sC?*?_ijQtPbf}t=|X~XqFy!=9r{19M6GOl2R*;``H zQvRUFOL$h`iYIg`hBI3snN` z;Q5DR!Q5^{CGJ1vQhO48j;>?fE8}76btz_G;(pv}6b5=T4v<~*R5`wa3pl*ghViHc z7__JrjW15enU+gvt|-^}m&JpO{HaiU>>~{mjv((Rn_`oGJPp}Uhqcm@?6@C+KJ_v% z7yHbg{91zP=K5*`ZU`~Ik3EKJ$FpEuV8HHUCNt(-r9qwGr;%!-+ z&+iir1&93#jP}S&l=P^A--$C=`EjIAA939J-_|fqQi!!~Z-nXc(yS%^rPU>VaQ_XL zYy8TwY_m7uPKyK_%HcZJTT=P25|mL`Ac_|%JDCZO&ww@VLagIFY1Tzrk*!(e1U=to zpkAT`Q#K&ao8cyg;zl7fBl`^RwM#7yME-#b7h>?z<6*L6`Bf0qwjjSaUg4_|7wR~? z9H*3Tg-`#=DZ}G|rG+p%7&%TlxY=vGF9(Kiim{o(5+I?x8kabEz^bQjaQp3JydUEO zG+z4&pH4kP=+0Hz+6Lxg;u z!O9sNXG#+a#A7h!L>QRw@_?`vku_t=MYuQ07`8?Z!;b$V(cA&pQ!mpYFKIib$eqM_ zqoR zL;koWm+gH99Zi{Vww=ovMjNnAXDtx-_0T6rQ{jun4EXlt58AFy=5}PyIgM>S`Yi~g z<(d+>K|8nlMQ$Zj>v=FjFI-S4H6B8%{79Lg7(*g+z(#gARPNjg74f}ju{(*_%YP<| zwF9B-@)Nug(Ty|N#hBB52e->JoyGRdA@?r$&vJZawUA03&g*J<}}t} z;~cb71!H4IFFu$a@;FZYe;ScJS))|***d7%cM|rkTMh?9-&*wIvGGZ#s&Upb?73l#YtOU6HSH=?!n?trr z{shhZ#f(^9A@TCN&(A#0Lf8~htQRT+cKKVVo1Q@49Qr`4LLTr%1mgG(lP_T7g@ZJy zc@oI%eu`#}$#l<$-=HvI8#*7`Muw}i;9-#)sEnLOx&I8Ha_lC~zphV$obJP-?fJZ2 zFr8uEP!4&>^= z_T+Xlf9Nc1ZT3e!yL31?Um9xu{>2X)6>0o7H3&6sA-|`-0pIVc^uPo|p0b=c`hJ?q zd5pi4+&y(P?1B|zD9o`61ZJ^Sk8Z(JCkx6pmQu^0pEThk#{+Df&;Izp&D<{&*&{*) zoM)l}QVgA$BbWHR15+*$xp|L4xiB5JZ@Nl0YEOh8W;*byW;eFVb#a_`WoSN`2XaRL zVdtDw9N%~o7S0SuU-u2TC%6eloB(HPX)#_!QV?{NVNYu;1IN6{tQ*IYUn10mZC8R( z>iTIoky;NDJ&$O>P$kI3gj2Q2eRM--IOfSllG9<=A@hU+YuR21>tsHFm~{qE%GQNd z>$HN3!7$X0ealZzSP$;y)mX!m165xoFu7h28uNd`t5@rQEYasJDjPvz{nJ$LYCIg- z-GC!{n_z#w4!SP;NUpXw^W8}t4xcH3*L*|NIJgyRFI>b3u2*8CeIn%PN`HZm>~{J{2tfn>c+rR-xb7t+B3-1>%^rJH_=El7aDeoh&S;PxRxJ&`0 zx4~&^mi)ND^&!|wGbYOnK&eg%A6;BSn|PDpyw@bs8GMhn-WOy_zqHehgDq6jRfH)% zsLjq@SO#`+9VDyy0^EAqK|Iwi;xK$>`Z(1|84T=|E{IG`|`I$!ZgJ(ePPT^|T54 zPf6h$jo9MSGuEh3@(b74#iLU@p6xLV9;1B2750>ti~#FUAQpq2bps>bzw$E?=F%X>I1L`K?6g4^ANwMvWMKHX7QEPGRGoQ|Rz;0?q#t z1lm9Aamr{0zw`YC=-ns5ENtWh#%<2gzh4SA zONXSxpeIN;jCJ2a#m;d0;OPk%kvxrsb<^-tkpP=FR!rNSQsHmOXs60q`SS_vk)(Q*_^JrMjxA%-oh=#dmO^aym`u*y7w5W*)me#a zH_=jodp4n&W?YwG@&-%D-`Tt2m&{qP$T^Qv(;H~PT1`ge{tY6u`z@Gym1B>>B(UE3 zo-&~~=)E-$I8UN1Xha-CUE@mLrBl^hmh(C~K2S!DQX`OuAF%ziGm-Rqi+y%em?e`g z@b=Bgq%SsI=3B5k$otMykl5Bt-rfIBZPnJI$TKE3nighIT#-gPed8RA0QZ z=5?kB@&0@sZ(5zE@wX+|)BBHMsHZyhYBxZ+z&ehnUkPDxDQI1n2#<@l!u8VysIw#k zN_LfC)a;qqXB&cX8ujoo?-XtiZJ?<(ABos`QFO-~cy@0U-69mhGgf{JJFGh(hZjZ$ zF6E%*`WR4o-@(mf&ai#ZhaQ)|fL{9qn9RS2Kp@)=Q!^a#?z$k-aZZqxXnc;&8#}qK z!CPd{wOW$C$`kv-R=^$GGt|t{kSnSh1#P8jI=si79%6zq+vg39T)rKZ()Q3#uD0a% z4H-7*dmj9FVux(;G|E3W2?HH2QmdkB+V-J@>B0H`^qJ;?Ke#jSGV%nfHOq0=T z{{z>%ONn~jEO2`$37#28sa3BM5%4((eL*s;ovjqN1m)u;zjOv z=0#~m60=$UV1KcN|J|?)z5P1DrpJ}@MR?%ieZT2Y#1Sx_eVewgmVwM!(%>WX0n^{5 zf3p7z`PaHmb z{V0ctcZJ!;@w2dzu|@eoQ|5i(VhA%!gd5B(%-n;Hg&a)s~sRxft!twi`2T(BV46PBJ zRn8jS5ia9v)enW7Td!R&rGkjzmr3IW$M( z#@)M_U;PE(B3VQI6nn5I#Q=?*Z;)dTFME17<)kYr?@&cx}jxbA~6cd*+O zh*4Rw%$57;B+h8IZjcBF>gv+?lXxx#F8G3VIu7sTma`OBbU)q-5p4F=pRfda#Rz$d$@(=uUZ>u&`% zGhURYc4t$Aj;lm}-A*tG)`m)+HqT|A1@4UrLEj`vw##ijx!dl5mjuVb{?G&d3cXHf z_$)zLYa4J_%;{bu+RV-s?_g+_5>p$w7=_+CFshS6C@yiW@!R(p%ikSDofn4Kwpt23 zYL-$?xPQr*{D0nFy<2(-=P@*so*SW<&*6Tc2vgRT_Y>CDCs`<2} ze-u+Dt22qGq%lx#g#0bNM0B6?Fe2a_EUUN(e%T7_xO*TzZxkV)%od_(+akunB87tb zbaX!d2y13tq|wK(V0By`t+(g{^2Ca{dh$L7z2Vrxrg`{Xd4RfH8Z8H(PNzR#}Arwecacvp&|t{T%ObZwk9y0;>TdT?LbKOs$buUCq(bGu$*VEA}`{DY<49E^&3nWw&{gnmS zH|K9cVef7FM(#XXanE4(#2wTsxe1oEZYHvei}2L#zv&*&Od`u^Wo2Ax#l>T?%<+|87^D&jzcuDUz5Yw=JlR8r zmdoJukw$1<>j68jB=a8aa^rT$TFlt3PNMsLI{Vq%41TOaa-7Q`k1VLB)2u$glKt9@ zlxHeQ96h@JoqgR!({^?aIYAYKQ~~~f4afK;wa)2W{U6U2%_7W zcf4YIU9u_cJY=^mMP1+fH8(iV;Ngd7shR97j1zMt(ZfloXrad&abL^~yKdpm5lL_x z^<*Ap6l2)(8d{OAiO~;2z}-8V_w-2)^~oK?Qr|Et|M5Pzj}F8ctx=G4->A=@4NIj^4g#r@n(H@3qCAXm2(z8_O{d^qhk0gnl^F>cO9PqS z-}Ins1#YOOq+I&}996yn0Z-fTkF@ zWdEXeMn_L~Ed9%KxHK7mf1Se$Obo#vrVG(5E+2-(Q_w#tm41|*#|k|V2B8PWH47cO zA!C|2`+7K!j`|?HwGXFaivws@d=2C#YO{-E9PnfRO_Y-5lIl~!iOu*gT9Up5T8&eQ zX;wRIxpWuLb9;Q(oS9(2+d%~-oN)TQLcYM*Mf}6*JT5b4fbCvWMoEFeL7fy#-Rp#f z$BT&D^>)hIiQ{l=by0TTWajDtgm!!-ORIJXMyxckK`Qywo8lTTPi{ z?`h1E%y1mm@}?XKhOik8w8e<~PW=kOv6N#)-{bRay%JG<^6hH(za6Mjxf2gGTm6hryO$Qhw?i|?s&ljp2JTdpA0RGbygi=nIdGwEm zn=Ka6gb+&@R!_m8C0eZ6TuIPXc!&OS@hEWa4pE5z1Y+KDP&DueoZou$_mXv_a`F)f z+aidAbN@p8g9$KcpBj$bk;X>*RqHlvw6r!jrjiK6^OJ~VX7+C*a!nNblI88PZNGbq6*S6R%{m4wdscG z>vQRsnU`Rza{(57yopV1m*AJ*W#Tyl$vh7e)O(}L^r#uI79#iXwC)^M?6oxR$kpRV zt=bP?4#hC5Wwh87)hwL*f%Bi=^`|GjzEGcq!|+O^m+Wg8L$#t^?ADc13|llA%O2Lj z_6>*8ZoGzv-B z_VyGl{WgzfYlgsiP79H#3?&XSk?^G?2jpXtL1p{^EDh(u>tSE4DyfFSdppr5{2HiB zzXjtbA)qNU3FR^xVfm>^jK9_`BGfAezot#a4?9EY(A#AYbx$37V?yz>x->2Rah^Vz zl@1No+o9A!oplpbCMw-mA*y2ydP&A&(I$>ne)cHOLrsU{LkZ%!tA{Y?#XR(EEvN0= zJn<#q8>zkKORgAwq>m>Ef?2^#W@Z5AKarbA>x2|ARyZ6Y&#JTWd(WWS_jWuzaFKY_ z_mjv$b*gtG0}n+Ru?jy|;j6n1{3$|%H6>jwFsN~!_xHmx%LLCt;^t<7XRc?$q8a-k zj=u_$Wt3RETM;1FT*N&`V(ddMqoC2Y0`$Ud80nJ}v0d^F#%u^7e|`!wzkbBS#<%d&ch(zF%!*q7RP_euNC)XRznXZAcF*!ALIclw_5;%727f41Y^7>eVSEeyQe0I~3l`x zTk-CRmHw{*I;TaH?cw$r%O~1{zu{NZx;}^Jtn-?B@iIu=+aVHpZZ@c|Ji#+jUIP0y zmT($M7un!lj`u46gJT6^jHbH}#Ge8v-mVAw_aDLlaiFheEGD<6H$rjLR5(%Q1ywQI z=#me!*yJr**i-kBWUUzG`8#mDgTQc9-S>sqtwgMlor|}{)Yy}EZtTb|mdm=J?`1pO!r| zfO9Qnm?tj?sflV3!fu10g&7>LKmdc(TcGMn6O{P1*DTz%6zj()lcAXR;8~agV>(|T zduRuq*mVib{Bx*JR9+41=1W(<5LhY*5;@WNDOUWTDE z3U+IO#aDmESZoKrHYg`U@L|vjm<=V_XLvyyP?+&STeMoDuh}Q1& zM)gVCaEZxF*gLg^9Q?&O`c9o9nMM!DhK3<f&?*sX{Q;toDGi38P5`W{W z82BYL4YbQ9u(nm6@K#@zRp{illH-YNgRnl^@99pHeV%}tQyo>WddypGavtP%oQ4TH z3h?xSGk7SdFz#dR}uT%f|bXmpXSv%SHx*3`;a zLl57Zo+Fn|{osqXWx|btVU$goz%++yz}4g$0&8~Tj7=fDznTlMNI)EHwnh@y<=;rt ztsL?|=>*jmvA{%sN2@HW6QH|yLCwoH3BHa^03VibhH>AIG&pY;u_tXDVe0`uXuCB> zIi80)4NpmH(jT<=rOFcRL(q2c62@KK%I?lgqUj+mptJHLui&8_+?x3fm0T9EFS7i& zrnl$dRlOUWVy}>)p(Xr@_f?Qotb$mR2wXA$0*txKQjyQE=m{<@c5Oi_%`v@#D}-)S z>t|{3yp-D%cji&Wxp5df*%i#poaxMni#3vMCs23CMHp524;-tLaIfrjES;divRq;% zIOqY?JXwLVCz^;EN51nuWduPNj`JrEsu6VsNoKXzHBv3|irg3<=NxY-C(HTY?nh7$$6OfeGUuru=L{U(3Lstb z974AvDsX9LhTD+To)SgPI3uW5%YpHrg*-A>6k?U5p)|CP-Z?CVCJ`1)gGWBm7Y;_f zb7{EOXAp#_5-!#4rJWnHXsFK@`gWx_TFv}GYgci}YW+mm7(9(tYpaEG`>xTUzoM+0 zRU^vpm=1{=bKv39J~CY|gQh(61b^8j@OPpzlUzHU9h@k|Y9^#X$oez5o6_+FiHWep?U@agG8J^;BB8HyYiOIKrH$6C5&)!la6~SPwy*gMe&UimAx95R z-H~UenCUQu1zYjKBL$}GQWZF)44~y4bH1j}VRHAx0E{Z?Qm^|I-ltq3O2AV(C&dXiV!Q!BY^}9}(ooo#SMDK>==UBKW=YEGZE0 zg3vef@%)QUp1_q|R6%1M|Hf+-^ywYNhTa{#&VfoylR*B^#g&BpdJaG7&0-oXCtwII zp=v{~@W<*HB4Dq_JUZ`$igTY~QuzQCxEh7Cm5SlNrvmKAZ$su4f4nB;#>PrJE#kjs)i*PNUyyZW8;~{hVXp3>?>Pf=?k+5l(&Nm0pn{ z5;txVvsZ?UCMq+gYs)xw{R~8W7Qv4x*;duK4u_{^k+$#|WcKe6{G21lHI(WzwHqIB z&p|sWO;4vg=RcYgdPuH*~7cpwW* zCg0)(){3$9^2xlC4Lg_s$=U48H=5YkK0?+FnKIS|io~G4n#^73hIL;w;QTxjGA}m~ zc7E9mmOaHJ{hAQ(p~DXFiOoaNX-C1la{~Pv*~R-jpQH6};@3H0#$Gko_w4dA+X zGpSx!&nX&7c<)v$KlJ`_a;`rWmMWgdn4wU#TW!ZSFFA+O@|?pp`Z29jSWTXiNQ`JH zLL)0tcIHVfSg~7|%(~!;fhC^cBbLZ^bEzXcF|JYWfd=hLYgVxeli6x7HS zq2F9tA~|&}?Zhbqx1*Zpp7yMuZ8Zy zzSCU84Byi~EWDhASlK zV=3I*vKd0Bdg6~V2?n3f03`=m(6WsK)@TO9Zczgz+wExFINeHy+d@iyoQ*0=uVI<; zc9t#|VRr}hkbo^`z%Su0=MvgRkK}#8RWGcop>sA8*u&223Zt~rA4oFiNIRWmMm$WitVTl1&+{l=rh zZ{cBsDr@V*wJLD^<@*HmacE@-hI|;sn%t#ytNvrW<>|+zDK3IL=fv56_%8fXcnhH^ z8=+%*3CK1Wp`GJ%Y9?_GPApU@Km^b>#UV! z-fVV{=LOXGnn!x)zJpG4CvK{q#lD*90S;aPDD$!mrUa`qc1~KXcV`;-|7d~2*FUVR z%PF1_Ql$HZ>M>iSq$Yu8>%Uca#wib557nZtn=smpY{QZnH&Ir! zoj5&HhW>fX{QtT`vh70EIN^JR^aB%CPZI7L9s3 z9cs(+=!4M$P)^LmQp;`NS-lzx^Kv*Ec{@#dRS7yANid|ppX_|Nf$XU|fyIYYXz!bu zjPM&-=BLP1EIv_!*%n>c^{*AbT4a&l|5B*`xi|FcBym`Ad=e&ga~tQ`E2)rs0Xa>t zaf*HzzhS2{*e?4^d_F0`mPsecyuHGB5&h8lqb+!F4!f#;&b>EjCnF=^K>D|FX>V^X zAv)~JFb{0$i}G4p!kz@TrQhj%dUc+ER9 zIOnMxyKdoTj2O~pE_{uK8Tt{#fc_&!(L*5I5svjgZE5@DQWVTNgZ#osVsPvU9aX8~ z-6)*^`zns3e(+-OlRgVO;&kz`R}cnpNnNq9USijuj(HAMxS2nZYe?eKQME78l-p|c z%GXm8{T8f${{}PbK3p+}V)ow;9OX5i$kb;dhFe3{JRQ`!g?MmKkByC&uIalzgAFB| z!&6=Z4qdRZoL}QXgX?o(q0kZ{^mZ?b=&XZp3+F&V{SWHjBZ!6%qyW`U)%aX`jzwp~ z!7=qBBy?I}Ut%lm-`vjgs$7MQPuGx#Ra|mrEh2MZ8YsDJ$D*ro)cX8=%s#XVo&prdS`3Ir%%NY8_heBGKIrc_>#DIV<@XtRBRc~W?uWv@XE<6F#De zX$}ou9!fQ@UBG>R#Ms9Df;8fwE|+@RK!tW1bA*-!_~z?1ZeA}3x8B#huRqek*XbrL z4ZlfhY9q+;mLV{Rvq8g|E4h|g4b1wxfH!ONC)|9o8T`D@f-QeF%N(hto+1QY_cnol z{d5TVL>T?x1URuk7WxF|;L7f^ysQ1Hu=Q1PO{2RE&Yjnc-~SV09&eIC`@Boge>fEH zO^5(py$7K4ggpjO@ic58&doOp5O;}bs0O?ks#8db)t zJ1kH3PZNPu=gIWMk@=v~c$%jnEsielYN&#N5s7}h7p)>ANga1zt}SLs+VuZ*BV~ho z;r~!|-tk<$VIR*9*@SF`j3^Yo_jM#~Dt^+?)FNp}gSN~hvy3uQktmT-eDCWBg`$uM zi8di6DM{)%&-35&7yo%#=iK*oeLnB^F%(>VpWhWf#x+Fj!1?22sOQ>j!%Z2S<@gis z1+Xwj`T)K+*LCHKU_$$i(*Y2O^iYH1*8Z!*C|cYiQ{^%rNq zK0!{eI|S9IrLbaq314Y!BS>-+ou#>EIQywR4qXqy>Ps8(dXN%xP`MR+QzkPiK^JI= zygY>bDWSi<+Jo2Bc+3pe!qvBoID)AQVVxpK$X#*XXzP76m;VBP@4uw~Dkm`Ir(a@U ztr-QTh1dX`pynZ>McE+gW5+R^z{3tn+*N14r; zr3bEWBRivaA2*JE>&L}17z4ALH~MOWSBL}us& z7M}|sKNw3)elbpR?6cv?-a&d!6QRPx0uW9xvE>Ut8G}=rt ztvH$-X}pFO1^IMuHfN4>y$3T2bTRnfRxIP5UnhFfdF8ruSf>>H} z^kXE7X8K@{gFLveizb5(9&len0av?b^E5v1VQp;0nXrot9Ba`kEsvYR7Q6`{rCWdF z3C~sMxK%--=Av^eGxYP`$BSANs6$~hJu){BFY zzah~qDdudwk13P%g-UB4hGDC4Y!kUk_K9|ar+_(qC}TzQzT1FA4x)|R2o9HKLiO_k zn737$-9hwm5qCeEv~)YFd|k+zyD4$)o;Og+o5UU}42HR`9Kmzo7H-|qMpr4^#W|lR zGa(n>LfF`KR4%xNoq7+rmYgMHdqk3y{v>zuQDsn*1Qs%`c z)qM0E6CVcf#9FP<`Qk7&t~bRayfZY$G}gwtIk^niozbOhXRzSh)JjFB>>*mmqfw;)I=<+W=Cw^-f`zBu>9gloY3osUNa}me zuV^13QQJL0u0M!-uDFAjzaOty5MZ*r1e-Xx7OeiHnjkaeYyk^9H-HRs#>DSS*^*It_Qq><+@@^ET)C|>m|f_cmL@)q2k4+h-q>cfM4SnRJxMo*Pu zWM>p++uS1ij3n4oM%SQ>w1YMBs76){eyDV{*`2}B0j6crRv}kztK@<4ef?xhrUaV> z56RUHb^O4&x5=#?K_Gib1&-Vd#ms+|r1}`cirp7sLhdP}b@X*eB;)X5dJ86Tq_?>K zCP=qXXY13Kg7oibP>nNV6PIYfx}OuFbcYnE9{WOOfBr|c)1ITX$YbbQm&)(GJez#s zOc@WQKH+dj9c<`)#Tn}*SbZf;Y}>k#CYYp__)Ik64{@!X z8bSMKa3o2tX?Y=e7g~~Qbjb2Q8sTsS$iWP}dQzG^Z%xN98`Fs5^a$q7o~__3ewCir zts-9w&X9dtT>FHhtY_OkC3laCV`EVueIb`j6n&atYnP@?L3j!Hx)kt!{>dV7UzFJx zZYHVM{t2f3?IYbK+`Xr#50~4-Q@@O9_{unx+?{)YtZ_ex2XaSnDYrp+rFsbGEQ=>) z&L7}UBJO??G&>5r=?@LYf-uGqDLyv;kqw_KWu3(^uX zW^5Vyzx+d9d3C~~#b4>Wst)`(X)g2Kbp{%m$HRd!UsCWwp4G{ii$#;kr;4$sIn?OGHeuS4R&cKxs5vXbsfZ$s_w0htz zeK>J4vtiF?Ub2)bdt1erFsrMvfum-gl_P9fV**qZEMmeP4wI;>U-4beesXN$3;yZ4 zHAE*Rh&=XwjTUo`^O?m4u(n$RrK7T`OJN1JaD;`6MCE{>?p$uUzEhS>k7D3rwgr@780qcl2~1@&5DF`=WUK)eXQms z>EW6vKBC<0JIoLYD!wD_{|G;vTZdbWHUYH%AyT(*;L6KeP~TmIjep&YzU7ax{`Vi~^j!uP=@cGYmc!f9 zKSamw7KYVVp|g%QepukhD@j_9BO5yCmB!tWn&$v* zs`#;m8b3D&@0w*0Xg7(K+`AEWWN*SN8w^>)MLOtQ<^~VjoKY%t39}$t6|04Wpl{B@ zlFaB=bne3t{(t{Y157LiOVI$*H_*brGJF_ho6Sh$d>$U!QbX4Gs)gm1;&qJm4 zW4?oU9b5*EiX~TzH zR8@)N?O8>56ON?wZL}vaGB>@T_u+_j?L`SlEt|sMQyc_>$0ph3hBE*FotX1ks~*T>4E(( zxJK1O(r$VM>iBo~YHyI#PE|+0J6A9wAQ-p*34@69oXOj31k-+tvpp7F)axQ29-mji z{Ir?OspjOw$!~ z(_tlyv=paZjRMHlD^P`RMPPU<3B^-4vMK9AY)TUA;pH!NmdQw?@7xCXPmM~+s-zCm zd5~+ux?hCDvXZ1hNS1!!?jLJpcQWQ4=b`%B9D4O#EYxlMNmeX0fX*$MuxebD$u6>k zx9OQ=M&$+ieLrWr-M~>AQ}R)N+h-W>1a|3vDT$?w+40|N31FrsALI>mLfP=;d zC_8@#{9brMN_#rga3kr3VRP}uJTbWVcq*ztTu8h4$}qm+6NYwL(WUWnJO_)f*faeK zaYTX(V?UzntM@2uWJY67R^t2Gf3!o#fT7?1(T(3O@?+%Ba^%MxQ0xrlhwRS9c$fE- zv73VzV!|-OV>S(cItkY|6_E|2Vyr~&7oyc7N-pxJvZ5QCF`-nDZ7%YG$!E;yrqwO@ zb%7!?`eYJmmG^<=D>E=6@gvuma^s%}a)gcu8+cnS0tR1tY2e`tAfS~4%U8*kD}vmyBy~$3V;z ziW^V2ljFHI*d*JH^DJ*e&WG8UH+PKqysv>EI~mlR(S#9Cg>jVsw{GDbaX*yyc?Fq5A^hQDdDf3A9y-MmP&Y~91I?TN&q zOok^|Ig3s8>?JE)QebbqIHSF(5<{zQl1f82CadcreUlJ_^WCM`kjg?nohr$WpNmJ2 zrOwzfaVI7%e?d>qZw0d$DMs*1ApIV)jGylq3l37pO6F?Ffc!2cG?<@;v!}$uCPPDR zKB>VO7Hrv${ol~S^){%@c!1?oLUG9SCaySPz$mB7Fe?somfhc1coFaZ@~aD@RqnY49b|3uKP74DMcR>Hkt7%f{oZ-UmrXlxSRyEcZ0RF1}oO@51nHTaBL_Y zy$8dnaBvwtX2I=^I0~urn_^Pp?F4VFreSKvc{;7;5>S`Fysu+X;J@xQdJ1tV%grLN zs$wtL-oC=w@vc#kmma+AJZ`SFr4T~dA=0;V3q%dIfY+D+bBRpi%xocKPeBe(`c4&E z>vIj)!ELB2cY-VwK7nfjUr~QG1=e*P1NzZinI8;7b;mb+OH!>92EW3D>Fis!sIg3rDUJGs5%1pMy_>c~>!<`A>$pn4*6zch zixNC-9Z9BzYsd}FZ-GPUHN@=R4Xo#$gWo5Ok|PK1fWhCB*f6(<*CaWOJ`Y%e)6NSr zg4*%$lorvV#Bg5it#MkU=nmJ;2GYK;ePnEWA#eC`A>XrZ6SmkZG93qViPcF#NVuLw zGfXz~?SDOmEt0KV;wy)kTfPAm@B1bFnSQuK_X~t2E3&d0^RZH-j0aEBVB>NP@};c| zcf9PUYfhfU<4I1a_I(`ueUnMjbZP3gBN~R5ZNY0{lI&XjwGbAyh25MS%>QF<0oE&4 z;rQ`4^iak&X7G<9)*Ol@b`=9S9xP8R zgpBZ)#82rN5nUjNqcwk_y5t$u1T_*zemS|)ZA*1_T;m^^bDJjK_{&d9JC9#y+p=N< zQ!t-~;1!PgRTvB@Z#n%H!?@>W!%PejlXmJ8{jxNR7)}uiEA7OyAgs_9U=JQU z49!u!w2kQ^3HoB}$;6vne>ZYe&rjZ8#?M06zCUq5d~GTkq*x z5VmwX80>ilPYxbH@4aWJc|sLN3@ie_b7%0~iB#A(eHDHfv4C)sSiBOzhY_<((DrG> z_Mksd{z(C!FW`Zv>t%SQunV_~OViX;X?9J!8T9`+k6C)17`gc>#9biIYz8QUa6oJ1mszv=n0npaqeL-7gvWt zOBHlqV9Yuh&PSb?Fw&qN2!mYUz_&&WgCAEzwog47eKmnfTE~H5PZ}nsuV#-dYb2Um z2!o`U?l(Gk(8_sfU@P$k|7>O=lI&i?D7Am%x;P7Z5c`5=t z%R9AXbiq~}7I?(l@$NQ>KKT?rK3ak0>%T+q?R>gR_bE?)Vm>*dKZ_ZkRml-lpHc1q zq@kX9LfhX;gQkrO#NBwwHAl`P3i-joeI>Y}E*&2!TpXc*h(ydhp!fblgD(%A-$8<{Pn{l>&!rRZXhpCEgI9(p(+ziA&%-i}nd~0x3abdUc@m8A{O%j+7ljN-090a!+kMCyhsD5 zs;J;S$NBJ8VK*%r48jGbChVcLRT!==kAYgYV3C{3bGAz(>Oov8^;t46T)hcaoiV4* z560l(uLn4HEC?MIm*DWw44ff)me?r&!v54l;P?S~9uvzTzs?NV#t2B*kpgbNb4h>r zEg0STg@k&_5qmbD+ofJ3j+#OA!1J?sQ&pYk+SNyL9=@Z;9_)p@PzDD-7{l|6a?CRS z0h+c_jXt!S3<|R0bi6(fQ;oU(UY{ZB@uU*ycqoxMbOTLtT1pP_BH^>%9&9yyLX6&U zR@;S>gdF{r-VaZmf&@wh6Ooax1aro(PKeS6K_Lh(gV1L0nbfL6(>K z{%`fk6D2{`|5ODeeorHgmIpw3IiDx7BZIQ{xYphHdbY=x`}>V7!sjh~Gh#_#EH!6{4~^gnuKQ)Rs%M= zm%14G?7#Xdysn^AlvEjy^__yyKnZg)-%H|M_p1qSWJ*gl1K#FO-q6`jiL)L~s_SCSr+ zcEJQTnm;4#_7QZHlVitrMWAiWTf$)qiGh9(Niu&yC#;L%jDBUM`7MetZMYr+&zwcN zLlhU9n2?IImMlcx1Mfq|jPa>3*zj!{J79d7Gov0Q5B+X{Wt9?Z{P!Ez8p%XiJ&x%9 zDvsak)q#Vbf=Rg4ZBkXv*>_AkASG)yuW;E{p7;19cKh`ptnlaV&-3+A>g^3WL8FmM zo7hmpkiim%{chYIOu zJ~fb`-dYsWNTr92OZbx995Q>tYa;mD4u+m@AZs3Vad{yZ`uMySzB8zxYeGxF;>a%g z4L`f02nZPEs8>%TTAonH*6!Y8mxe{|6&XSQN(*ajN7XFIBYXRS1s z*<+N59*Q+z1*|lL{@urjL5nD3fAtby>Ecn+@iZB$1F~U7%tLUVT1n-N;-TZ14V$bv zgvsj{&@WSGK-K$GNVKOgvk!-pPcEWNl8q>&UtS7FvJ2qXrwXV(G!+W1ywSz$JWP7~ z9hxToqV2bWsK*6Aklhi>FP|_0J4OnyBH#|5&YVMn({5taZSEede2}8)M5gJbHC9_j zK;C>qRR5HQAy@2KcO^x9@%lE1zq$yG66)-!d8%M$bChK3c|mRHEc|^#jm;d}2fkP3 zKv6*o&inr<6{9JbkbafddAp6LAJUIGPm^f|M=+0nqlOt>@nplq@lx@JC-`b3BAE7~ zo94Ve3HsHY_*S9>+2K{#Zz0N#ggqkEH5R5m>V-<}2A;rp3dEJy(gi+qk*$kE!*V+a z6ez)}T@A44M?Ls=p5zEGU06Q&j_Pc9fwct_3C%SJ-$%)?^4K1*=ZiC*>;Le!OT^I= zsuG}9dex@FAq7{i&Vn`<8+P!;VR$@S7W(>bKtS;Z!faP!1hzlIp%5+B;oc;g@LvoP zk4_?Bw}=S|Fr&wIN5OD~JR2}bID2d;QJXGH76;{EpVMc~MplEfJ-6b^kQngQ;mls63M6;8ap%hUbaG@8NvqGo&@1i4dBPqPcp}W~@x4P+CvN2Lv)aZBj;sN_ zh&SYlUk4~FaD)xLMK)%!tuT65k@1R9VsqCPqGjPOCgA5;s^KNXY|07c+O^A3|0rjU zvED^jadwxFZ|9Lg;ZX1?_NA6)W6-FQMI?3OQOT)?dK5IHeE2Xr8;-z`J+Q~?9>byc zp3q7~8Nt^@RN$}-doW)fR*wasUBWKR4jHD*o_lbu{R^aRjY37IOz@k+JFIc_>PeU_8b(|kF5XQtYv;QYk{qx_=4_cvKq?MxW#F}^GFr%5GfTU;ru=L-xOZ8cnQs|Q7Qgt0UkXNe z-FL2`ZsVBx0*a;Jw zwkID}1J_>C`P~(kD`rBKV*~aYIHO8QA&#Hn-h*Lj z@b=&+x^;GwYQ1?NBE17Pq)9W+W}PMuwPEIhUN^PQ0 zXXnh`J)}qQZ!>Lr{|F@})e^vXxwCfuj$5hbpl$k@0xp9b&JkBucDl zM1cY$NGAa}T73nD%DvI-Gj|3#-;arO2C*;q=nK(kTtM5ZyxB~Hy{6p3{ z$DrVX6EN^JkoemufKki|n*6UAT&(Pvw!%_kIy#e$?bU!vsp+u&i3}XdwPV{B?dP)J z9-w3K2^7v6u-&#T;2pFUE;i_)%~uolU4T2hTXCAcW#00{^@W)h^>?5oIE)|LmoTRv z#b9z%3b9?3gU2PTp+{>!7{89f)%pq8lSV=IQ7ni}55r?~lkw^rW0>(n2<(r?@mJbB zr_M9y!c}XEGshpIvuQAK>L??V1>|A*^+38^U=66nL}Pu!W6%)P=5tXV5~(nW{rWl@ zyW-MdV5$(i?281PyWxW_3U&O$AI#{`EVCd_gu z=pcPA@d3=ZHe=S&S(wJ{DwiHwN>Z+xp{0B*su-$4ZH6_}?d``44--NAp&a}C%WZuA zArJdM=z^D+~(3QR^J;mp;Q=^wE*-=`n)(3N~%&Lib&#ad_q#SR86k zzcd*^L81=xE~6PI59lz`3kJxVZD*kW$YMOrZh`jyN~yiH0m7d55V&R#dXlHHu8U`b z<}5xr)-{(6Ja7ikWfnB(&Y&LO&k>9FGVHzVci`WlhPgTFO!z%hxY#EIIRhgQ!o4HX zU(dq}|0Q7cGF`OlYob+JhpAezHDrgc0Lf#nxUuXg46<>kb3_ZXRr!2Fr#ezPR~O$Z zwW5IQ1N_g3Ywn(2O#5|G`6@7pZT%cbH0G`%+kOuN>va-jWv<}BB^fBzA0)e09*5&Q zU!&t4N&GU{O)q@Q#CrV}D!6bNKBOI`7iUKE)}Pvq<=5VT?;L5?>!B?`E#=RViQrXF za^Q0JN0Dc#NdB~cqBSqG!9Uv>RK|m#^1s8@in0e_lkpI3;$}E4v-#xD>Mzj${5~z1 z%+1*FFKUZuZfA<2=^+%loa@Ko0J)uSvN>Sy95kmSkjZ>x{%Hph-B|ffNcedyJ&@NKL4m&NDP;m)G4h#`v@8mhe>+m z7MR)pkXKnVlWp71qlIFo)V|*p4)(8turJs7lOtx}nAbJjy8$^-C=;$=PI!FhY)`L{1I1O9^@_Gd!KZV-NxuVM90l4_%>RL4hEQ@ zD_z1jl(0bQJ0IacR*5ldoyKVIv_%;idurVGo7igikXmlGG_?68u5^>4am7ZAE4!F! zcb<$N>g{NzD7Wu8KAGfiYNzWL0WMmsPINAuC3T0kK~$&>_Dz|_B)u;oZ?idD6i2yy zXwyJnid{p|H0_eUmW8Zag9m!sOkf&)NISu@y2;st)I8#S5#JMYhZ}wxF8Fz(_ z`ER0MH@idYMLke_yB+-RHDHrWF1SwE0Nsg4NaE)Y?+5VPtglX^~ki z=H}h!vr8;Nw^y1GbLH|f{lDq5V(uI!J49k3-VN%j~u$9 zcIg-v#q!|9zfW+ov=D9g@5ks34-q^!F&~5Gp{4zH+`IfTzFVu$&>8BG>~F?2F5m+u z^z(H$uBWZ0*Qna00_ZiBViG4NQTb;EP~5eRsq=k_L;Fmavr|7)?Ryu%<=jOWNfE-w zmOH?z^_)$_&M6qTJ{VM|D}gb60tdt&5pnlXzV^!kPVRdfIyCc8a()?3j#ouV`F?0- zSA)tNCFX<^XP%r>2Q||sFuQ)A;@?u9g6z7SBi`*Rj$5@r<@z;Dd%#DwciO$XI=Dg7+3@VX%P@QQ^g? zSfRt+IVIRlb^k!#U7a7{mQAWtRhbHzVxS+w@M2^Xx{@=vz{&|@-K1dheh~)N+tPv2 ztK?YzYTP(#f!cf_KJAwUNlRf`9{U-}YcCP|moC^{e->H&H0&1EU}nc~{idoRzO3MS z{xQG1gl*7gCM>&*?ssY-ZJs(>3O$FYC&nkd)xn!T`^id|Q<(U!fW-N41he_EV3g}VQG230#|zN?|GWTm z(=7)N?ls-E`Tsun?eOy4dC(!iUEW8NIq#4JjmhC8Mcjh5`=N?k%Q+(1_+?D4iX^*J zJgBmsB#J-tgDYl!!2L0>3eG`b7!Zb6S097Zf4GiV+!6AItKDg5iJ@k~I_^BvO+I=} zh3i(r^zE#pX!T+q^^=t6em`HSu1**h-(Cp!#I%^%=RN7^GXYd~axwPrUqZ;GIwF-> z2;D=f?B;di_(wmJvb)xyywpV2YQH!WCpimGrgFQ7mNAe}Sjbj=nhru&|MB+3oWUZa z>-_h&yHQGwLZFfrqM995;dz0~waL6XV`*|V-JL+&6Ab=*nM!6yQlJ)Cy>$YUJ<}e0 zwyg!vEm}~y@(FpIss_>XZ$sDlWsKsQ+gQ?AkLCj2sMh%rV=S+d^|@Nyb4#0%o$?Hy z=E$P*$^f=pQ-{swt)koc6f=(3;%P4j6xw_QEa%K%PY!RvnAR0YUYbDS+b!6Ad=zel zdxLG?iqiXMg;>u>B_zSZup+mMTvTl&CZ(Gw^pBIgmCtyd<)V1gZZ6DNs{?g^SA8z=TG;qb>3s^uxIb63*v33qGJO6T!7rF~W3F=I+-dngcAPOxrq9G?(1vK=> zVZ!WFXq3!%x1p@uW|ZF2 z0Kc|1l4CA5Q2414WfI1*V%PvwBEvB*SOyjzx(sW#jH1_+0I=C#iOb{X@-oCYQjVN0 zrqrc^v5pnHfOep_kt!Q0nuO`=d7OD;3b>gFvC|^5arVGZ>Z0e#MEkVSBzBRo*xvy`7lYs^ZgYLg=Bi4DGFUV_WVna_k#N>v0u;BR^%>g4&nR>Tid4rifFk zuLA5h0bwXeNyB^LpTIl$JH0uh3LJUs*>fkifq7pg4#+6ON4HLDv$l&f531sx9s|<< z(jCWa18GX$Of36-iF%NQ-29;gTZ2yFx$xC&_GWQ1^F<|D&5`-XyfvX&bQ(%7Od_!0 z4&1#E3i|1yXsk5=C)Vh*-Qs)5IdT@)ED+>OpaoE8m4+D{@ksyqVrnETM>g$Dr;9}z z@U(Xs4c{$?51&_YnQ?%D(=!+)=}LHdNs1X*J`G%U7_zCdZyPu+rn?syhHo@|m z018@*(5yBCpP2~Lrib@g}J#> z*!8BImu97kW4tR6&PPf+8x3AH&} z=eD28P$oT<@mPD4C|%Uxax1rpUaST*Z(9squ{*(S+axCWMmpO1tFSAUUB`Z7ebA{F zEj{LNl|0e^M#E0LgVvo&!a%KDG^%S4`rY zR&sl#=g-h{xCSLeSAw?RWAI!pgs!n>v~vG6UVc`!jqBn z-h)*)e6ez^2PVuiWAo)Cm=UW%o}twsZLrVct66_SHN{r4qu3dt={veQ2#_(8pZIR& zMJmG&B4eAZS#h`yjyKYwN!kHR4?DmV{U})EkVykwqOrS(GstIMhP_kEcn6nI(tU?x zx721*Ih5qc$bYDXpB?Xc{Y_+!b2*r?1GsNDA5YeD-%nWqiU)7upBfipls~@!kr!KW zWLi6&X&H#x{{l)qIhrBO1#XuG^zfk&6mgrzesvEdkG~b-*lrc(u2cxk+vyE&MjXKB zaSTVJ9tU})Vlv^v3+$G8kG4zuK>w!-o)qvVVT=%XPvwE}*&N)!F2ED}ccI8q8@RFT z8rt)gnN8!9CJN;xjtR7riWbzt?jQG8d+M}wppcsT1e*g8LkZO`P`>Xj4m!{*8G z^olB0RMLona%8qsx3&En=k0fkJnlE`@17gz9n9eLqO(5#;)-ifq@R#rRRJfE;^wn(xiAd?cTIflc+>A^*^0xP5XH-hH18 zKFyCXN>>KG+Br@I*L7_dlwlV%RY79m9-IEzMX>k#RcN-D$>mqX+0d|sSY7D}uU9KG z$Lc)Ej1?KszE>aDShi#1ygbeh^M-@5I-r~Ua~vqw2P4sgu<$$ACA*`8rq}P`fyC>0 zp!^=)5itlR<00^B6kyMMG1f9rhM1P#pv%q6K(_7{_Ww|VkM55__dbj1aZljp%vsFi zd)smSiwMw7&L9q3SD>h$Ew1m~!0Iph2ElKh@lsc+K%3DOsCDWF`*q*IynX=t?c5-5 zS2gb_=n-QC*s`zYyj9^f{~jYEbpc=1iWdbd8`e%?iK3 zORxIGU$oMmT|DZAJ}+}=`f_VXm?+PznJ^K3x29uOP&e^QdJg_qlIeV(ov_NLg^nlA zW1ewTaotb_rn*=Fza-xQ^UF)I$!RJ>|77v(52>;m+>FnET7s>t@NtHvf3S<)qI zLEjlO@H2K274DpcOoBURU0wu@HI3wFW;m|46C%^@1eGkl^c{R-j4{G33M8dgLHF`P zc(Xy7`C{gVt7157fjyTC$&g`}CS?$I{Spwd*}?O+ZN`Pml+f^s>F_)#3*9tZ|9 z@*pWPnc4bhHv6NziMr2O1JO1}g%?ld?PzJjr%D>QFn=kyX067zYnH>s?;BtwoSRuk z{lwLoQ_1#yLQK--`@E~X)p+*jAdYO?K}uCj=xOEW+`Hy434aoVV~#^uHoFevPYZ!g zR}PvSd&-k_Tm~J+Ug&b#it!hn0|#>YVK8Etw!X;VkDHW&NRJ^t9y5e>0?PEUNjS{e znvLEv3BbHBqEVid1f=dp59x56XFeZ1+~#0`R}c*iEX0WYy)>z}nZL;NHMXr>4iWLT zkPs)!+#dW(oE>r@u|5+2evX81Hfs2$e=4(o)io;oS_YR&XYeAb)-n>0-eCOQso-+y zJOmzhWM+dk934vpk1jtfd{~LyTu!QL{txJIE&xyMP@vqA=Wb;THJv*RJ-O%jm}w}s z>&{?Ko*KZB`C=saWH|)M4e;KGoy9lx($r}C1=4A=3?5`%KtB;Khpgm7!b&KnEwkoJb36zASIc3+6l>@&xl3#u1BlJaE5yh_fPKC* z0!F_Fg2g;@V9S4@Msz-c*(By;yfakw=#!Aa5K?II8xDDmP%k&`9Dh^}+YfmW|6KyC zCVLJ8O8QAck1iGW48$Xe2gtyuR5-a|2H?>~@?)?EPrwBBYTFkYAh!fkXY6BYSRTb}#U^gX{%Q$o>=ftc?h(cG zrOouNPd<&P6lcZ6e$p9Rc*$6Hw4z_mPHN0I=ZBoECADkAZ6w{J;Oe_>z$hJ9wVcnV!+aF`V9Sio zxQ?}hYcO~19Wat(Y5Gcz`&8@=1xwcAg^cAWK24Vl-FiuyR_uiXC5N!Slk0Tqn$S6p z6WMt>rEs}43N7E>rAc=rpdC4@*5q&~+p`;viEhT;IJ3LebE~0l!A#a{d^#wY2oXWU zMvRC$#O40J(;?Mz42(a6wU-~DzyAeLI-SJ5;qH{gosWa9%9T)ke}L}anhp-e%S-Lz z3dqA~E09pn1+@cR7+z!!E6-141O8kC@w*n7aln*SEI144HcF_R`I#lH*GZ-7YpvryW|@zkdVWI!)L%p$&`8iO%|KaNn-Z< zP_(b$2P2R!T4E~xCuQh|zShO2obClT13PrMIz9vzGTvWSz85>T2 z$C1soq*GKLnO~N?ww_(=zGfLbSpSWBPW1w2`8yotVmWpq)5wO}&EO$F6S}*?F_X(n zq{p8keys)Iw4el*3JbIJl{ZwJ`H81j$>PlsVf^^D7s{>oLZgrktD+K&x*iYsVRM$E z>7rG;mWQq{ZdB1k4P-!43GzgVKB2kHazWd&_?hp5_b^icsIo7fF`~7-7pO42xX(moP z37%FQ1?k1^crQ5~0^b|rlQWHYH9dh?hu^{BZSyckZ8oM-d2E>T1OkgIKW_Oa8-t=&Gpwsl1o>}rOn1!@6pJfF zpD&g$16#Y#Zh04&B930&8`#^W=J&`!UfPL*#a`^KEm zVBvEZS3HUJ&zQsZS1V!Dv^ShtM;nSGRT+zd@i=;58Z)|aBfa`5jGR#)PgIrPqT;i2 zR5gAkKYE`fEwU72mZm=94IaHqqVs;E`^`KObniS41rLE!Nf27Mq@%gaEPi)j0rsV2 zV~*@P;Qu-UyeHypq3;9Iz!zmZpC*9NHA!~$x=e7opoDz&1QHu3&-UIm07;t$685c+ zEDsnD%_;||*&43X`{+MjkwgOSzUhlz1$89*LI_>1r2_DVU##odj9nFtHRdk3gGnPKeTaLDO%elP>DaWcuz@_bRJWJhP(p;>(2~6TwDw4 z&Xbs``4!xLO9`!a&P6f4820 zFw!?N(8E@q`Ka~_%WlWhD~FGujl31dM_Pv)wd=_6tcSdCcU^oxeLg6hR3P@r&#>v* zaWe1ABqscABC_^>1nzhL;h^$Ytb4Q+Y!ruy%Zb}mGK$BKUUU?Tx36I#EgMxNA48YY zG#b0&6)c|^j?xjQasQPwko3lfs@VkbvZ4ab>aNwn7Okhad|4fp-Obt0iLYR&HXKq; z=&;|qt+8ZJH8py1k0=NC;l*rE{3j{IG;3cWIxkass-{2ji0T6nnr4f&<2Iwb8i3!q zJ5+wlA2{|am~@>}$GrM6(l4?bA0HgS$FFP6-iKzuy54cDOUW1dJM=BS9a3cGoK@ru z21Z=Bbda()_m~<9UYJSqhB3uFpZwM;!srfNrhlR)wUYLM$}4AR>0Jwwye$T0J^iq+ z;SC;{mo3l|N~IBhyi&0&yts#!IiP@YLsRv{XxmEf4=d_SlD_#P-dC z{0-e8{D~!#E1!btB3InBbry-8n~dA@Q*pm?5N?+@2fHm^G%2DC^LY}C5}!&SlLRA0zx(SX;I6Udi@XyPcuSw^nr!@c}AyuWr1P9FToKbxb$%DunI z(@W4LYSF^%0AH4AmU#nqt{;eVLJnN{i#Yg7f#vr%LCvG-eAxj6-@CGC7Wa)5xF|rM zdOSRQo`#J(PN3gi6=uhUcr;8lz)o`kaoTg0Cl&369rnJMmS2i8^`AN8K_|yDv7)XA zt3k=&I3(oHXAb-3Lv6)9KSjDZQx zps#L&wpDY@nsaVIa7!p|b!h~n>a(Qj#S!9g(GUx%2wC)JBC}R@1A6B8^Ws;kGdY(y zc44X~b&@=Uj}zuG-R;#N^QnoRJ$DDS&oGcTa}_?%*o_T?7r;8?5}r6#2;&M~pmNj+ zSiUX;k7_=lD_&|-H`!EjS2~9L_gfgow43;mW*TIVe;jBEzEj87d!gSr9o4EH2~IY0 zXQ!Z6)Qj%Gd0JCg>4~dQcPJS`cE*6su@D&0mtc=4EAS+SLP=Vv9Ed#lMr>Mvn;cEqN4b-9OZ3=BZvx{;FT@-P-$V<%lNZ)r33DylI(BQH_bcw1*%}_hGS;T_W zb7yz;P$3xXc#RiYlfh-U8Qg!?z3pP?fENlI(Ih6 zNH`42$Mm7puLe#F$&li8oiOe^L6_wrxE2}&-408!DsDF2d;0<$n-@mE$p67Hkrx7~ zZRX6g8^+kXBpp3&mC{7dZ$xG#$Nn?1h6`dduxhmemZ>D-f{b(spU9_?zS8XD%0}Wb z^(u<&lw(Q)gQ#eMAFilbhklMXh`z>B+B+wVN^O`AD>&|tkA(nNdByQ8v>wp%PpXVe z505!g?~Hj7=V3^9kSe&&LX`u3xU!k+Jo>qj@_nONJ=LD6NQpzoITX6Wmo4i3ZV0CZ z<>dVP3E(Ar4u3}(U~9iRtCg2T|Fx{ZbcO9$KS!A@TxbqG@Be^Xy$tBBe@yuzN7$nW zb)o3tIkKWT1(q7s6WqY%@VJiQzu|NozfJ_+I%{!Vqka@m?8mp~hUt)HFk}RAJPR3l z##U1S>^jzS_6J9Sv7#tp;?<~DN&(1KZ=nrZ)wuBTWn3Up4|Bes$ApFj0#nmm2(t;p zt&3gZijfIQH`}3P?`p7mI|bQF+4x$k)3B1?h7H-46^%@t_E!9W@Ud94E1fzh<*34@}U7%Y~e;sK!aspQ)8b z02CcLNf-5Bf!3-}D0(c8Eh_@x<9SWiiMIfZU8d6#MRTHB9fp>h^14 zfX{~zeD0Kk8%>hX)#yBDR$htQAI@WbO9s&dy^|zO2xz0eFjG1*fL|@rU`$q$4L@#2 zJZ=w?Q)Z%qD?;4=y{i?v7${?d?R4Hojcj9tQ9=(se2RUmy zverKhWy-dIa_TTV+h0v=^%i0NSueDhpn{ibGhs*GJ>q}lAwKkypqC$Xl@?P#g zwDFb4WKk8^{&faAZGVn~llSpP_x#86ZIWP)9Q458eVW8Wy%HlTgm~a;%!EcI(up0{ zA!N>b__D^3Cgc{r^K|jhb{)2R{cNUkcp4jJ_yMe$%TSWN z5<0(6Vq^9zWPhi2O0&U9%m+or{oWSWAlI5)=?3P!xTn2wtn1fNQ_k-41F z(}c_QdNj6@b&i*S*xkT}c1b*)t;Pmcoq=(SdAL!&7_IKT0`aZ8c*zkCd=YUmc5Gh^ zqUH}gdcYaxlGFJ6(^_i!v=^c@9KgVaV;{A2^1^Rt^4>Kykh~f(@XKEVKV7ZZFG+i7 zzhfS*G9MExla8W2L%Z?RRWWwNcM`mlyvKcSYEW&hB6~>gIHXrU0`k)X`)ApJ;{jn@ zF#K6C_0mr&-R6!hGu+_bH!l0!-wQscxI1t5a#odlzUT*flTT;!A>5MVRJW+2%h~y? zPjUtw(f6ggTJxZFvy;kmCK%B>gkX8 zi)RQ8aj6t@`pgMmkuK$?c3% z55S90A*40Vnec-$@JZ=+DpCBGXJswL-dHgiU?52#H&KnP7GDM4XWKBg|1hX6OM^Lg zQ;FinI1D>j2vy%>d7A${0w(h#+4S@>`M6((8EH@8_(X^4pY#H}E5ccnmZw0)diUX))G01$2b7LuyYbghXF$CjZh;2>Cdhnm&|b0)5i4o}UTMFADIms|Hi2 zA%tbSbp^-7F4C;=lI)e`Gr-%T0qfsWD`G zAZG;i)F)1NTEHMD6MYSDf=`t;vo$``^s1LQ%67eieyio|2;T$mTwcMZdR6nlumR+H z?eVAE1ZF#XlAPmq6&W8Ev6s@zIA%>RZvFBc1AL^|+mlUsDQ_rIO_PCvvzx%8vKo9_ z9J##XW=whdhwcbpgKm~5L6MuY+~4>LS&{K<%hl~Twe|>lEPn^2?G*2wS0MyU&Oy00 zU%Z{Pjhb^Nh(il~x%;&|#3 zeP;e2_mYKU07S0@gR+9nj_k|Eg=d~);LA*sc<2?+&dV0vzjD0i+<5%=Dwfxr#O<4| z^wNv>BjEN)z98qkKWjNll$A_gLOLIpP^;eKtkA7V?4g>&xMExr)MqWnq00gEJ2&U) zEy$x8Gk2QRJdfkdKqFvT^_S#oT;}P$EaKg4YX+gjHT3(p4>&iTrMvdqg56(m*&q2m)%e3y#& z&Q1g-==b54X9moRrz%`-e*x}H=8Ps5o~&WQDrR1h7QAOPaos^<@^Hdqv-b(cSZB@c z*fK=fd9EF(oK-=q+jT&7=L#^&SqAHOWuVZPKzM%80-N$U+fwFUC=|PdG>gkES?Xiq z_&_|q=@e>OUBrVs6d-B-FfEmGfqAF4F{#(T!MI74`03?sYUTNpr=m87TKYWdSYi#^ zjpYO_9Wo@lClv+~L_k`?hG<^?Lp3%BlX{*!_P*r$>?h~LmikcmcjgA#uX;^Z%W5&l zE=NF$;4XxYOQ&rz?&$bChMaM|gof}Hq$i6ov90FVyFLPY%aX~bkTlSfN&~YKxfsr0 zz_$G8043)W0^1*!r1E7rskK(b&evm{A@>GZv1~H7tdwWtWVXU(k3`(}`!qaj%)r8I zK4}XugE4P&y7s0EmS0~A23&t{ZT}29w@ZM#+}49r)HYNU>csNKA?kD@02qESL+9u`kR%P2-)WBWNubITakX73tz-`3anDGUR8)i_$u#Ky9*%X>MtDwV zFQK!xJ@~tb!3rF{Tqd#{qr*>C2!kd2j2V9E2#w=N9Pj+`r5N$;MoAK)~Esf*Y{{;c0Bkx$zjj?P-=4d1IWMBWW_=&>HEP{ z>h2nX$7kAcwr2*?>`U;|)r;^m?+@yDM8ni){}G4F0jMkHkAaSb&{L+({hfA_f_3*e zTk8VqwOv5nSJt7BiZ3pI84dCe7O~E&CW5c04vkE%KzET*?BQ(hn`4v7tE8uBr!xu@ zZ0oU9f`a6fd(`%~0a$)d#D@ctC=>c0tShMjy^22acX9wMt_uLu{gY^J-1GGjYfPHAro_ARTviCWjl5&+G=aV}g&EtX4)f+U>+lO^Joq~=W zLqaOOi%PB#M%&~fe3kVY1C}lYi#-EWCt(WO*qbt$r`|$S&I8=z@Rnpqou;Rn!tllx z1<2p}5hcIRfiRa=uunNfWgA~$q~3TSPCGb8bPi@ICG+`N{+Q-5iA;aR86BG>AoI{# zvxjb@kWh~=fO~_>ptpZI zuDPtqewr9WVxBe7e-go52ThhOQ_%%8;eM)Ea1ZsTh%qWgd=#-=jygxLL!sL%I`+ty zsN{K1YaN7=vhyq^&JVER8a*0w^qlj)$No`J<`u~h!fJJ_|ufprgd23Kq+gB26; zbMRM((=o*L z=O=)7s4N5=-Nk?D(ocoIY`}g&2Wr^d$EintnlT@_1AV-TL1QVWr!m479$IY>50(lD1!9qrbUG`T5>i*rpr9K|ibx{kp=B0tfF&U_R_mG|t7G*12 zZh+?MMX;&rGC#w5JafYQ3)wfZ1uGdbCelj_R0j>P_sL|;@7)gTR;*?&8~fwC4Ldkq zRwa#4;PPMJI1bG7AhcimhPaOQz&uSg9Nl9Bfk7*g&OeVExc1EgMi*}{y9WMCEl{K6 zBv>!a1mgwepqLOwH=Gnv*7#$A_7I8hx=X~^2=R^L+DTw_uVXc@oBh2Nb*e60zv2HrfeKkZUFF%jHw$nkW zrAuIWb28iO7K*`Bf+3valP(z-L$1tN!JJ-lokVwuGcPZ{q6hc=l^NzaR_}zWoXBo7--!p$#wm`FkSmE zdan1y5rezLy;_ek@A1a>Kf~x_2PxE0FlGjtqNrVbHW9BB(5M;fsPM-JSYlvI|FMTi z|1lploDcwgqtf_qXDb9;Y=m#Sl(4bF8Rzxdfs0=}WR#9UbYUD_Wf96x?@0&0&KKNn zt`1_jt$l6Tw|uRqkCNg2{<;>@h1jJhaq;@{J2ozo?0)wcHB2b8E=GEuENo zMSv?Vx6wG~!(^+|3Y>N;8rNUB1fOnvz<*Ik$cvg}s;(W4(dzZoFT;StukM5MEh?Dz z_YSIc2av*V7jThBB$e-y;9m*Yf{{11(KLAh_QeW;llu-THY*;xtg3L!$0hWso+rjA zjiQ?CAaRm2MCZqxfp>}pxVQJxqN7H5>1z-~z0<_H8^_?6rx1Jb$9xFe8i5Pe^5A~- z8ycS#k8xKoW2wme)@;_xy@vbvE1)4t(C5}Jw;X28sK??8UNddXyCwiAS56z0_ z&~0~)jN{!QZI|QV?vt5hM*ADA;r9zvGFnk)<_5A=+zH;Lcab}JTwl5UA31O|7UrMH zfLIk}x`T7~$cbs7&HeM>_%a#HYFlwyQ3UOZ?4v%*ZHX@7o_muuX?odx9!3KFtdNN3 z%Fl4KPg%w?=m_lUNv2hY9)WS}QcUcS0Et8a9U7+8AKqpHU1A!v1hcwnJctA|}FY(=k14UMRyJA1lV$KRDaDLm4jJS_B97Niyv% zL!_$R8ty5?67$>1aR2-hfnwuv&}rnlIQ}PbVtyQoJS(87D*b35bO_ho*JaN?Ure{( zs^b+&)@wh($mIcE67&($?pJ?cP%Hyzw=TW!|H@&s$}2Wv&SrS`w9{swbaAfe#G(nz{w#z%l^e9`XE5a1-380HyK&>MJ1EiO zCD>J$3@t+&S;5_Se3!5oekSqQ+UJ4v*>Wu`y_0|qMdEB)uqBQq$`AviGU~JA2$8H` z0;(UDku=+EGh@?cV)@<_YO8^rV{#XV&bV@Y0S&G%@W1?m|G&P#|0NjIC@4zccTSUM zjlRV|+i(bc$`fO96OYlRoj%ZTa0l^?5>vkwUxl}T04Pm+H5BM-7FerbV$&qyb6}sCy)sN6WQlJx}?zO zG-|${LCG-*R%*gyG_)v%QJ>pXUiT|x-FksbX4XQ#SuhouHJc4^b)^G~WvR&lC2-Vx ziEHx3P}yG!p1hagZM{)KlG_zno1Lw6}1=W*@l!l43rcOvCEbYxt}p3Nm$a=&5PRw8Tmj4lItq#y!gH z__hSp6ss0EH4f7KX7a4AUl~;Ey~Y;zdx9(6{`ANrdDNfPfMh}h-kHpR)g5X4P#6LW z8h1fU?ME}0&JwH+{UFd^y$LGKZyWmlkcak+e?d`?BbHdDn-IpQSH;!Wr z=rJY(T#w)@z~)l|+U1ZBMV2mP#@I#-3r|4pP#0))-U|cid$@d)1k-x`BcY2^z^_P) z?4HT>I^Ro^5t@&V*Pr0+<JMp`l0W&+up82wh`s6R_Bj4tr_3JN@P}^qya1E;&S(9S)1V}@geS=; zFeoU*S_RkxhEPSkcmqQ?a0ad9_h&TKMkNnZ$s zwSI&S{Gx37C~y7Qg)HVjq-9go*|Mw3FluMSySHu$*4H=E^DSber?H32ZaspY;VO8z zZV!Fyd+a{(Kkcn|~-dvlt~Tb=if- zWxyz@kIppdA+g@Up!+tAN(Y;xp8jLOgnv7rz(9zmMF_Bqo1uJboQ5SSN!YkP6XexT zp~JsDyz_$b?48p0g1me2;IKNI+Ve%2XExU%=W8*F{2jyDhx=%j)+|=~wHw+z(V@eZ zweV&AF}Tw$%X;Zw2LqpBGO&R=H=a0wxh~xKCaH*&sTBfqa~cg6tLBYv%^|KDpJ0?a zgZT76B>aXe^qN1yp;hP5M(H5=GJ22qF3=yo%;55lkD`}so)hz`E8r9= z#b|GN%k2$LlfFZhAjfr#PgL9D&dcF=J9;6U&N_omKZKc1WjnYenuD+IiO{1<=fX0M zKixIW5VyX3gD-QgqxFY4@?`h}FQ3tlRCE=&W}bT{xB$|b4_X|&qv6)j$|7~MB*!-`sO z(|s3^>qv#+%>@x~u=qXEn}3@uU4IZdU5~@$zpK#w$w@5k_=3My7J&EQ5X5@QLgwZq z-T=3Qa1O2@3?0YZxg3wd?P92Z%MQ3WJ9~VZA(J+;kU14_8J+Hbr1yUQq9)Q8AywuP z@j5;cns>z$>#-W7ympvtKZnQV1qFWOA($6VVa=AzCesbtiPaFt)eMe7F&nP)y7nx- z@d<)pZv5SNWIIN^3xOg20!B@&lQQA@ETi(AmReOr zRKjR3)N#FyBhqbPs_#XO?zrN;T3zCFw@>g(Di&{S=RV(JihGvaBRdqioZ_=SI_C&D43|YuFywkD0=zUOjGoJ7xh!%itv;Gd z=DfLWGUlyAtk?IU{rv@|PTz9LxGh&;x>zvy-{sIh-?(%B;x}+mQvl-(&%>8LB81p; z_a(!nq$(r^>Lz-joXmD;=-W&7?{I~!%@riB(iR8CJpz7S29&Jwqpp9gz)5`Z%}3bBJgn7$+)?5U90*3!LQL^a^F0c)ZbIXj>va75bz$%LoN&Mh2If` zT(K1pvy)7E&1q&q)>}NC_ndl3B!PwQC=E7}fj#egVNbwfqE|UadqmSKoc|QzZ;fS) zW58QFsizh0%%}k8^OoG)Wg}QAT_p#b#j(LS1nxiFNG8gKV|cGWoqKK?GtYp_;^(L_ zUZer7xO}7Go5iSY{g`L7RgC@Oo)6hPNwy0Tpqw8HUBnpemB)xt*h#YGMk9W&*I`xj zGHLN40&ea?Ov(mRRH;=akvTujwE$djDVo_$2$zoh5 zkxp~+@@df;7OR)Gle{axAkvA;e0yZW_L?s0r{cvZ?)Sy^359fx_jArJ_PW~@(vYGoQI#|*FfUYO*nbnF}PmP4+FWINpkHvd^o~&gi{QXjF~+>Eh3~iR7uVhHreWc&s3aMJ1^G|$wQ3l;cD=-8 z`DPdm`A01aCPP_o0WIge9|_V1X7R&02q#jvIpPsp`nM+=KunDTt}IoTdD940U1h7M&zd;ChSJBS0Va(d^S<^DzAPN0_&W+dqnm;t#!G;;$fwXUTrT{^jm&2lvD8 zFFZKAF$?4AG>ltmib8=?F?;U@EI%LtjytA6Pv;DL#-EIqbrc2*r!XDyU-|2IZ>P(Y zM)0}ZeG*%{0nxW#aBqDTuHUE3{yGvur+?7H>taW7W1BSO-BzK3US$}xU&O50Pz_J& z^N{8Gz#*UKu$P)U%^uo}V9-_xtY|bB$V^;LpE+K~U2`3&XIU-ol9l9gz1nE3SjTVu z*#SEy)=_Wug?K3|5*p35@ZHu|aK?8AyQX9+o=w@x-~6EqBfP&;`fv`4oVEuOyHN1j zmP4lg?#0%Pm4dCai^>+1e8`!d zU7L+swiDRGnP>2|AP;v*pM$|&x9G+C=@>Zh7*+dj6Q|{-Bp~uV?{#M`YL{DL-JeO= zyebmMu@Vp%nTA3S9z*KwAy~O9O|VMr6iu^OjfoK!Xy_dVpYYoNT$7~;ES$q1`Q57t-F->-9tR*Nvz3;`xb zOM{8AIf(P#tb;obYp}2J6l46fo8AW_WPcYxu&9_oP38@RanF|MgIma5MJYzN=@8BG z;@DKCqTn7~MxR)HpG;zzJH*f0G8Ed(DiP%PR>xMF0Zk~aYvy}yH<9EZxRar3hWG)12#9_|r zGMwFdkp3Nw#wNYX^x2x_l>VBA)1Mi!`TA0*ED{GdFL!|IoqSy3*i7oRqTpWoD(179 z4jLb4Ve*SIaDBEDt-AtY*ECo9R(KjI-&w^wmZrqq=}iKUEpA|*xEt=z4xs-wIiN%+ z1KpE$<0p>8TCrJ~eR_W{rcSlPR*6!&%y*dTmG{#x%e+zP;t4pV^+TYuO_P$sNxUw# zT<|r20;3=Qqm#xma5&!t7xxI$)aUy^a^YK4o-~4PPBPHKUj*w{EaP?R2T&jRIncY0 zVI5BR;xaKs)=8uYDsR80S)L4i>oOjr8+XI5COy_9Qk2#B9D;uzsJkQuk^USv?I+$koC@OWucN1LBUEb{(c0bb=xLElq+3fF zww%3166;=&g4Vknr}rCADBBvdzU_p&Mq+Haa5aVulwpW^3iOG}GBc`v;gWOH;NRZ> zs9hXK{=LhBwi#)pcIrnst@D`{q=j~bp zVrd5Wd)qsDyNoc?gSdH<(k}G&rg$p(GrU+DjZ)3_I3wpT^0)9{EI1T@-i;#H85S(2 z2SH|83YF!!VLn~TFmYEoJia#(W?Ek*Hj9Q){!SE8ohr$G+q8v$>_{Fs?X2Pbx)g(Q zIwsI|Jr@H1d_=#CkwjkR9{+af6lPDe0X;Azg1P@$P{%#rp?R7Lc3Llh{(`Ml&J(RlR;kK2dPg;eLO*% zy~AZBlvrQI#c=PeBnsMInIddO%rlYX+FE)&u)5JLJX8{9E4Mj zw-7G$nl@(RrHAk$iwY^9ddgZso_nn#PPj76$&o^>euLh=cBkhz%N)**bO5u zMzF(w7>dk{iMeAQp!QpmJ~xWQ3JyRAcjmFVszFi>XX8-eGJ5p-R`!(DKJKih3t|xC`aXt0go5{~!y$GHUdO)YC8hqpU=^OiHiTYY0=Kie` zoM)d%y|4CDOS36BP-VvV`0q4nQKW*DcS>xY+G5zTKo2L3#^Eu46}J0|JoEF@G}_=Z z1KiFG^V%L7;+4a|>TKlt`YF2j`l10=?z{mP?aSfQ#w(C5VhitFw&KY7;cglN4KoDwzWc(uVOHIzr~T{*v>)g&|@FU@9J?Iw-mWSF^@qu|^-OoU%l z(avvKyj2&*L-$Kb6rr)8vFnH63zv0>w@HSE#0Su!TmgRlXF$c~Ih;#)hNd?;2Kn~+ zaLPIeH>eMW=U9wV1sJoWk$8)I!OaO5VcrCFQsB9O$qD^Lv`S`@ zLrW`p8xt~lO=wS-dema|;ucuHq?V@Cl)&v}wpcl15C=Nli1~kCA;DP>C%UWwTW3|! z`WPwLDyPJ}YI{h3cb&%7cMPz~QXQ?r^Pt>vA>)7TGOZ3B#B%{nL~Ua?eYN@{zAv>x zpYA^5esCPqq1jB+4Gv-4Qw7Fj>p>`2mZimUqV&*C6|!O>p&8HCL(R@X?DSYgMm??3 zfA9uwEYd+$qfgL%cp5Zo9K&SaIx_EDtJ%Rj+i>fi1yra{1rpw`A<^C^z(p>KTHABX zsXjOMj)WRM+mM1Eo5!Q^flXY*bv!F_eKN{lp2c?DRmT&uO+4W(WiYKtiQOwc87Ia| zGFI(-@XPb{Jgcfbq;0kmdDV3XM(-}eklAjaJm89TZCRMX>!)L{X0mg(tsvvfMOj5~ zW>Q0SsqWi<@GmP!aC~6`oH3N4y*>FPJ1+_L-TXiTYEAw(dogPJa!5pPa;aMwPj4^9 z_HiD-54(&q?Wb@>sv)}XuOlwTZRjINg3yS`tZwpUh}u|yZac2iB=-=)hhWqax4=2p zQy4u-IhdjlO&e|~v9hb0=oDY8*-T5#FTe@m8YoN3BnO)) z#8& zN)wDiGay9sIp%KYhq&M$oOg-qK6c7N$|)OsZ?Fp+2Bbi7Lov@fBA;A%>ksdx?}O90 zHu_2RHCPQlK+Pc$#&4WIQ3(zM>Af$^s?F@cTUG|QTbt4K*)ml2bP1ZCwnv#aM{rop znbiz;!C5QCnQ>-oI5+oz*;CCEtoXcQQj)v^68Ur39cOo7Y)Uv3pLV2C%U+_`=^*gl zvkuvnoX2*`8({P`!FHM^yRKwB8@EH2okT(*aa0TKcuN?K2yFJmuV(FQEB@1zq!~iax0PMXJ=|@Za}2=(^_!6ttPrIgCGl$)6?&k8Fm< z4FmM0<^<-7@HCihJ%;J;xbLFzdJx&vKsKr<0`|xo@a0%D4pkLG7CRh=-`2UTFjR#gq7!d zZwb2=;gvZbO@D<4V)vVCf~-f^$hU{ihMq<9Z3l?W^lUuVBEfn!NwY;-)vz>) z2cz=2V7Mrb_DCDDpj-}aHldI+TtkbjkI)&%1Q7G&I3$|h1&vL$f>OnQG{sn(*t&Vr z9O)hubVuQn*b(sEs*bE0=iSz`)eX8jQO&oP?VzF9-H&%FVE z8_w~ZpTKpjlkxSLS(xRz8<)P`07GxMj^~>^6f18L7*?!hW4;$B_U*eegjfmNB4c6GrwOY$WyejdWO5g(h1&;tbuDX!;@^5Bw2^iyz%Vnmb#H zj2f^LcW_L!uoOJ`c?SD&DVIAEGi8tTzU124#h9FyL<1tzV0iywEIiJK`KhYxjJitL zp}ZGM3v1CmmUC#gccI6QG)PJlhLNY7Bd3A~0ZXi)&*&ZXt^G+~bsojaVM%7y&1`hm zX@SbQWB8Bjs@BU+z%7rXz-R9pdayg6j_fi;%cpU8eNPQMI~hvGZ;)WCmgm6zGxMNd zDH^_?)W-t~66}#+QD$Dr47TEF13arQ=J~lF;JP*M>64qP>_feqaL;EVJN!}_&ILwe zfvhl9{gf`KyJ!ZhD$_Z2-(-=l?h#f?g~yJH4A6;qEow`+Q)DHd3-viE-k*T5qQ- z;EO6*GImm(cXW$29!D=w-IzGTY&ppJ!I+5F{OQJ)X9O{I{V);Hj7^_i% z&I=%zdiXCrB(4B^_TRwuY!DGos0OR_UzlZ_gTI~CaO!d;M&2a|I}60{v70!&&2503 zyhp@B^e)aA-e^{nAPOBqs?2?fdc5;F6TR4*Fsm?{<87Eg-)$l0N$XP7|7Z!Fj3lc1 ztb$_cXCx2Lf_9Pwgo!61Oc+PYdX(@3t@P!#gqD)JW0q>mFb^NB`g{PWjKqZKU z%}J&B{?T(>+n|7heZv^UaYiONzDJwi<)F;g!p%2H82ju7SS?g!!k_rT%fH{S;<+)Z z^P<3Yya+Q8p3NOTrm^iW{)eqIji>5+|F>DlOp>8wN~t7;v+ms-X+VZ3R5U0Gq0op7 zAwwu+R*@kj!dds01{##+Q0Wt)G*D?!hJO3|@c(-KpL^n*eb!!U-S78xeSplbUw}cb zQysAtj5mD2IXC~1nz3Xkn$<@N7o`)0CRx1s>knxz7>Cy(Qh1$hh7LI!5N(;mv~T6H z{~|eW<2hC4N5*4NEi~bS-DPvlmZP|NW-Hb{O5h1ec0=v01mf^Fo@}?c16)RwEz;t_ zr3cYa5nf9^7i_0S%T!67lqbeb)q~NC7K~@XIB({+3vk6zkUcym&00uBqGH?*NF9>E zyMmmYZ;;y@p zXC;Ae>NqCOQVqtE^GOZ|KjWEsK1PF1M^xlG^sYVNh;ITEKcksNrTkLwOjgV`I*K=+mg#GYS^#`B-?#%9SgN~D;w zm6;g(zycBv#zOp2ebkq0L2qA$y$^Mok85Gx`Z z7o_3nrxO6yH*x0ROjajejk&BN10rg-c&<8E0S?AvLHB7GU&?jH{14*M-R*RWz(g3Z ze@I>rRik>>LR@iT64mTIR9EPcgo39pkOV1f^itS}q5@6ihVFZkZI%RX!W+0df?$4v z%0eV9(co`;98Z7h;hbs3VD$1n&Q-Al0b3Iiz`sdC?iZ0l?(VBQ=^wvdVLQerByt|t zlc-?9xqM?A$sD`?h#y?UgM7|~cC(&V%qivDH$EV*;!cCRWhwuaxiTa@oInyk?u3rs zZ1b4zL26G;fJ9gax`k-`#j+9`PvjyWk2w;iIa$+iz@ z{%kJUTr$Y{tF#ES<}y_Oai>KCHsB(jL0UXE@tZuQpoH&2Ml1#Rg=-Ij-nMu$;IE5& z&*y;8nsPMVe+OnOQeO9jxA3&)CYR-(&CHi)U~9=DGR&EG9&#=-jU7vYO>lyJ{wnP6 z6}Pa(b{qA_3FNKOd~)GqAguOV2Zbj)==6#m=D)owxV}yS{Jy2ZuHE|z!|up4%szb( zUR!|1wNn^R%^heJk&h0;S$Mwo9>>+Q1X-cmWUxPqwC-8N>#2i$O==8gz)&!(Ll63|S_{d+?zMp2ptB(jgT%+z^Z@nGES};ep8D zS#aw-&x`WSA@gGct0%>Ub|nxVVC(JdvHFZp7aRDLM5sBqT?iWz=7waEC4@W zECrWM=Wr-t0h9eBlqAtk`u#>C)}7r%o=-T%`($wzZQPP^RO~vqeQ@OO_t_7-=~G-F z%+2i=93rQE+QIfzJygGa0hxZHkY|<-^G-|SbNe?a@j(){-p_)NC4_2OexpJb;Y31u z6$<{yM0_ayHL^6#gu*;49)PLKj&h*VBV0BpxCf!aamkcKIleER?R69|AnwA;s z?D3OMS{4I>TC&8bS#P&$FgAaI z-O`2TCj&RotSD!+d0tHqkN-y`f@h(Zu^fGr>qe5euDw(E3HZjcQ<&MN57Ucz#ZAbh=4AIq;iB?6CHX`f;Wwq%^cvB45} zogQ32{&GEDXpVu!_b0F-nmJ_0;~C6v?rgt&{RvFmnn&~Wq*$X}BiLgs!<^HCL1 zn4Z-S`uqLyR_IB3L;D7J@dvSgsU&`T5(|kjx%kkQVpTgOYi8<`k1HiGGBc9=De8cw z!Ifae?c1_67DE4RM^fdTKtiX^W`Bk(V~amOz)JUF$n4jJmVgJeRMneC->AiBp*mRd zLz=ZbSxe7PK2P=B!{K@VVcr_e9*()dko=t-uS?=Mzw_Hd%t+9oec$xKqPq_p>g>7M z>_b@ALtxNl01=s9yTMs4^k_9IrtM! z*=161n`C(V=`Pm3n#iUPl|cLqE8@#_!LBVh2|J(vh2)ph7<%^(3fAopH#Jr$RzK=Dj`!SH!eY~!dfu7 zeFP1@Xk+nSG4eKQ50(#pp#5Kq!S)UXTgh~orsPf^?RLk;7Bf~bMT2HTFISZmq`}`M zS+dX*nZ>6d`m{akKFtGcKixt1+KtdIXTk(E-393%*GTlL*U%y~0R{JRT=4~h%uk1A zzGIytll=QW4OsdV9+~N3gXc*|f1!cb4?d#C!uja#+DAmqgfV+_IjY^dghR@U%#Y|T zA)m7M04e5i{@gT@x99-pi_PIB2TJ41=6L*X?kN~iSj<|ki^RtvTvsNTW18ya;zo`o zRlIJ5I;@dF<6q*`X488ZI3>;8d~^_ppYmAaP0rA9eJYLEPx&<}rwLZAW~*GIsAzF3 z#^(KnmU)8Me=-#;UbN#EbB>Di`xEATxJYEyyrjvS^P!WtLyXTn_@QYCM|Q@ej^aWZ zH?zf(pf zshK!G_&e40{zmnwArT%1G6f=$W zs!G8!<*m5m&s^3~E){&d74fsWJZpIDGm6gcqaw!%-nwRomM)EOb)O_DJD`YhK}R9* z;4Vm=?f}V8v8PQIx6*ZXf2@A{1{WrIlkTyf^euXj8l zF$SJ~pTMTxO@gitNjRdm9KHG-$?q?-Yj4eJ04wc0=&4g?y%=wNmOO!+QkcL-)Y_wD zcMkf$S7X!uaC;v$ZYFb8AI_)Frw=Rb(SO!=^n4aW<5D?h!GXW15PA+r++Kodl^s35 zLXT-s&*Gd{H~FF0A~1N94h$Z?PZS4#;6k}PFi9$>6W6zq+RGd9$^GT5z_MgeY0%^L zU7^gAwNYFjNE_S^u7GCOOK`_d8}4rXh|`B2>Ngh8SY$#2aCdB^8-hG>a~{R;Lan8PY1x# zWi_}p{-Cynk0}Y$=Ih5;khWxH)^o2gYY?TyIrXlS;+zHC-wUzEu8Tm=H3ch2WbkP6 zWzfyv%MLnvlI4FjAd=B#?%HU8on^MvD9jJVCi~c>iqo@oD6&l=CZdT=*L+Ye)SZ*HKyY2FDbY= zWfW#ahVdO}3qHAU482sR!P@E1h^NUtd7WU5c`a(7IyMp3g|0;PNFDrCr^$C;S54L5FT>&! zHe{AVH!Kn5%mT{W*s9$Rsn(buJ?5W^W1oAlajOJb);<+Nef8L!b(`?U-E3mdd2tC1 z1w;5;Uw-!`880_xNcmafT=|A}Bm+lS%>+Y0f|OcLM51c*0KJJoZMg z9{XvpE{G1Qv)>*?qDq7%&x|}k(a%Hef=>`=KLf&umh5Z}2_t2ti zn3yk!zhgtOFJnHx_c7;84XuWTlx?6qbPatXcH_s0EBt|5`Is&5$!v^o1dgNvUPOj$ zGScEn6;#k?p51gqk_glbh_IFFI&6$sH9EbzZf^YSDkS*}u_te>Ba^oTVVTz$Q4;5) zEsurC?wHZrk+@@>@NwFgjwpnu7xYOV~&6M0#Iz^Xq zJwVf?gJ4%$k6#+p1STRv!2k>;QKCeQJE= zI?P_&fT~uXs19!{1ZgL8`}GaTjFwR6f#3ANl1tR;)OLENQv(VH)8UTT8D7!YMl>7` z!tYr=s1>6^WM*e$`gMkyRvO^FZGmKK?Hx$d*=y#SJ0IV&oBb&r|Nt zeXm>AVvLaajcOnEE8qijK0&8RVO$noi|S3YsM(WC6OU&N4Rd#=S`^IcN>{mU+dq5&nE^wlF&SN zoa;#XLg9)UQ1i`%j62C-@!S>{84z|((^O{v{`IKX4#=>*wSPnfL2sHEbVMOWi*AH~ zoXwav;K7}v=b~En0A$KJg7Ml1ME{ovDD7AU3e^)BZ+;@z|C7Ku{kgEycR38e1CnFZ zMP|*&L)nRq)K%{_T3K6=;TJx*M*BKujyt2*%`BRA^$JbCA;Y}VJb)d|<501m%T1o- z*cd(y^(+mO9=Fh`+!QX41TZRx)D>9>5+0#Q0TbEH=G^|X^v0Lf9%}>OLCU6 zezVS@$CplYi`|8fTx02z>*e&Bva17S4uTPt@?k!3<2ttCfa&wv6aYaDEv!X8;D z4#{O1C>qs|Ns=qs+V;>fjbN3EY zFrWKL{=PLEn`7KSwPgU4KEz??q?h#Fq0`vby%*H?^(DVLm&4Q3{4C<^uE59jBeRfj{&%(nviYs5l~muCgAS zLw6}0S?UY5%f|UgJ@e6^lwz!?Jl2g~q#Z45!7bqiO`r z`xlibJK^)4Zd8e`Mw>UeJT^j}bzR7@xZ>R~hsmRvEBolk`xN+oG@p#|({SMGDn@XQ z6t#L{hprC7%mvGrWbfH4_~_q7vfAx6c9&Jdd9ehdvR zwXE895hh$D7M}dML^^~IgP~YD)E*YZ-Y;B!HHc%GU3!Ks2f6e1N-Ojnoz29l#9++U z6-*A78EN9ar$sBXQ2DeXJLvoe`_A3xcv9VHrFw!aEu6t|6=crxK1vAy#l`px)-ui`m2U=^3kzFSR-?-jx_vsC`?t{f-} zaAwJLAm7hUC#Ft+Fz}QDIP#2{c9*L#)_EEe3RuYE)-x#+;O(#3fo=NxK;_0vHY;-s zdR10JjnZxy&0d0bWlGGYJ7HLo?uo0TS^OQEi?^*p$w@yk*4IiN{MBus@b`U8OV7av z$U`L$DYjSFmU?fVi&9%mFm7lFswI_3*{Cg6xz9#B&Y{q&HVdLW&6q0DdT6bcLz~?) z?08E$de8rei(cq5h4aLr@Q@NMzk3B;^5ekZm0!JW2!q+P__Vj99-D3U^E4*vutjz+ z@$8-)A~vASl$T|YP~iZSU6O!y)-ACA_dM9I`iXRYy+t2>OMp=BJ@a+4GP`}-W84rl z#oXuqU+SyOt-a@hku4JgJDU-FaQ7m0UxMHmC(LY|kWGu9R}-7ASjxYb2}V})Se19r zk;yCrOASRBc`JrOJ>N0ukO5@HU*)HZ{Rc*mG@&Zp3tV%H&5tF;;QP)q91~R;p9GC# zwLaH3oq8L`yp-93_*}4j@&q%z%82v8b6E3$^Ukf91tMcA%oYPtwq3!VwY@07boaSn zhom+JZZ-pdFAbEcxQto@S$IuPftjo5i%h^e?3^LXeq4DHFI{Q?-JPFcqw#V^QmPAe z=4a5OPui&N!yRCuKc89fY9cW-7iId&x5DI?HB>0(ZT*#}O04LS65K>M4&BmPvRWm8 zz7#o2j>enQTNcXfMwUlr57&dnzzZb*i%D|-n~g^Tnl001mfw+pDL(|+c$$oYee3DE zRT8jNBL>C>^|8(8CKhvNC@r6@u%MNDem5DAH{BZbE}k>wDPGs>C_qF^9;2h^GqEd(g@l6JAxmp+|2*gCJ!-w z)7aD(JBehw4-Tv^0kM`^BHA5E&pnF9j>aKkvEeq(%H04D%lhH;kT-Ej3gI_7O=f+A z_= zr?8nHuYf~nDu(Tw&72engu6eNGab&|{D4aoMgQ19zaw`e<64XXnp5$}XC)^2tqhYG z*@T>HgO<*?LA`Ir(bXp}K<3)BXl3DoR+0sBZc`j}{NYC$wbJSLL!8rA?F{J5+(@E#D6`fp z&ZGC)ne37!M)-5jUY?hS3$^$*pC7Ar6KlDylK5i_{%IoyM6V`M7x6jJrZN+LmQ7>+ z{KwG5<+iZn)N&9}zK$kq4e*DC8njCsGxuA04-`c>2N(Bw-c#I(c2DPVPN8H`_Z+QM+E{^t_u!WlWq$t3X-)YtY!E5U2k{$Bw zd~V;}7QYWfmUHf}xy|)6XWjt+9a*61*aB+}KJoQyx@kc4a^{$I4u<@`NS_Wm;_8+v z6pzZJ{sqUeBmD~TF;T_{y-cpN_6e;|PKVQ*^YQEYLi{n{fNh2YsLRN+EgyuLn)wB= zqBaYLvdu8c@FmZB(^}B^$GwMJ?eV%tCC7X_M&5AU%FPwF;Hl6+{dDfr{kuiMcTIo2 zmYEx#|CE8*AG+(-Y<+{yLj-3!YO>2Gp9P2BotXXN6iO@9n|D~Lpg>m$I81m3OQuP4 zp4iPaU3U}fS0ex~e#X=30w=*@ZY;0=z&4`i)q*j;Uiiy>4hf&S#VpWXj5z;kz|PgW za8bJ2Nz0V#?*LBc00V(CMD*6`kDxRBn3PWjSc zI&(MLj?)v3IQBX)9(tkym1eDXDD zOC;0KgjIN9-#nhI^Eo_MJ{1yDbxGy%D-aaK-6L&OVP@&{;K?tKp`7_Z_wCAq<}Z_& zC6|S;MXZ5Z%cj8{>oX|S#m9|Y{?|pWmjoSfhTyXrSZ+4gyyyNZrdZm5N$uvtPGdD( zQZx(3POHK2NDX(tz%h$Jh|vfRC!=NaNr;^RGH$0)>2M`<#a!d~Ou0BNoX)FREWz}m z7?Yz|fS&WY?)Cg=Q2iN(f)*jXGlem*<2c9Kycmiq%Br~Ty*U+FdJ=3;oS;y12_`mw z1PHx_U5o-|B~4~u%u~bTHNnU@HDP8AoyYrttyDowHMr+IsIam}m{ZZ5A!s>G+FzPcmVMZHI(bALl`lwSz2ybz&R~|r z*VA+7(_y@J1Aid>4$aVxMe);Mq4&TG_$*CKsaSD?UDV^~Zq~ zb7^lK#{#yyhGToea71Vcv-XDo?XjLg7d>yqF*<5*q~XTOG|;ae6Xf+se|iIrvRHs_Z@Bq^b|PtM9w${XQjnm3kX-eR z#{QDmbi=+F@cMq7@As2Mn?GT2K~EPvuIaF!rb%Gs?GGrlF`sT+CV?H5l8lOE0@a+g z17o#pVT8~5vzAD+<`Y}V{|oMt=^=r-4^E9#6j|fNch@z1u7rx1nuof z{O0i&V6!9)o}aqPZ*ETqe}y1)`bhDaE$8q z!@1N|*iS7s;G^)FCO)3Ut}AaOZYap+wH5J_V|HPRyCJIj%P==vC!n)oEq|#^6|^4M ziggduiM>(;{GMro*NPX^_C-mV86ZU(C1v+*u z#Y;=3Fb2aKkZ%5*_vn%fy*-P^ra3ZnSbHvbzWl`7`CT4Vnniiz50q(?!WYsuY)5Rp zGU4a_bU1yq6QsUzoz3BD3_P)i6Hs&B^;5yXURMMowR0G-=Pgxs;2bQBCL?@JmU**T z51TeS;^Io~jCJM`juhG9j-h`j_00+4p%q5-{fEq=mAEtEIzG@uY`wRa-biae8FL4Q zmE)mx^C|RlXoSK0AJAQ4G1$Qw)H;v=W35?a_nJER{ke@A@KqU;M}zR0n?wA(z87Ry z4}xCOQuKGuhuvIe4@~`eUrgoLFRd%kWa3k*A-SBFD9fFJgZDv9RXN<&sDRt6e#48? znjn6o9&SyT$dKp#QP*)vl1;M9g*`cM$shAwq*X8*tGo;ucIgCIz2YGCE91P| znu# zmZUB{oS(XQGR*RIggJB{G6&`{3hAj(biWz9FKaPE>JEy{Tl}b2IS+@kvY_#Z07@iS(cSx&GjqGD`J($*LU7Y7p4B{U zJT#Y^{YIM;R!yD>wAH8L>;eKxTt{a03HoZ%TDGaQn!fw!3vtV|={w6w)G6dF8XTR% ze374kVtYl=b;&jGE&B@QJ4L8%@C%rEJOct}YC_7nJ8<4qlF@9QMT2QNJ+FTgzeoN; z*NK_n-fahq=L$02A1Gu*e}RE^ea!Wq&Mb^oW0i^`P-1!wHz(7_xFj(~*Xk&|`K7|U zI+Vws+jDK;eWpfu}o zXM|)f%*99FcheBdOZ-S;f>O^+VWf7H{D&)*N_;-wIq*2B-=UJqOA`}m8$pW$1^H1O};na5^ohhm}I zFOsCq2Y4)v=^cFDt36(r0mEjMD-S_V_gPr0{<~h$Qs4Yt$gcX=-^#%B!&}^ctDmkZ zxkEAq>|tZ{a%z+%$6D!XFgD9>alP0)IvlA74;IWJ9(sr}P8a!>jt6n!%9+fvQ@Jz} z|3K*3Qt({C`38?wfRf@=cJlXp5*X+ONjGAkna?pJ1SYf1H8s$2O^k`U_61(t;P_4( z^x@WTF@E)IdtQEh4Q*#tnY^|PazXzjSUKG#Z^9j*Z9y7RKcTF~L^k%m zFqMcbfJh<*I*;}lp`_0ZP`IN& zCTF;CJm@r1I@$_L2DIqx(&K2X{sFR+^EuX&Ei|jNqeX8tOfy#ib?3`u_4dtZ$3FlA z{TIRS>m;(|Njcr@p@xUsUzqpaZ36kj(YRpVa_V$Pk&P;oAhs2DXrZ+de~ZimE$QoI zuDuz`OyzRt3P4C^xc_! z9GUL~-}Sh#f6W`T{9A+qnl`+i$W)O2Ovv0(Ymktdji2&6$m;A$bmwNR2gbD+O<^Tw z2-nrSMbUkUkEjf1G~MN=wsAHuSD#CbomAhyraN&(oC>kbOfzci%HitK4fPG z;iLs}bYY4dtyP@Oc-sqs?Zp%vvMGbF@2+Eq$rn)jV#UTKWYRwmlSxdRIa#ChvR++B z17FKoL-9r*)kG{=hZ_U;VwgkwL)R#s!rg1c}p$$`taTH(>Z^O#k#@pxcuB?;Wv z2-S59L3e*Y=yvUbE5>ECV|+IreSUxhng_FzH8)`8-LsUe9->F*{UAD$$Kl7dH(1fE ziB(cEm_5OeWul$&qw`es-_-_sbv^Xm)k&7sP=v|zycNfJ!TmzL>j`OlV8UkmZ!p9mCe1ZOFIJRakHh$=Xg9ioB`ovan$t}l; zL;vb>YKM8Jy%Q-BbjEMps%(s(5L^7;0gV0$U?$&0Wu=8!XLBxBdsKk_nw^iq+ZUkS z&0UZ-z=N*H|4_kBgr~6~1)Y8>vkjx;=rC4-l2$93-o#%ts?Y_=qY;={&hY~NhYa9& z0b2imUcmnY7z`DhZpp3a^@rhh_%;YR@sTVtw`Mf@Tg>_zr(pBVoxJEEA=b`B4YfEc zU}~@t{WCx~g7iezI?~iU_)9px)>()f<`+|2!)_e8Bgl@udXM!^Iryw%8Wvj{v4>RD z@ax;h(At+pC-@XV$gB&*&cTy+?^!Jwi#|&PY~>+`KZRw41Y!Mljx2N1o4N1i2*Fa7 z1giE!SPmZrlg|^sj)#XVlmw zZ{C7^r2^aabrCDRhqJV7Y~dzZH>vqx4*u*)LU|i7b}Zl~ zBWLkD0i1Kf8WtYTCN{#+{8=OiN=;@l-MimH=9f|$BA!MV(}|3qxHBVzZ|Kgi((uf3 z59;2m0-alqeDjT0z*=60O)cTqPyLrpeKn`Tv~*Qge8)xHd$0`cF6``uZ^XMClo3mK`L1m#x^I3k<;S z?NGmYF=IbJ2l~G!5cA<)xKQAXzDajb~!hF26fK~7j zVq9`>fXvNowEbC3I(7&$MuW@DM_NUh;78M06T=MYM z4VwBy0}p+j#jNrC3H?eRNV)z-97xtP^_lFCcF{I$IyX7p+CIQ55U$3wE89VQ-Xv(w zdrWqD3bEqcD&X|)7Ic?Qq?wb#F~LX{^oDfUkM=+mjXfc(ZxS)J?IzWal*}v>uA%=m zE~S0ep}xoRCfT;?Ba!u#f%k__P)CU_u+G1UEdl8e)oTKAdv`c72lhob#=cqobI`I>}zEeSS;u_(TiwgRl z-3kvEsk5voq2+B0$&W2g=<4_%p|j(-M#2VeQtJudU!6dqW7%ZmTP1enz+`53L=F`9 zti{qGLwL5>9c;VfVA7^!-0mm`ryHKY`U?qE-)jb2a_uAfyKCdnJPSJMmJ)3KwH&VQ zO|N&(8zF7x3)!Hb^0@GG9IB`N#=_h0P;eEux3%-bSu^g^Qm6NPkj*cS1^r=Jwl2b&j{dv@8W)8?%iR)2x>ET%K5LkQ8=7$X z_;TJ(gLGWox*a_wo|Doxd-z?FiFxYBVTp8}`H@!{cy!J*HYwH&pQ?#q&$}hKvgsA7 zcbf60{ZwNc3VXw@kotjy{x9Tg()X<`=>uc4t8fdGlc=6gJj#4V_-NQ6|Am zf8Rw5J|05>?NJ5qa~}1G2;$QAcDwJ{funT4S$Z;Jg6iE2G4QUz7j? zAMQ7rD#?4*o=$RSD=}lm)9AEPaj4s4&OEx81bRE?)2@>9=c(Fy0Z9)je2Ih^I)PcCxQ92>(kn31&$-S@PC zud*Wj`Pma>lUs1`!x)}kC+^n7XWDiv&rUyk0rkEr9U zE$9~i7!tGJ;Kz^g@ci*8{Z`Yy&te+%S@US6vxZ*v&lK-^T0u0gz3NZ4{BC? z2FuLjFmhcHgX+}b=hIN)BK1Ert~(x%(g!uc?excRF39Ev(YZfmaIDY*EG9~`BPYW_ z?zS|9U#SI`qAGY|C&jG3Fk)V_dI7REC((WM280*?qQkF zfg`kskA9+pZkKSQbtc8s*SMXqhs;wsHcG z0Wn5t+ys6-%A!&Bx^VBQB{HwmAhmWj|K#y>P=U!1_%0HcZ2SlrP3Q2*-YTNOvbjeK>_-6%)DEg9ZC)EEt0e-@~&&1xC`m3k(YX@czi1 z28)Z?SZ(+X{zzWOv;zeYHRbBg?-o~>xx1f~9ZNRnY75vY^$NPUI>U??(;(6E7H`!W1ssz* zjwAC^7|PsG9IBpqRW}X6}IqWr!LtUr_tY3pt2Qs+EUx8WEVS<15_^|AFOqD$(}Z|}w-hYB>> zr_37ttEZdo%~@r`B{*vCiSNC?kR4v0>|Kv5s4Xf?lYgiYDJv1oF9d#$b^!YM>9f|$ zgD`xV6ZjXWpn%{W+}mWxkC#;g8|P9kx$A_ppPrz>uEk(7-x^o@uVNO*o;B}!-Ht&* zCI}p@^R7lOEq&^0UjH`|+|Kqw8@K!0%$+<8pSWU3HXjtcEtv4k)x5%UHZat)oz;IZ z4Fq*xVEsx(CjH+vSdg-x&gp#ug$<{O$CV>=JoOZ9ySEBGIR; z3}s@#F=yf6`OvEhj4JrTqCfco&M9PY(knW@G8!yYHCVrd^;i)Q zj&kv(v~86H6Bn0{W0CbV^4Kl>^8-gSu|5`X0>Gp9u`gi=>38mcs84Zb(fARmTkb<`pK9Hj~tH|5}8@%FK3ha$) z*mL1I@dY(D;z%|rvlhejeXHQBQylKrPR0C<7VvY?nE8tM$JAURnWwUQO1;v*JN@RU~a$3URF&#JR37P~@B` z7!7Aa_0I>~4Ok@}nfab<_EAACJttPBp+%6(M%$$$eaRPXQvV#aWq|XXw)SdA##ail9_K1ICJ9a*gYgMD522PZQ7K z4r5AuroJUgtGday4}y%~vm%Uh^+Msp5-b(IO^aTg0o{4mQEuBhxF{2aqQ0Y`KVk=M z%oa>~RRO^ko5|O#si3!@fUiILjCzajBqd?f=>jP~?(~@o&-Xn>&kM55=~t4>{;A4X zEEWcTS875xTgPjpmq~=ZFunnBdh;TUtlZ|MV;xetH}Jj<)2I2)Zm?+JHOn zsk7?AOYqOBnZT?SCXKPtRI=F}v+YIjgPrfc zp>8s`Kf4A>U*@B9jUgJW?BN>WHFz(4GxVw4fIFpXP*-pOEO|YknyZapCy0`F-BFOy zEX+pEDFI`-MMPokRI1j0nc8oi2c3TPWd8+GEU*7W-mck3rp-2CM>Q6(HV10KZ>$9k z_dMsT7M0V)h=urL)pqg;Z(^soH8d$S@pe331CNhQft@Gkz}E293^R2Sy7#QYLa!wA zz~?NrJFpdY*sO-;&2F&Nq8fI+&4owCqRby3BS!AQOMaDs1YVJtK`cupv5>QO#|+;> zr~CWO?Y(=*m-Nf@oakTv?}2z|cQC_qOC_kgz(n%pt3=)F@e82v=8su$?ggAJnT=*L zH)ws!dUiASUNG1;2>l1{!<=7%>?6r=(*C}mqzQPTxk3<8l@TPjS9!zy>R`N@coE7i zA7IVzSl=^lu+xT0MF}opx500dO+H;opuaQM7;ZxwA)(I;g z95#0s<(}W+G^An0Xmg!2DWsaho929MoSY7~E^gtd*|nTuBLy4O=0SAG5GHd*kD~mI z_;kS`&V0wE@uZ`mb6Fvsqg2BqU!w6x=mDg=-@wBrEk;IS9PNTQ%9?>6e*YPYKSys7 zk#~)dmbe!Fd<+HKI*!&4@2Ka?weaXuFo?`L16A1zN!!?M^5fD3dg$h1>iIMlqD?i~ z)dlu|@s%W$isOW-vdm!lVq(2H3*4rwL7QnOj$EJ2cF(xOr8g&&%!Cjabb0|kbr<0z zm(Dt};|aJN^ygOxbwjK;VST=h;j7O}LA8S;&TVkR+<<7}%OwK?ELvzv!wtN%Tb@fc z+{4c)0?dq^(v0VVkM*fF;gEVffVVa2FO_Kg2l8tYaIhnm#`j&ItAjVt^j)2hpuK=9 zvPW1m_73K)UBo)tX`-KTGv2Mrz?4pT#?PvR1Y5enA%S8rxW5Fx)W`!XafdpkEyU;N zIP2WLGVJZ_r|t&rR4s5boNQ0zpJyeR#HlkN?XVi-Z{UG}!U?$cq(2plHf5qZ^ss2b zR=l!S3!)w=vunfu5iQ$s+VS%t^+{2MxPmMmTQdhd$xOJ!o%yyZ*;CzfgJjLw5wfDm z4x%IsX-w7@NboRZooAb2oy#tM=t&8BXzwUtT=RKd2bI~8@mgMdej=AIRA;@h;`Zopx^DSYts0>;w zq%ig9W4iyu3Q$+vP8Tf>#j2SllnY=$%k6Vup(4uM=*y*k*I4v;&I;(dlV_uU;TsL&{F6q|5&XL8emWL@3Ts+EKRw&LH5_UnJfjAU( zp2gRNrX+F|*9zkH+Y2wJ16P%SH8U@wWOE7jrA}w`s325Tf8o6z7Geu8$+GQqFZGL9 zftH76fzs+xswtcY`rB{Q+=x6f*cXKk(-dI#O;z0cSs4xFtZ3Wn{dgl^kU6HK3bmJp z&@`-%Jbd~b(&j$@h4m`O*pB?H;55T}t5YON7|D7fX=}7tmpWKO}42S&$Z< z3crg}7=9~tZg*SLd-R9$y4{fAyc?vRjlzC0tIRk@tG`;Zq34eq8aZD?hXaD>s z$7XW3`mfIwf=49>9n2Mi__i3%0HnwW7ObXiwxZBO!tl@MQBaC4z;5}cwD3qSF{m7Z zw|S8`T6PQ8$i!n&u{>=Pj6*5Szr4!ZN#LV144-#CMtQljyhMA>d{dsx-MT);J+FVl zL#zeetfP1-BNtNWR}{GL4NN|sgFj|=w2SuwT>Xyo4eoPPy=@cW$kQG=#VZBI(*SQ> ziN`Yr(oE1^js|t_80@T+#f*1z@QQW^+MZWLmz=Y_oNh02_^vb~>N|z05A%bki^SMj z{s%BS{sVs}_x!F?jK%=@N=lUf!3dY6n^G$c?>|1p3qz^!OWp|F`#(a7syXv^g%K)w zS@8o6q!<|+uGPTIMX#M4sjzrI>KC0s+qq*{xoiO`*cSyGp3TK`KYsFB7g@1CwkG1- zZ+EFgL^i(dO{PgccQ}g7I~cIGVB@;V!26p9W83l${Z`vyx3ve(8xdmupc*%!>!xi* zqHN;r70lwmC@yIcj^Ui~`5g?yip&Re&%AaLTUm^=WRvmBrgW6J-bjKaUt-9O4bU~- zhLU?MVb*$%N;S9$M}-bzsZuMdFZ|7Wld=QZwBykBS(14*p#mLBWtjFi5wv9I2lC|U zF>bv|q;TObR%!;&;s?S^cVrcYJ4w+;-YsbS`3bcuPB0I(7a}f7r@>d{233C=49Rt3 z*r!#9`=2R6hlMITt2r0c<}8Hm^PeMGXhairH)4ZfJeG4L#nz5KM2p=So-ixzRYuNE|3$jy+AW@M-FDE`ffX9QBn0f#Z(w#Nvu{4Zcv>Ba%(POaThm*F?$P|3J1{F zzJ$Cs`DiXC(gP`2hj9*X(Kt4i*lDZ>>9w_lyqU-x2{FUz`MaQBR0iyw4Kd_jD*Puc zg`ua0$lZq%Fz)CLP``g3^-mQ-zx+pNn!SmxEIftwKkt*PH}b$kDF-#SpM%8J$>{T7 z8BhQ6Ft*39tDoll7e#YkQYGiBe9vEBFzb#5jipw&&?g$iT9bGq3e6}w@`-AN*wDH4 zGT=MyEgc^~zUs;;tcA%WP!;Ti8x1qKd(Qyc?b-v|W3Tb!EKlR0(H7EPEWie9in5IX z=KM^x({LsA9M(E%FmV@u@qa@AX9X+bNYwI}Uo`<^UQR|C;>E+QKj=`H9=kBQ0;AvF zf(_gqag10EKm68XGBBqIWBg> zld*J`KSmT>Cd+3^llL<{*vD;C*m{w8xS@KAnmo_MAe}-oT|=8Ct8`eD{slhOJhaWx zrLPV|lk4jr;`;)9o@Ct*K5NF^D{(ljcW1n?_5Kp9eY}9g|H(s%%0;XPTls%fooP5$ z@AtP8Q5iFfgbYQcNVu=Hg(O3zG-yC*)PTfSMM7nY2$2j?A<0l=xUaQ~GNhssQ6ddA zs3=V`^xwbd-Sf&jhvRVHdtcXDpU-)sUPC2yFxO_i#`cp#mpb7iuK<56xPjtK8q&|x zpf$S&^=vGOU#%dUx%&>^^UX`r;?aUPWvxJTohYt#-vT)@`84C%WH>rDm1~&AK;y?g zBEp@UeJ@?dz=TAS{rVp-LFgJb+gL)c(@i|`WfJ?iJA|I}xQdxVFEC>E1U9N|n7r=y ztQfsu)Luo?RE6%PC!pxTGhWf$1Q6XZ1;&;+qf6R*Oc>b;I^Kstp*tGt^onr(xmX;K zDk6?mBgRfqiD-La70@GL5cGE&c=yj@T5FD1b~Qbu;%+Qlc$5N8BDEyu$5w0(Hf9~w zy3kca4~I8t5VhZ;jH{Rm<9waF)2UtMN9muze$@!pIkyW9k-#$rNx-%s=*E(5rTlIFk-mPlw8s~1GZCy+4mimpp|l( zOPq+9ta%{}wt@L{j#&bVEDfmiu*?I&OJ8w*WftlfL~_qE3xGMkq|jpmo>XlG+mDYS zP58uPaZSw7&Eyauj8;iAJTpuMr#Fgv~*|&@xW_DYU2R!Q>dipyeQ9`h+Zept zsfeF;ETRu@{|Aqn7PB{BPsOL_BpFTK7CiKIGfa$@A!8Z7_+zvYR>Yp;sN5?!ngpd` z9&1o@LLcO8s(|0==O9>mGpIjYgC|#{;>1Hd$bHv`BD^$gFP4CxjWzJ)syNB5cIM|S z_2-HF$pMGO)g-t6EFL!SCl=S+$na<$=4h6puJG2Y?10Swgn1z~Wk~KcV`>M*WA~MD)!ktj8Smk- z&fS=9nTEF%3sGWxfFq(>;VyqEMxyy1rdHlW(S@#bGuM1rw|^_dEx3Y0R~E7Lhb7qU z-SN<7g0MHbnFPH)#1~tcPNbzKzTVc}$m}Q4T?=_1 zw|@ZZ5pDXzW|;4H@iy#T7K(efI>PQr;k4=!!bQL7u~+ROxr%zl2{wH;La zZZ>L`xPmg*aDQq$AMz(>qP$rW)|`~q1>a9sW2Vl#2PZgdXr;C)rmbwo_I*>C z@-IUCIH3zz|NS!mUGFpSS^o;=Ug6u3|Nj#tVfO+P<)PNj?Q(Y`GlaqCeG?l}L33|(U&;JhE0 zcXq?^+7bN7^P$dicTo3_K4i5@!VX_z>T=H#M{fV4?>^O_f6Z|WxyL2Yp3CyW)`pl) z?3_wGGt7wFDrH7z5oa55+KbiYr*Yg_mC8hM&vq+Es88VbcK5VUNVJ^icF>K?`>xG+ z#&)8egdMRvV}M(ZtfP9H6i8C_HgsIH1-qpQLv(%sr`93G{$cQLy&&_5yEpHjw1~ZP zITRJmG}wFtb#n7(JBaYU6O1e){s$!(OS4b7T{sacHuLeu5f2!?YRr_}eaSob@ELrc z7lGm3gIqfG8vS^IPkwVvjEwpPOiSTSRJeH#FK(_z{oSE-wQ~&^dYy%7hfk0#LjC+d zNoA0DTZ7Rx;-j+d26n_a3@T4PAoAlvRpRcU@Jy!!I@v3r{c|cP*A+tdB|DVtev7&0 zxnME6AKUEvFu!6StlP`Af)h$9TPV#;(~QJT3(upu4i7$+Rq&0kQ$qbGg6H=h99qiq zGF0{O4tE~!{JItn`oz-t0_GqnaEYYt`v;-jYhd#pVOA#lE(kPAL!BcOuF|?5*m}oe1)LPkh^mX^R)Kksx$Q*p(GC5767Qf8X_8tf0`~>c*?Y1 zyo9cPy9P&(>NDm-N~}(?9({N34hbrHj@IiAp4ufWuakit~>sgUr?{)QovcpQj74&kmG;TE%S0uwDhH?_>vV$_yD z2D3NHC@wsQ>2i3DgmvF4~zl*PSDnKyT%(m{6W{WKJnUe1|tn|GhbQ-KD4xbWW z%iC;Ze_o)!>}TSfXDlh{`$1lgDM0H(IkLmxFPP5q0n3t&@KWG9^gnO}9UCrlQ!xgg zhLnol3y& zS*gTH{|pqDJR>`7++h8BFFbZ60&<s>m?1bL?r^zAG?QNd$Jv%w~h-){;-l595SGmtj-uF4o#ohPfQk4G|w2 z__kk7nB@=7f#IIBu<6YpW=dTKq5a(1=FI|TN(zg6Iujw!xtcHIyO(C4@-uz6G6oa= z{vmU8qHynr0_=qCFlX~o@VoHH6g}L5Ow%VO!4}9*Pr;xGCot3JF1$H832q;~gd(mt zz+|Bn*87i>4ufJyT62k5Y;c8Lw!kx3$_E9DOt9>lMc9pZa0Zt|&-PphcMA7HJpVW; zY{=)+Zh6LHvK$@wQ43^oDDP_P4cMdj5Pq84f&MoQIKEPx7v-)E-~LS(pls&+H%@JSNhqZ5|-%i98P_ zEA%-~j6=JYqyN%QXqgjE_LXu3xi^3jsBQ5$N&M4MSW9)9v=jPm* zL?hx7tP6+$soxCW_DCtp+F8RZ2q!xah0x3ccVQXZinjXh;A|KNdD{_fhDF#A!AXp+ z^bd^D6$E+3IgGns5snVqz@v92tf=5Gkfp`^F6r}xdA|=`bY<|w!vrwO`$YCcyMv?i zQ}mdczNvN%B1`(NxVQ z1Qp9WNaFfXl6CeM7~Nh2eII||jRU9Xx09iuX)Vcy-LJ)!E+%lt;Rx9~s*ir!=V(NC zCSD$R1hqzb8168W6`wVU-E_u?v6$-t_48~&JMkeDL~6qXUnTgnp_Iy9I*8u7Gnl3b zb*KpvXSZ(+fq+~wLgpmk$HVqylWG%R1U&KTGD`^YISk_i7ocTf0aY(8ps9;onQwa~ znePK(sMjh%jBnfGsF*VD^ZQFX%}RJ$@4Rr=4lZddCCDnBnaw?y#7I;af^tM#mHy_- zcvS2(TJ(uvStduLo7lyBG&%#lUU1D!`*!@ES66kFe+xW>dC0|>_3R(qwN%AcT z)?$tfE2chQh+qAA=)rn z1D(S;WJUOP+{e+UL~ox#mqSzWW%p%_xL61xL6#_N_#1^xMfiqCcT+|}m!lj@F_rlm z;M2Aq1s?c9kkb@yx3>i@Z%wQUJZcH#JlAMHx|ZGe+no1yhY(nQ;buA>rwN@h4VV55 z<;*d_2;8g3?=v~7cE<$x@nIFKDKnM9mQt9ac$>JVHN%)?I*4*R9UT(L*q5Gx4>OdR z#Xo1#_k$7ij!!w(9yBuj(}M8e)DYgVdW$E`e$nnCJ7lD1G4JIOEZ6ISNuD#k_Fxou zb_+sxEI?a@2L_1-K=>aSoN<5;JE$i9*(Z#J3yN{y%2+JmasZAAv{HCz4~b6==M6EQ zFd(8zTjoxKEdkFUZk{8+tuVSl;R+gJ>ot79}eF~l$$l3 zGg^mZ{;i{6%aIOK{~5wsy>5&;yA}jI*7L(A<&aNa-!N6{1r?eZj#E&A=@YHRhh{6` zPvT)>zs(u1FNnr^_3QYkNR)bh>Esd&`cM+Pl==`ZvzOP#oyVS|@0_c+%P*TGazyH{ zC8Eq~Pa)>r-Ck^+b{kjET7p@dTOdY9jhUCGz@VZyyD~DKb-i5$12B{%NE8NQAzk4c zidpzW!#o)_c>~u3lRl5ZmeYAV6vPpdG21&#E&K|~yKVv+dmHs598BFJxjkJY-KEe=isy4?+b>J0 zT~!*M+V_HYKSCQ^Qwt!==Pjm+CetQ8G4|jBL(F?AK>JeG5G4n3RCrv434QX6rG*}8 z^cyGj!;{zmV?P{Gw?Tnf%Sm<9Zz$U`nYk#w2Ib2B!#o>hY{@td4Xt5dE8GZv0owR{ zAePs>_X^Z({z)gb>?a$~i$RH+CL?m&jwX%P^D>7UXjzRI`(5oO_+2W;^l0rW5g$?Z zW>-C)jI`pFNDWZ4{Z6>M=oJ2G%|oN}S=i;N3>zG^;eo#xn<(;~{19z|sb^!c(9;To zEV-REUy!{ex(HR*eTF$1+t5BFi$?3uz@TFXQ0is@=vTXfM_~b^180A6$;5!aBN$hF zf+W9IWKNvC#!(gCfcBZap!(`DWDCEiy?4Wj;*@o8u;vW!_Ie>!a;pITR5OA0Q^H(p za5Dz|eSmvPwTQ>obHw423>(H}2p<`lv;R#}q!*hn;{DLwP(3u6JHubb_JayAD_W4z zT^PdU0p4=&yCQn=ygRhbc~4{V-O&64i(jT@LGwoeM(A}iUW?w0A+8Vb$2L2Ta`26O zjZTAGQc=7nw{%|R*an(i+fFxa=qCMbakytvF7-}24Mn_HH1Ej;qyaf_@%&@HLB0bX zo$CT!D<|?-UHnW&l)~_6TPSXyv<98`yroMPay!*R6Xx=#iRkgjn0@IX$IWcxX!RT` z_`+`>0phozVf%Z|5GuqDaHfyS{gW7PmpHIWkAhd{PJ>ZbD2d|ol|5fn@xI(|-l-xD z%rIDwzqB|qHP@>2%aS0=7IfgIv%8tV?B}4}CI&C_tGJ9|C2G{3r?byrK+_M!5D2Hi zdu$vZI6bAS{#kOw!!mw<-5n}zD~QWC?nS~WGnDin6V?6Gn56Ayc)!0I-(|(R>@tmwX9m8KfBNv9} ziNS$v73?wWgRUDV@T_GMc|G+dq>e^|L6kV`)Bj0j_sing-8dMM8zQ9_O3e=&(Z6h6RcPOX5lugbV>j|8LkXf`|XzA<|Hc|h~D z8PL3pBL&?%L-wTgVq!xdp1Na%CHuRuPIH(#CO+a7DOABtlWs`*y9~^y?`0YT&Vz*1 zVX|2H3?{EoqK@V((KV`wKd{c9WIWHqD#Iu`w&n#Kx?;#w`{~k2dBv4;vQjwucNTqg zR2Hq@`~i0pAX?2rEuI= zBSbgk#Pj98y}_XGe{tR194Iqr!}r`gss6SQ9NoANdM-#Y)=yIKXsZc;MDDf}S{JzkGuwW|2O zRF0i-a}s#e6V&Fj zG_=Ov)b#B$s%>cKowny0N7O*oR~NYP_A{^DU@HG;P6iDdzsa3p zgkgruI2iudPxrj}N4$zenNQB2dEpyEP{n41bgi7kv>wZVqowc3s@BIOT4j_b%(G%w z)m4!nSr^GBUujnRKq~E=*^AzPV_;i&28J&eXK{@`#1v2BYcCE)gE}+LuxE|3Ma}58 zDv~}K7Q@$;rTqD^o>=6nPM(i`#@zp$;f+Nb^lRM(@t6d%uw9ImGaje1hL5mg$3SHy zM~RcusUR!oh{3h7i#SGK(^i^ASKo-IeL8v&C0z66+znR-oJMp0Puj5S8oVeH zX55a2(!!cW*ircaPp(|dj2XS9YP!$3ELR6V>Dv?hXB3C>xj7-w~ zPJ$2QV!6N+o-nVE*Dm^-DC!u1dkaJ5RXAFf#CZ~aIv3}3X2C(>I$pM?8T8It#+Gc* zAj=m%A@FM+n^i5zM6^u>!_oPC?~HadE$t<> z*wMw%l{`jLy*Ds@BBD4HFprhG%8>zkOd#XG`((kiTj;guC`X)hrhO~kV3~dmKkY9! zTRUKa#xI3HU48`=C2|xq=Re?5xg5vdl;g-rN0Kz21jF_g(AgCR&uk*mVrvOidvgpg z21dcvE82MC*gI5JxW#+1`z&TJtRg1jQpwyx;&8EIc(6HtOx8!)ue_D`UyPnHDIOyhfPsAK;n3 zJV)nMVdbV6fyITjF!i1bsZ;*|Ay>>{f-#cQN9Hru<|pvP9vysKQ3%ha@8aWQbNLeU z8(>5t8~t~PvX1JJXmurrg#P2m;s^FZW$Sh#b#FaWE)|b~*#?mOpAyKh$FNW6G!+Z} zi&7_f?6SU@Y>J8$9{j_Z$&PVUQ9)T)+7^I$njsaBB|A+$Di-6e7hE)7qzm6@grUNA zBXTqH06JaQK{>A``1mClzL#b|}L-<5U6h#EZ zFqzBX=4P5Q%^mjO_o$K{0MCUna97a_<73C^sh}j3vw4mc!kodTL5p6v8%gInA0@Tt z6}cSTC6pXk!x!}GFZc-ljTl`~O
KN>)dQQ_)?OQ8axN?IyHD{xj*Zty9ZynRjX{|dp8xNx9q@$iKs4GDAaUR$9e zh^n~+fth~1jb+OCrY;5J#Qf-o*QwBAuor79PEmWi0gkph9kj5F9@~DFmI(U5{59Xf z@b(fsaw8cOqEq>E3b)}(H6w^{jG!wPNU%C`3*e%7Ih;!rW3zs5K>ss8@niCJsE95E zx!+|}O)nO*H6w|oQhi0;gCJrZQbC+a8mS5Mz~#Ja5G;@k!a*_EZ+#aw#@XXh#UQ+T z{{>uVP+@-Vn2f%Q7BG?CA!rf%9(`w2K-Gy^tkvI5M0I@xZi(Tn^U)bp{-y#G&}v8? zyyS92z8sBc&v95HEMvOUhI_|eev77e^1-Wa3FxviY?ZVwNat5We$Ns%F7hI-8ZyVP zr;dR_^(#D@n?Ods{NXKn{uq^Razyhl%^cam3;j%EAxO&s?S4&Rg}qL|FA){Ainn1h zBp%@V!gFXUHb4S8%kjjALa6Apg2g?Padq2fR5jHFm!&aa+!KRAp;d6vYc@C?JxVv- z3L-fwP1x(Z7Va&jOKe+e(1h9brqJ<4(txk zczHV*8viEa{U_CMBPE3LOD8ZH3K7hXKe`N^D#FSwuH?@0ip~wgqz0((2S?XY=_`|SgtR~2DbUYLFdP~(RT;D6Pv({s;{LG1H)^$=%^UR&!55^ zuloV6%TuBDz8Yjjm4KYB6E&3!pcOm*Kw-m7yi?AR4abB@HxXkt$>#BQn%tmKJ3=AU z>MPH0tu|N=1Y&)`Kk_I0FWf2bMl1P8klU~hT#_d-n=~)e9zP{ee%C>meW&U8#9RpM zI>k3&C&0z@GvHkr$xjqlz^>F&wBC?2c5UH$J8i3QslyRCX|$G%+HQg6)ZcWH1c7}e zVvLD8*CelaL!CY!#CV5U*z{@vs~y1^bG?o+q5OOt-Bm_)J=$SB&9o|Mc{Z9}$i+L{ zELR}>G%?qXL6IC;jsh8nrgz)HuJ|D@V23GPJ7*a>jrEhWO0L9FUXb~0ECSibgVFIh zONI}Yz;DG!?r(jDn}X-Dfv+!N;#S#~_ z)MLL~K4%W)a&D(Bv70qyjTRC$4XvGHXn%7dEbo1Zt#+wsyQdHy4hXX&9GT7`Ydvbb>=b&*4Vw`0ZoR+!_Rfz3^C@TR>7Y$-1U z(dJd45GRKhFX!_&=%sTxaxUjIk>E4?i?nxx18b3zgPLP$@N|gM&wF;mUF#EIvG=^` zPUZqJljugpBp$7-y$D5;6?DD!7IamRWopAR>8EDS#$f!HI=wFA`M9V8NxVaK3*9EG zO?KmW0UG~5FTilVyPu!e{B2wR-;aPDp1XGY*@d_zw=0r)$uT&1YYDM`?F)xJ6A5=g z#CGEhzO4QhwCRiE^0?8EY?Ovaqi*8MZOX7NeFuqjz5-kL%RaBFI)h_`kgF+W%C|D>x(L?>8;0(|I35mhj3a_giota! zB2Z=Q2-i1RfL*3HcoOz2@rRfaIL-J62m9<;4HbLl_v}_EKHm;%GOFL|4YC{Ql1<0K^9GX7{7+=boGB^o!r`bnkKy`@GMe;*&n~u*B} zjQ%n%q$Nub2mcoEa)SyW{Nf3^B5)!z%XuPOs(TvTV@?xCMUMGkwvN@L0Qr94qt^NSjZ^p3Nu@3C=7jsLDB+gKK zne0{Gi0%gqQQdk1{->P^ZK-8s!TnP5piLar#%|Nrn=_fzUlCONuQXfcpvF4IJtR9v zr%{ooIyAyjnCK+>RP8Q}ChwOc?YaDv+?1^0SI^%;qZ>GeL~IldS6vDoEvdLs|1gYr zPsPgiTwczjFzSaT5Nmjbvozh|C`4Ca!Rwu%TGIrjDb~2Lb1Br$)T7^xzJTz+N&MM8 zh|;x4ZQzQr*Smoj(Z&0r;=Zkd=Cd4x3EP`rT?># zLg}?=blO*m|Cvre!2(eTHA zGk2sVLFmv-c6>oDS>N-F?<(N}`hTCIxb_ul^blzD`aAGaB!+hGo`(v*=2H8@|KPu? zaiDnLoSOfr!yf&9A~pLFz4fmJ4^?`TTF2`&?&@jy^HC1gs+ZF}33p)hqYX|Qxd`=& z++ENSxDKon|DRS0#{aOv)POu3TJo8nuPDoUSgb@5gI(MVUYs$QwGi@D307SgA)xS> z{tjM-%0IQiCp{0|zWNKFN*&;im=)P?9S_$3UV?&DGB##e(&1lUQDOE;BBGIlE@Ea> za``BUZEK_pYU4@TH4D5T8VWrzrquMvd05nX301Dvg0%Z}a(8+l>*b>dPKKx9gt8n6 z)g)AkSS&#wF$v5OJP5)D&*|z{Z>x%Y1n}!>JqR*?g>*|WY8ULsq^crvtWgs8_bRiF z9KCD)g_&UZFojH+c^B8|3&QQ@3HW|uD%zjoW0{gUuY1g#75=gk1*X-(r{n_=zeSK~ z=4)YWL@p{!JVgf2rh@ydbl!^C?XX?sHY9w0&Fi1tLk{~1!`-k6csZjSmF=?db9^@~ ze<%f)hJJIis{Qc&o-99Gdl5aJQ%A1hTfY|H%JC=to6ccIh*kGeO;JPW68Wc258h^O^$IL!9Om- z_)uB^U4HhG_D8RIS%1oK=9Fa2>Aq%qT-UIw&nP-dB*@nEO-9@G+~ zF?1vb8#YrqShWRTF5oN$ee1E{Uom+X+=G(yM>rBxIEEBW!S*NVXp+2~SvOaJ5%qFn zJU&XXh9xm{qSJNg7SUlYKb*+AZ5+TGewXOQUs@QRZOLqM=>os@0nn(u4sL4Zq`tG6 zYGtuh?ua6BdXP)(In%tjV-{)Dti?N&+tF};lk1vld?#m3H|;Cs>qdsaod=0@cYGR2 z?^=m(g1180OoL^jUo4BQ(!8n&8cu*?=JJ$vgR`nM849~~# zZ~7QvF3hUvb9DNaNO+WGRMorI5+}^;=Utx}jV3iynC&^&`RDo-+5MBIVCTtb*!3qD zU2-b;N@?LNd(s(w`aY3b(Hz<&6p9?G4Rp#wLqiw-999TgzRXCfC9=4BXBkgBpEEwB z#z4d=J+v|lrAq~bcx#cH1NcAZZ+EMr4T}arewHR9uFZ$Vfs&*&Ydh_DYDRN>7Qmdu zOBmer4SR2o;L%cV)?3RAzo-iXlT?pi&2LjX%X-s*AYU|c-vL8Qxx38*Rkm%91ulD9 zL{4e7Q;fJq>=$^V_>c~EFv-a4nFi>!E0RgK8`-1yjA2c~}2pT7r zS8jj!2pd!WlI9T)U_LQajAIlWGSXuI*w}%ZzZpa~EySJ2Iik1h6ErzB2fN?jgmn7_ zY}syo^ftF86W*p`>~&cf{uYQi)ytXpciNyn^BW00Y>vB}ZsWe?@wknh45d$2fhjA@ ztlSy{Bl~mtzOM1O{>mzxknI9(+jf#0b$gjQ`MYp%i$3(#UclDCOzgaF3OPfCa5+Aj zxU8H5?QT!GTw@9Zo~+^6O@~O_26e{X>;he)CCA1ZOlR^fRzUtSURB@klkhyM6WUz^ zu;Z#BDbeB+^~9^3!BZC%D(B;S{*NqEINn!fw1!=(M8c*rFN z7nQn_%he%h8nYBO1|`5o-wr5_i6X1^ro$!=aZtAQfRN%5{@;nwaBP0I@j{#4!j_%3o=blj0>Wd=xk>2#sJ)q52A&?)mgjNENDCOi0lp1 z$C|I(LDy}VuQTNs@ef&tJ<1p9WHt*5;7XOBIM*>~H)4!$#zCNdAxP-E;`kF?^iN#O zzU5D16}`OqMtjT9a-JU%`po6zJa1Gf46mUb>H|3X;Ve9_p3PXiSO}s^@8V8NE!MC~ zo!|JCk5il1p~Tid#Jf%lFVIYAy%0fdm8UZ=$F&)=9oCrpEFJR$M@aGS%_#BnD^8v1 z%%2e+*v$YWh-)!%RbB7RmBt4%Of~-UMo;^%kxNrD528 zL1;+ygr#O-rfxM8@VBHq*3B(Pq4w7(e=y`}l!NAnsjTXbFQlSXj~!fngnpT3hyh#F z@mO^RFW88MM{}ge{=JICU4AR1t^Py&$G?)-^EVUg@Q;{gnN9X6tpe5m7PAi>m0=6NWerz>E=caorU?o{w4jkSn50jhI@Za_sm~faS;d_^(mWmX+bMgaJ zdK3zKA|HbGBr~+Mc}819vw@NLj6dq^sKPsEXndE5a`V3Oo((Ci=Y zMae`q<;M+v!}CYv_l{Sfax#f5`?Cf=%AW-L!KO{KY z@*NK);+9j1u#n5G1^$iIu=$bPsLS6`KUOJ5MOSV z*X?bIcg{Lv{IiYp`g<{ujtat@niP|G|3KW{d6BQ8^&M|tO+&4mA~;q zJ+wxeL5=V{Fy$cM%-UiyW3@S2am3QAiop=36hm{r$K%HIZ?v&%I~MBl_=?}ZLBh`m zd@Bz_5V-q`%&{c-Zl5Htt`>gU*{JSnD!_tqfTLkDjKG2(NaOytNk>_0DIr z{;}k@R0S^D?@Ad*Lx@sa551!Y*!@kajNPsV+$Cj$vn#W}cC?nd2e-q>v@@t-7R36%by9cfMb&7>K|Il^4!3s7(f43RzP}Y_ zx~@(}vC@B(xalx)FFLuqNIPD&4uJ6lY1)|-O9#y&$v&u*a7~^uRXWe#)7L>h)jcBbJw%wZ8DGffUKhTm zurX~pk`6P{HK1Ya97HV!)G+%Fvvz(0x3xQXC*S3QbcYgaTj>DIf-K(C{!+5eL5;ES z?Sw}j&(XaRtC4(M zGmIa<7NJ(04(1E|<6ECNL1uh+U^whQV>E8Z%GVXqWo{pN-=?WDrP;M0>)nN4imEuH z(K-BNex7f(&>Q?F219|WGf!v45>5WubN~AUT$__evJHkX>byA2osfY8LiP}ykO>yf zJa*UWe^fjmg|~098&-P>u|XfJX?EjD%AcDG(PM!$d*}w$eDe^GP8Ngo`XR8NqKN{D zIXLtqhaA>@3;|E9@nd@rNGENC)BX>@4*jU){ZammYrfP&{0EUS<}4?NR-%hbE5sTs z22XJVd>l6l38%$b(e6}ofA%&eW}IjO}Q(WeQ{4DZqZbluHHTKk&p&Z-d(Y z+1T^&8J?cZ*w={lU`;zo{q@Z%GI=}j3f42RJ@;!L#Mp`Pqigs1UcqwY5@Y z?e0%U@0si1n_)UJ(-LBemE6(er!4!vUKZ_MJ%FZzrqtRs61AlUh&ig0%U3z8lt?Fj z%g+Rvwx^s)^AQRq$uaMPXIDK65@hgw2`&3vPCU+?!X9U9#@WV~Jv46+LLP*GZq^;% z8!q!dm}t+D_-#=~Y#yv#5sMRbHehznI0@i7u{!TPj3Wu zxLQvizrD-NNC_xQ)nV4tEbd(T6(oh;qFrq*{+WEASFK$`^Z%TO2HA0}^XbFSljF(g zfq5`#PXS2YRRsSEH@w;F2zk>_aO}fc;xcC{ggrSyzNh)4b9^095qpmTb&~82j}P3g z^cI?apT-K;w4vd^5c(S1VVQ?@RmJNeY+LS%IU*eEa@t+IFaHXM=j;ZF7tT=nmc>em zqi{l7g55EB2#G}--#RCjvnS0Y^H_P*3N9xLHzZS~3M+iFXgYs|Xa*`5b7XjS7{-h* z(1%}zv1z^`ipK4x!R6XaYV1sE61JE1?TkPRjzMtNW+p0fkFJA1-cgz3oUP1Qo2e8Y z#Tglun9*TIrNjhSi@RpIfi2mXMJwhR*+e+~23WWl}_)9C%lyHRC7mnGk+jV1Z&^tEmv z`2YA#9j*ug#D2uR8+u8<^CGGq!JRMG3&EWlRcxq!i+vCHL`v!dTuj}7dPP6*kNR5B z9!NsNd4?cmrodU4cOK4}QNmVKVKT4b?Uu71v*3EO6#V;p-pWPWc{%aFm^&t)0 zE)Ztwz;=GZa4yU+TLaaXZc?|+kAdBDn^Zn#L9RgqZ#p>9z3>_9lXlz-Q~qW zTrUGtEIEeBj3?xQ;B5?iJ`5AB*Wp^>TJ(*W&t+x_Gs9~JKAA5L8ot8JSd1{}=-M%T z*h-s@iLoX*J*45pOb)?x9Qzk3L8fI9&Y0*%|0O5Gpmj6+_%;>JWgbD-0ZHt>a)El^ z4#mo?8cdkT2bw*t0;TQT(EExD7AfektuCV`d$%Xh1RR6+~xa`?-iraCt3&>6Mmw!tr+Vt--sS9TnvM!?68;(!%q*lq2DGIIzwkQ8~AGr@s?Nv z{s#_&l&AoPtWk%&-ImNQ`7!Xq7PP`iurAOXnng93!O7Ra``;ekkJ^3M{9+c=+}b^;6WxkR&*#BgJp%576WP%S1$_BW8BJbBLF&#p z+?aj@eHArO>~}H9@3;V>Qf1J-|2$nkF&^)W=rP7O;!)G?C(gWIL;H_3^2?7;Vs`(h z&gSoThC0ra=zqBj`j#4!G}H6+#?vI6RHn!@M`}XH=?UD-XeuLk`VFr+p&u6s7Gl<{ zQjQBv(gyn5PkbmMYs(3zw{@s7+?hI=-x7UCg?fZba z&XKq-JP2lXoJ6}-^WbZWF|(xYGA`Vu$X;mW%n_2D={tyH9Dm;l-P2jn)_+7jXuQ>O z*R8OmryV5M#a7yI7B^EP8>o4316O5Sfak@lFnYsIsLMKmNqt7F&dFAc->dq zyK;27(g6(vZ^3pvl|S_24J6&xtNht22%@KM!eGuj>{f5Z;jvB>_-X~Ym#avy_aZE* zyMakNN}(ifI`jC~EVy1dlWYW0J}dlWY4fP)|oeeQBX(as+J%qZjuFEj~UFpZ*CAO z{t;w;Z{WGa1e5S>U1(&0D6(`4IM0bAcSPP)brmU-ASrFAUEzgt1qRUGu@jg(?f7xS zSDGgF6*oS5POj`YjiH}kz%hY~nBH_1Jmbz|*rZ?3Vk8Yy7xQ8934MHfH450Bk(?pI zk~dpOf&TIC#Ratq;OOR${(CrcQPmuLz9I+$Jn*Dx=W56 zx-IA}dnGhPHDf?513 zpANXNMv)CD5@Nk?P&C^s&gOM?@U}K`HiGpk&|+|aYJ?ugN`-^`M@rjB5OG4c1uRSX!$9!>7cKNI2c)vLukNlBhB9?u`R3mZL^>H-zhj6osMG=r@eGC3R z*-2|uvO#6hZZLi(2HWIAvF@i9BWbc0loveVOk(lm&@wsZ-Yrc~;kt;G+eEQxQZEgj z8iQw6=s;g=7*zcCkA4$-52B^hp`u=%-qCtRq-@_pvQP!_am%GWkt;#TWin&GE(K#| z{KmlcQe1NQKH9&Q#+T7)Xwy_k-)%Vrp4J=KAk%kvYwAS0)and2OGz_{X7M zPmQe_d{0zQOkyKfCIHs?(F*;EWJ$t3{&g1#TI~Cc=X#h2_FE8D*6!ka72gEW?d7z# zbOvsy+w23TISk8XxxyoPL zqW~Y3xPI*C2u!uiMvWEGR?X$|{NfIdJvDzSqZ06zx&;3tGrR|J&D8T)pc95Y!}&yb z!9m`s&qlC1_&z@4EKIwEN_g&j+aY118w|a+0pD5I@aMe@SnKf-vb#59N1*}7qY`At z_Z>p|_BT&U^eQF`3|N)?Eken;i?}?O7P+202do_K(#yhf%!Pkh*!RR6Z)v5Yu>2u9 zHX4O*xsG7)_BCi_`-?I9g$wbNzC>gPqVVw_ZV%`x&cqz3pwR~lVW?&&ihZYWJS-E= z?3>1A0vEuWDQD^1M_X9)&Cz&iE!Y2y-$urhlX>UoDKUL#dO=pff%ks#5AGb|3}(+? z(Jir6q^tQBE^>W^K}TNDQ$lAkvwa<$E0kbcpRQ#U+j`+`Rs*d!OrULbV$d^p7JJ1i z3PeIrL1^uKCb+c`_txeUpD<}`yY9)q_Hzcxeh>yTZkN7Qz#c_%%4t{e6SBE=3=XZA zfCtM=m;z^4__r?+B2TU$iRCxQ6cG`ex??xm7a!$KyQ6^ta!*lZX9ca5+{Fwxcf;B? zTkq~UBPm^{Ol6uzjCA6clEdoz%&@^TEy(t{R08F z8>cdbo^CEm$ zEDa;dop9umGq3pCZPeNP3)RNcaFM4iXV`8=+o^IinM?;EHHV7r}aeP&o6E-zDx^bbNTdI5mJ2)i92jZ z$q~!-Xzg>-~uNUSAY3eGWAR^&oKk8d?2CocY?&4ZB%U zR(N29v`1XU^Kqrz{c2hj9OH~}nU8U#)r;8n6p-lAGq92xu_?JJw5DbaxE|7F-HS7b zS&1j^tdAxlQxk|q&1By6mv68|U>4)`WeeU4o=X=mh=MF;2xqkvz<*BXAg6#2uIUVG zuHFoO$*Xa$@iY3m{xm(lErwTUS`UYhiZauqvxubTbhaw91Xs^;fOXd+u&BnKQL4C4 z$9~QS-*4-%UuQmUkOP6klf$2eJlqpq3M*A)*T9jhzo4Fi+=OJ4Ex<{Af zCqS>^DtP`V2&}Ij;rLoC-gBtJCIwNXr$o?e!#T7aFrrF(I_b@>d1#^%0flSTaqT2Q z?hL}QEEX5w&&G>fFSil}9S(zFf+^gZ^^oJ=mZAD-K6Z|+!S~aT6Vsgq{4ybJve`3+ z2AV~Hs65w)PUn_vh7#1({xT+ochYqi25FDx7Vcc!1)|vuB#C@RCx>XVrKgDN5fp>H z?0NX}>J~Z=n2;++1)1tLQO4=}THLnr98^8wIA10lx8hkmR)==eX)n$Z?J2|5Hb}Q zVC=iU(OyFp;$o3%72tlGR2a-gr<{B+@cTrvW`4z*uG=(Wnm>2PPb6C%K9Jxw<&gi; z2NS0Fq3`F{#Cw|p1b!Gp_Z7zEn#xjI=;91>D-IDKrNfLHcV_$Z#vTI?H{sBNrPzAQ z4{R-Fkldvlmn2)AnpjVUSgr@MV4EuYIKmW!BImG^&4ig_{z1e&)`jkxuELMjRl%aq zVr<2a8j>2EOCx`^pwlj8=K8CXl?|pnI6tulq38|lRBYzY?>>hDr`7P-PG1oESBe!H zSIBoEM~n(o0jH=O*dl9$>qg(x=Q%&9TK+QZ_Be-$KQ~~L>N%?P%!9^AG-0;TT~Iit z#+LZx&}WTT@%xKhQu)IYK1Chl8**6;`x^?(q})`jW+w7HZ`Bg132o4>9}fDv>UpP7 z)~foKSe389MgGVsL9%-&o0uQy122>7B=dG2zJDfzmt0u>ZjE|;6fexWnz}%B*J(bP zJOVy1(`jhzVbGublP)ewhg2Ia_RQ>jDxh*3l{~xAVTuI4n4X`>p5;yo|njKOrqk4xp^5 z1dLBCq(6TOu}Vw6kgO?1R?;5}Q6TO-JgC&bPm4rYTi6GygmX~DNQ}L&Y)t!J9Rru6 zU%B@|20YNX1<4&(>77FgtlC~b{Bbi4x+`Bni^WyyZ+wc(zdr-td0xV=bsMbirRdS| zy}oct^9rt0GlTgxPK^G}*=XI>PygLk<}aw$qUUq(@!T){<|XY)wS%9acjDE)x-Q_A)mQDh0*QhK#^f z2BjUYQSqJ2h*$buczSCUPJQ^0bbr2xzO}7w9am6i5=3av=yU0M0Je()RPRFY_^?E+uaXHI5 zSWW_#+3>%ahvEIzD=G?q{lnt3ihPd}?k;swm`S&8CfA&gVo#hS8pz5r>$c^Adc_#_ zYOEyX|FUVI-!XjScLWpuMiRL{vfS=y3lc3E&{fhR`=1VgjPzsJMebuT+$A$UPQkAa z_Cr|EB2wC34>K=ypmD7tQ(I?%U(fZTrg|8zlHEb;trVEqb&Bx)N2isZh8|Q#{($9s z6j9lEGSm999xGQ)MPlPfIti-YR&(d>& zqa^cJE|^SnrZJ&PyaFFl%=W&GMFK|=u5&Di4-0X^z6W$U5hA-q-cip_T~L=23#^e0 zM2#jx)>kew@uM17>T9!!q7$j_^bwxs-c+LIAPEBpxZbgx5f-Yh;7K)arrNpfP*!h` z3i1|EX`c<=gIyI&b1;NHy2<N&)C+Osn1cO4aAhFMl zOrsiD6ElL{TyGd>n=^WO#aLgh!;}Wr!R`e|kTzwZtM?^*U%ML;BIYuNXTxyXnQY#L zt7W7tl?ayGO3 zdMQK+Jmw9iHsNk1b%x%PWWL)D!QY){cr#OCtE!yN6ECAG_!t+4#cqZC%FlO!@!SXd z>)v6o3ueKWNsKWwA3kacz$SG^{ONuLl~p)% zao8LB=~+Ca?>>%Mp$U~^wFjX3Wh1HE_mrle*vHR_IF2Iwc(i5v1Dd?X47#{kj(5^i z^zp8ymSfz**d)s=x9x%luTEkAc{Ox;lMS;~rqM!7CWW0Y6b%MRzH=3+ndFQ?Rl8ui zpEzvF;`6ly%kjagaxnjQiQd$Eh&ucy^v2>i5>pz&syB!-qBahse!no+2b~0?j;?U- z_dyU7TFnG6PRHRXIn?q1FgxwnU}I`6Oxx0oLAx?QA^QLgK4}fkZ!ePb{1ljxFa`%l zl~|qaCy7>?J!!CMCM~^}v5Vs}KjSIT-glRYxMmbf-|#@j@&z=lO(WIWad`7g87@ny zrMtsUquHwwlD3M=uq`E!(;#E{=~E`jFO%R&R~6x*z8$FJEy-T27lN1{cc4UD8;dWu z@|}&XaQ}>IQZZ76mjaiN2JcUhtN9KloNdF6A~RWst`W5RJ_`+hC4oYbnpLs>ADsL$ z6upmU63b3WGFtfurXO2LcQbcLrKvXl9aY9Ho2zKZ>D6G^lmn|J>MV=QjF`wZijc$2 z_kRCQ!nFM%P{i$K<@9I64*o+7JSD&XP z2JHXYN#Z=O!=LN}=(Fb)TAa9#d$)b2o4bdoe4jky9GwL99J9{HWD1yM3NvxL!ihO& z5M-_;qE_!a+GYJ1`*#Nr!3#=El9?=HG~orTxg?8=j+}uTSrY63XE_Ww^ap5hFZ?}m z1J1oUfU4UjLRp+N+xS$1{rE?hMFnvtvO5}OBHL+Q*jHkZx0PSP7NOH>W%k=FMfT}J zZN_*~C%Lfg1$id!;FQR5&Hna(-)u7r`*yw!Y$H<58# z-ih8HlVPW*3+P?flgh0^#%Sf!Jwu+{KmLs?jGtJPp;1pVJF-k$CQ>COm+2h zOm+i2H~59$-#Z94lD7!GEr^q!TeVEwIUx|; zFCWEH^B;t~It{C)a4d@x*D+a{W8sd46C3M0C_b71yO=Vl-h75wh!#RHmst-jdr7~^ z#L&vvTlh_;l!&gbCz<;*;B1Q=ZdIzlorY^rbo*_zJsCkOcsF@(_URLI8K%4DIwmzm1!6*F+sK4UDvPB}y$3V_g@v9L{jaNd5_dly3 zR);+xF3<9wo+G-8T43I@OJrHhL|lv(xOQ(pS>baEAg_b^aCXWaaXNTk#tZ7Ve8G)L z4lw7c8fjHjfiR6CSmxJ9l4I}TH}eW|ZH@)4T^NtvJyFES?-IZJZx{DnnFcfWHd_Ws zJ|`j^J2}JXHo`+O#x5(e#hSe6Irj1`>Aeg z4YqB%3`b?ftlkCACt0|VCdDq}^6GUsV?dY}O_s3!)eo?w`7CueEutEs zYzY>3U(h97Ph!EHNhn`d0fVC>@OW!JNzCg+M~@6@^*tW%eOF~x>+K`)e&h69Zl9IN zS#GZtAwpL^TL$qFo7nt$MI>fX4o**M#BFy%$?KXEaQT%g<1|8G*}`z_K0X5k8pde! zt3r4=bQTT!m}(4K2xaZ$nB+6rO?SKOkKe)X86e+3WZ?ZCa^!r0Sa19AV!vDI=b;f%EmuC;3=3b&e2ZPjczro}N2 z?RB6vQyVkK4e$ab?7eRiY*mpY>UhaR@IMh&Yu*H~_G|?e?(Az~+Xs_1qELGNC1^OM z%8bpJf+mxqAf+q=mI!{K(GCK5OWS~rHQ2)@Eea>z5{Ic;z*(%z<=Fc6%b9^^^)%qV zAEc2Pe4$fTL}m6}zRkTnobP*zbaaJyO;~B(%yiNw0Or1 zcAJqJ1e|yY877NK$l+JeZrTKIRf0%jzb=HAOoX+^4&utLcXUEj1~`=Ep=96?@pRe* zI3*LD*R-Nq>mqPd+(FaDM&QI(QPv~&CRX)Yf>Ct=aqP-KsrwgUXl@11dcQup&TYVV z>CMn>VFAh|cWH|BO1Ng4h?$S8_-=EI>3~ol4c{!vn3i0Hm$mM+wo(^AdFFtWT^aV* zh_H>-zU1_n7hgsqi#92X;->vgT%K8i^mBQ?vb*_o>a;U7)wKY`pK`3%=TGoK|9zBG zVe!GHi7+ow1Vb{{fc&%PRV(KgL4&<9?)U!(XGk0d{Fus)l$7(;Wln;LyE#)EPz{T9 z%XtA#&Zrv@Pq%vf;)Q)$1|JdY4E~SlKF8`p;FAC+F^D#MoB4o{Jz>;HMLH+P;=-v~F zLhK#P8@P(O@8co!(0XT@d6+IJAE zt&X6@=}p}1%oZ!?IjC*V$d^ROlDo`=brahwgw>{~B1{(Z;K? zzeQ&Lk-{F`2~1w}6xM%G8F%bF3w19JaZJK?vbz0jmEVyestGBi=WhjU`PE8ydLD!x z5j{BYs{&>(nh5nXtnqdo=Nj3?2Q5b}{=6@fvCX-yQlE2F9y`&BR(Zc^?T|Rm+Tg@| zUVk6rhT9-l576gu1locOG_@q-$4}n;!L@Bv?n@gkx0?vt8l|!EfjVr8RA+*P4`KD0 z0{Huga}G?Z#@wk%v@QHD4D0KW^IOt+nRh;NS&jzS#xYm)exwm6_den~Q-Q5oqYh!J zU+5`_z~>Ij(fH{Qzwli)3GnO1kVWe;>{>PHv=wFKL}eiJ{5LM+uoSFrKShr_tJ%V8 z?zc6k4&7pE=xEbt-uU=g2tD?bcm8)B|Iz%nxW;M*7;OJ+k&^W8dWVRqM|DFCpCqY3f$>Z2q~9u+Q7IWm_T!P6czXOZU1OIGDO&O16!4B-T$_fyw=cuzw;T^{ zi7dq7ed_!@1`>bBFjlDv97`*R<3xLczM>V?YrhCm2hE{gVX!KDUpCls`Me3#4tifw zD*9y(~V9e+d z_CKrE!1eZ8oCW3B87#qxSGu$db z`I!_R+mrx8#!9Ss9yinH?)ghqSTWOq%@}o&yt( zWZ`$0s~B(ag@*OJ)3xQ^)Y0$}w0SKf1B+gB|DQ5z)B6sW%#MWwjXsdGOra~4LmDVDXD3TDDhcDH%~XQx6uRSyPfDohz=FEN6kN1wHNO8X%B&6aB{Nm# zGRC{J@lw1qyj$@H{8G=tu<-;oH@FIe4t3+-x~WXj;V#knD^vWi+9zs1x1eGhjD@tAt-y{6u2zd}?AeB~bv%`!EzD!xP*nc0(CVsmDm0zF2~Dc8=u&x}?9YEq z#YI|Z$>mHO)i7g;gE@7t2sVw{vz`1H_)Z1TvSkF$Ro#ZalM^tv#FX=XFc^35 zEs3*#i7x>UY>wP z0iAwR9%AdNQHtx;%>N(@$(v`eKL6d}I`yA;naf3pyqzBU%=?TDjepViz%!UU^EN~o zo09Zd55cISAIxqUvtI?WspO-x_%}@g-=CTSd;Nc5-JC_tOp!d8hRiX$j5Dnpa1*20n{!Mxy~jW{`9n3<{O zf=eSsm{5T@%5=-Yv^Dy;OH7#k9xBAP&J)03g#;R8YX)vwj`ZKyNJISUN5r%0OhWRE_Xlnl)Xc?7Y zY{MSl3SnLRXPwM5Ru8A4O8QLhqyiL_xk%@S7tqtbMxb;(6LQTS;?pb7u)Io(iJN|t z9E_L_`yDH&U0EpS)YuM-y~l8TD4+cJs!U@~_LAVSne43}Sjl*fz?It<;EtCVw_|bO zxGyHi23??&NHo@Mm4fL@Zr~ElAG9>;1D4#`38BI!=vdasz2%>RsL4&-pE*EdJ2K(r zffZo0`WvrUKn>?7?ZE6Adl>!CMoh}x2fX{Q0K@I4gQ}!^mCg79CV%2==J~qk_)SZl zt!Zq=;zx2|zi$uKD+m?j1_q&m+{8fR|0N9U+PRkl3~bwM^ndUIp@Pb%W2=^{b;3s{ zH`J9mNdq<~(Ikx!7#We`MVL;&^W||!8+PEbqBQg~{J}Y@IY-D*b%+&SfXkX@fs^Mt z%rowWZ7TY(dyg5Ld^eAttx$l=`bwz(Vln!ftb%t2bKtkec6=;&zw+mvrMPC(WG3ct zH$7l)%DB4ysq*LD0jsH#nV_IU=q0-c=HJy|!~$NT^O72vXpjKYx(Yz%X)LjIj3pJn z*Mr$QCH%>8`y@rDGWi-dG=3X&(kUlChJBBXYLuhkV8 zRawef_bcF69>;^9l1NoWG+}d;GTCBoiFu3O<3(O9{ir6uI{ft_hpfIpMM?;f-mnw( zO!~3u;(pXEj>P{;q)A55Nz8gP&TCn}liqvJ`F4oN$o}3(H1y7b_V03Qy}zWE=Zj%h4V21wARuLm-%Y*dNjU~ATG!BPGTl58_lQg z$0NY(Mmk+JSdEQ?+&=Q97(SP;$MVoHnrRY3_vlT4WLl4_r29ys-EJ5W;_{yjy5Mwj z7&97UiS)sC-ZFC*9Ynu_@O)wRF@$l-|P>o`I~vmJo5N|m0IAa%}r| zcL<##$tqZCKtTIOxDdd??mNP??&mc+p}iNa60V_5@+sa+ej~Ja4B+X6`%rfF63;bm z2CJLx4Xj3iK?VIcSYLDnCqI4ysZz2KW_AHR9?MfhjwKzY z7C|ocdEt^Sar$R@G@gB#2EN_ajCbN8__oWC{oQAX^HnSv!+WjUxhbxyDX@=MGx`u-o|9pxA93SXJeWf7-v7ew0VjgdT_ZNH*vcwaWwa{j zpB_{xsjw>lJV5KiD>B(*j2d|tah>&W@~7u2P0QBg{@%qCvD4%jAHkUQWdeQLCdAG- zpa}srg5WFhi?6No2wRSxMblps8KqG!1AMFvOEpq?jpNh6DI=sR=b0xf8F(GL4@`sP zqo?8HEC+UF+c=e1nZm<`+t6BDlm3|*i9WvbumV3&%TwJv`#qAF+h>Cfo5F}@i7qgc zHeslT5VNZJ3lVkt2|Y(;7y%;2KJcvt^^f{kAJfL)JLbUtl!}4+SPBe+RsRnf- z*Wm5y8&sr2i21Wv%&H<_IbGp>YXB9?d{Q`32Igvhha9C=(zrUG&VR55<<%;n z_|6-8F6$n4Z*W1+1S`gz>#m&TSegy>9&qY>B7U@U!zUl&vHj>~Q z_73EN(82W()*eYl3KLN^7@*A45~tld16iO(`v%2XQ?AdT!MRI4+8kM-nulb0*GaIm zCvYj)0fyRN!j~Irg#LYwEw6H*ueJboZ`w{%BP?M5?_BctuM%65$?b{WbKMWmG#ps5 zie!*Tj^Qx?6C4Jp+=Fgn)>Xk5>Us!VGKw$V5RX==JZ^r`0UN{2N$<=H9NQMal zl<(VtipJ3vwvhLDN;1)Jna6BS|Asb_ry)Sxj;JsA0JqqFTwC#qtf`3NHSAi5d1Knx zS27d++${z~MdQ#Fdzlt5vxl5QLT|blS|NguZ4_2;8Nw|SoXGgAJ67EiFUUQ|dweG+Nwz)w46Ix91zCqV^fAY0 zI2YZI|4DL=_R$(DviTb1ZFxhtZ#;%0oR_u@7veM;f@x=-V0D8F1b?XGt1HZ5lR9e9 z-FhjnZm*ujZ69?MT-XM&FTeA{Q=ifPh2| z@3EQ{+I-VtgNqyir&mz5ZQIbbtPcXJzrcm}*>s0KpMGzC3wJI{vkfu-!2yn4yz58< zJiMC*@%M+Z_Qwr8z|A_c*EW!B(-#nBJ&twGr_e{k5w=K0pyI$Z*u886m}qaq%7p85 zSAsR^8s81yBLC1n!&Kh)>QcBweqj4gC#vtBj&ga*G-$FYQ~N8H{ym*V`@ODGlfPx) z`rv()r`>g8O*7HzOD60%vw%#!WW)+ROaZB(A!_c~PJLo;!``_G7?zh0p|;cD_exPZ zOm#r*Pd3+)nvTgg{$mo~|3F5Ng+&vdpv_qWRN9b;FK1kUORqNI6(4VqpK=%aY@Be> zK1JHA1z1#;O#F|(N1GU7P!y~or&rZcjc=zQ7D=O*D&C`}&!ri;&d2ZfO^g%wy%Q0HO6savG8x?L~wr3iVeLe|B zN;YDRk^-^#z~ureyP&J+2^o7igAFV$0ON#qtd;NqS-6ng{VWc5LPl^;)f84Q ziBGd%aqg6i$KWaNNj=PM*apWP@W?;p1Y0Sl&<6*XgLHrvkyT?;1T$GFBw)S ztHZ|i0{os#W0?A?j7l99XPguEgSAOK&5JUC9d^~EGQ9@n_sqg+3gu|wTR;KoBMF(9A z@c0#oQ}-+J8%;#f{@h{QlrROF4$mQtHk09pNdocm7e*D8ROoK2rGXVsh?#LQj_jKN zHOjl#rE7#4g{x&$gfGK0va1Kl<9e96TZgUDwqU&7LQ&1&C{-GkWbglHLeCgq$I3qg zQ0ys)PV0Yx>xX>E`(Od;X=A+T#4q&tSPK1qv6t!y4WSI~#f-U+K=ZRO+c#2h`H+Q%+l7X#7u7->VC;n;I%^Ny+cqI|BrmX5)QgD zhjCg)0ji(NgQA@0=zGE$4hga7E%6u=LavbKVRq=cel3LB*AQ7FeP~`039E$egO<)S zP+OUT;ytlIA8kV2!A0PIaWa{Ckz;;uJ_4Q|5->e}9S$}AgTSn}_)hs6j1Kv6IjG4P zm6Ql&drpAKpfI={pM`h!IWc!8capR#Pw7H-57T0qj@O)b@fIIn0tPLk^wO9hJNn@; zCbb(e1wZ>NE8b6H%xcoppD%FUl+a4?~(xFMZqwq4T9D`OlFIv2#7% z-*_Jca-_g1+Z=4SSYjfVlM1$U$KVzE>^0dlSk7_SbfJ+<6VYaKLq#~Acoe9z!p!fW zO8yYnbDjC`6d{jg(K}-~CY@$MDOv=7Hy*{lP05_2rH@yy^pdw=MidEfH{g6irf{3{ z8i(&(z=Uym{gl;nP`v3K{k|*{A@}oi+n&+qBX|Z2)7mDD4nX~c7_z99#b&dON zSc(T0#i9WWV6p30P-Zp2f06<#opl33%7wtbI}m-eeaQ33$3%0g8Mn*QX3or+#(a*p z#n?>}Y;Lk4b4E#y<`tx&VZR2WM-oY2v?nzeX~wm4`*|||I97XR0hRU?WRpfY7WK7E za<9084jc8+WqaLFUP6)env=@grYZtSK3%wKtu8yBvkCk&ROz~pmqGk;1Qy@2#156q zxcSLQrPA^#@ZHg$+HO&0Zf-pa0c$!*SI-hIyPAb!S1R$uYmS{_MiKi%;pyFDm}n6T z)1N0pqS;2eck)Vny=4mCy8R8d96X7`=J!!-Fb$?1t|5kfq7W&$2@bd`k{>1V*qCfc zn35lGC(H{ylHKvi%UJL$<(M0*9&%igDn7eema#Qxh5nMyL~ip!x@ugI?Z_5^O?ubx z+|P~7x+jr1@q{>gto%EDGj$)nUTBWPKKsx*b{b*s3PPyK6n3;B2RBx>!L&DGoUdF7 zeb<+w>^yxEcJDM@a-|>M)>}c{W1s|x1;{qPZr6I3vwUV=jp-G4z=&bR6IumlqjEZr^544E$ zO6+pOZmPYs7ALd@P$&PZIHcl$eUBGl#o_7vm?KLuIp8b)p2otM&S$>Y%)OvlJde4R zTSejgxqs=EN!oB>z6rYg z*+v#TJ3$kUzoG4y+^rl(Z_p=dk$COgGpK%jgwp+?l&$n2!RwDf=2$NNt4xK+A_Lr3 zx({14T=DTKKIYyTCg0L3K|D^6Ied2tzBfFIm6n`$v$_RzjvKJsy%g9TiCV0#@;cU~ zS&RvHN~9$Qh5WcSC3Yup-OEXXSUYkIOv4nhzekXrvowmx-JXXIXJ#=Lmlk74+5n_% zkgWRb{EOTi+F^A@Mg+~|FQBrt_ly ze)gr4_G*Dp`$uAQei3^o<_@TNNU?7Vw3(cL{usJll#a|hfQMB6LGN2(4BND&GRMXl z^7ksh7Kzi~?=?c@MkFD9hZ*}^k>h5~YO!h;z7302@*s50bV{4076g z@8%qjEesx61K9GP5Np|e6ePVW>FfOr&e$W2mS;0at(PLhpQ;Yk#=o%s9fL9D@5x8$ z2%I{86bow~V#e7s_0qqU+8D`c}C zCY#YFlIJ=YJ%&TCs4`c0!S|vWsE|{z&nRez6^y~ zRxPe02Ig8=adax*ZS6gLvM3czo=4CO$6t`>WrUKg8qCTN7uX%P44Pkd;g51vOtvWE zPjZ|M8(u}y_EA~3>`t)7I*tuFUipa>?0!qx`?g>h5=WFuWgw?>E#<&=s8)K2xLKC- zyf#!5iy>9EdYuY>$&WzGWM}e1DGPOnj$&9&6o#8_w{)AUhS#U$Q)$;%bji(YM5;iC zf4VuAY?S^=a=q?iUH=*~e!7h;_Ddk=y(K_*p*HM3ehj^%Rp7L{DBNWK_Q66_kq3O`d%1LzaF3;Um4Rg8MD~yCzU}e*b^dZXRyuBA8}sFL|)<@Ym!~o ziga&2)PL1uRonCFQS}7K6fj_Z2&IzbDov)R^ai$wo`QzPHokfY$0WVw26|`KLlrlF zE!ybIX0}`4$0AWUpt6j3e5>KkAoj!|h+~SlE$1;e<}$0)d}02V&k)7AqrYA%L``wd zN62p_CS9DTq2?Zyf6R5ywKAaR!LO><51gTS$4e6Ym&@_0o`MxmqHyN)W_tW~5_Bxe zqH=2!$lZ8OIF(v3W**6^hbp`)H9T ziKmPU!F&5eX0%8iO9pG`YqlA5Z~5?4cd~e4HRo$|vtXBrTm|Kgu6%j9QyA*99*;Ob z#z2V-D*mPoJYNl34u^jw-J0C4$6prjZGhq5oz$#}R8xlVa`>wh3i<7%ooO^yZ@7n%Aw$Rz zGK35vLnyq@y_+YbM3YDwD5df*jZ$VIk&Q8 zmBVXvP2{qz3o+nX8%eg?4=0yhz>fop%tu*uJ=Qpy| ze*)W+e2Av@&BQfXM{V*ZOk^rAo#&iY4drghX82J%f>(1vigU+`kls>vOvy^3mC^-t zW#A*4ecGNU{kR*qaCdF#)JOCj?1ACSpU7b=Y1Zyo6*z?~#&_@HF!Ldo9s6pGf~DI* zX-OLXjH)KOa<5SRwlnC?oQ6WTETBqS5c1kR>0IVEfBm9v)bEtSBKAA^SSf||r&D15 zXcZ*sX47(cA>8R5#!IfQ!$FPdBw3k&TEIBhsVL^2zbkQxVZ2R%+gH#u+Rqm=vBR?A zt*AAZWz4TiWz@zjGG>07FTsVfVqKgW|R%pZBsMLo1d+1L-x^&!YF`5{wVO&i}h< z0wbt>7 z_T$b&ScsCA6B)Y`p>Q<83uf0p#;9&%R8y(M^(|Kj&4~m%fqd|)^`bj2=kX%Ex5H2) z1Ckui*Rbp_XCC{t1datg z2v7I#M^OPSJkL2vLStKLibyOLeRaT&13obA8prRRaGbO(-;MG^p=hF^iw8L_+9suF zfUX0ubZ`=0Y}$u*L26Wte~lIzh(p)P9I*6lfNjfVpvQV6#Oghz2j2dHhnwt(pY^sv*lQU^HO2KH4`#=NQHSFT!w>x*W+ywj^o`w3}J&ah=$uGGz+-~ldmbF^y5V+ z)D{F8Rr$P_RmmtH{th0zn*k17UQ$>1ICO+_@1p0FD0sdE##feqy!txl!xN)Mr359z zqj2+SabCKK2KcPq&-!*O1a((ITx#z_@^i02sm=qUFOUv1dW27>D zExA6j0j!%TZhtR@?$%A@r}b{`J(~!V2E%ZC?rJm`)S|gw*XWVexzs*5mlog1#;?#y zHZ(7W!*B1v4(Vy|#XyGmsj0K;G76yIZ!`AFh~i%_Z2-3&(7j_jSSU8*q4VO5&A>}^ zQ*NY>*N8D2R<5VwB9~}>V`%y3ThmDFWh>^=10iPSGD+5Un=~x!(t7qZB+$?O>BTZ_l@ZPbu~?$dJp8|ilKk~Nxa~dg@0OOVK`v~ zfBfX~kjLs^RaGfwWJ|C~)&IadVKqd2e8C?T?ZUYIeAtmS&h<5>LY@A7(35zJj+KG@ z#?PDw%8zrqn%m)=N2%C;Zx`O4EY8h#POw+@{v$f_hOG7Q57P2%4lzD$hd(`88a+@% z4{DSFqn=6>pLBo?ch?MSt)uFdW=w~t7CU*D7EDu5raELMt6w^qS?&{rrZ|W%vt{v) z)(yC)7D-K-6q#QOgK_`HHuCjpIRtD2CT>wUiip=jQ&c8zz&Z$LMytby?{7Hg<~dkD z8VlL^8F1pnA*#FoGpG&-vGRj6$!-G~w%?%%_fJv-qj5EOSbrD2{>_H&-uYk~97+~1 zRbtH^`k=$xP}nLXhIUWx!}j=C=+lyA8aWPtepUk1UaBSqrLQ1PL5vx%JBmV&4wH^U z3AiUH44m4fsRGx@NKw&+oo@rt_8h=6RdGzVuPDD+#LaEZzLjV1*0IW1xSTZZQ{rvN zpN%%La%_513wq5hrkXytaQ6}YavL;d{m1r_f$kX)|8D``J6N1480vz`Xgwlu;v$?l z6o}_ey}{Nc3*cLxB(vq_Rn*xb!1%bQg9yheNeT_+Z743s!k%`3Plv4QUqo@wq$pms zau)Q)Pl4&P_7d@yXn35Tj&?Fnc}4S|k#4Uykh&lrc5AGF>HIU~Na+$>bMh_!@5Kv5 zEHMmz+;c^zn+x!%MmFF0Ujdw6^NrtS+Cn})lVk-;qe1ZcU39HjL!BQVg{1{+(Kl8b z`^K(fL(Xq>pTc>}bQdyt$IQTRiw&eKuBW=2pWqeFjlL`N8*Z1K1S1pe;5D~ziO4y{ z&)N}-;u}^ln`BqQ29U--Zfb06^8%PxoetTlHW2wThb-Edg(`;^K&#k8;9Cd5#B9QMV6t)--n+FL>*b?~Z1`k$<-}N0wc|Msy|cuU&;+QN>Agg`+( z?0D#pro(Gt;8F!hao^|fm(t1YXFs9s_Xus9JK3hf&yeVr>eHS`fTc$+K#qDljj4-( z-3G<{Y_Z$Oh6pf;eIdM-lUd~Tg&mm4G4lqM%5b*R2#ghJ!eDJN3RpSY*vY*m%J0`v zqw6jZ+cq8Ey!gkTo8&|uEt&?WUdyoTy?Y=%untb-UV*RwK4MV5CpHd#!9{=lcvab- zQ90EBo4YOG^;C}i&AOqH@Csg$t+jgC}@n z8^j8pCC`6yXF#YYJyF3hYMz5KXMU1%w{>XAEMZ9N-ooIFD z*7|qRg&)o6!fhq|nQzod=hutaIYXXx)ttz*{JTWbx`z3yj_G6I?KV6U6GE;0D)_yT zuYuj#5BVVS(a*xg|Mr-21^__SP#1wW7^cs_(3%V%rgb3 z{4GCNn{fe-MrYXsUzb9!BMa%ifg$2OPXabc{s1}i81Br==khRfeWioEC)Psiz_J%QWV6i#%JpVLCS2M%o-JIR&9}Fe=}H2WIbr9W$?k zcHu!{;XTAkm;EM-pyueCBidA?d%;Y6$kunU1L)TQ;#iOFkMA=hleB}!L?WhYvU$x;zNG<1B zn?zh{rJ!-KF}rSxESMiM0F@?Xw(|LQXe*LqKYJ|X)m^y{i#eYnF};bKoL<78oYjQ> zJcBnq4A|TdSyuB;3hrLg#s9PQBf40RVa0@`a-oY7OyF=GkySgzo1ij6E3R76@h>$X za>5k;mBzy`H+%CvAjSC%ztXQ4PQ#rzfpTIcL9T!;em>Jca-ZD6@ZFz?c3~bmhjj6t z8)|^WoJ%xZe+h1)fQLd9K<-H%7SA*1IzIcbU$qfrrw^f>sSoVBR6_zK@7idpbimKe zEAfGV9@fo`A%_n&k$ncOD5d!iwiw)^Z(1fX&yr&~p2>7jH|WBi@%4y~&QSkBgV?C3 zvMOFIHVkB8MCo+0#-0IHfio~mD;1i5?uA)<)1f>shc4py;TtkG;Oiyb+;9sH;@!ASxOnI%%(o?$~bM$M^!U5{AMciusc*i4@6QK;Nv{nmzj}iUd6^h2bejkVa^K0jw_pu- zFR|Mw!v>`D7zn%tev`F8%Feg6qCkR)zwC!EV_!hiJxjLlt|8*wX1tS{PF6-fAVOUJ z(`(y3{(IwV#MkB^wfqaWP-dsX0|*@1$z5Q&uX0&A2NBwt&|g6=oo?itZUT#y@&270L2qmkbxfi0o<9+l1ikTt)0|9gor^K{~RF4 z-lDA>Vx7QZ91ag6TLXV{Odfx}_St+8nRpK#?$cwxar18T89O;nMk~3HGXaDJgrIt0 z5SM?Cz=rQD@ru>}WNi=L6(102GN8;yy_PQf3{7fi~H&!;S$GN`MVB1Hd{eAcV8)va$_88f7?87 z{(|{q61-u_Yw&G~H~A3ajX@5Fz;g7Jjpd`W<@4LaL6P$UO<8>dZ#yV*^9(<7$}5Sw zS6sqnH4_=f;)A5*%uCE0<}trR`*t<3x>{at1sJezYSn#pO zLK63r{RA!CQJZ6rx)odCyefIHupT8A(+_~@7kxIg<__lC6~Wpl!fY723;k>Bd20Df zaJKwX^!D-P3tOE5&kQc3an+S~+eL+hnr*~$?sZUhHvsl^rO}|4UfQE$jm6#>*m*G$ zGaMPPTu_78v_;^3mm;hDVK3(WC&$PpNHf~E3U!E922cBJ8lS3Lby@j5@BCMS<-P zVASs#=MNbt1x8_b?cfj0u%xi}F0?-u{lk_2iVaFe*K$s+!KH$h3=3?2SU z!Z}f~n8rumf%wBH;AF-IRnp{C77X>PcEfK!-;fFP&A2zzSAC9aH9bC zPl@CU={Muj#gc4MtvXyj!Dabd>Um=qLvcrAI#AUYxM=S;a9+9r)3u9WLCSevx{MQ4 z>~w>;D_S%_XF3zTGY3O|Rbc+NIuwnW4(~rvEcp7FHVmY}e|sco@{0(#{^=br-pvnW zZaacr12^-idXAfn?O{Zfj}O|Xz_)r|I?pg36Jr^hnMxzn@pnCM?>!z3diaTc?^S`= zHTq~9<%Eao^1#hoAT3Yl|Mtb6*2VXF9NW)-%kwc9|@E z)dFdrOTZxKB)BLqgW`(MoO|Rny6@wBAQMxmujMH8tl0>W)eQp!GU09Ug@>MZvF z{!5Sm57CD()}_jbJrZEeU-_V#NiOmJ;Li2yXT#i|VYsz81Uj0BA@JuRD0AOKit^v$ zDoO6e$>jx>W-CF-<)s|AKpg-4oX$vf9Y?115-J>1WJBd6NHR^r8m~6eFi-*uOD8aC z6?PDnYl|+H4iG+=40`VA=q2-k968-bjj!FngWH7J6s6y|^2J@$6YZphlf+s7h>vvX z?3-}(&j&PIu>{qYeK;Dxjcn-d$bYvs8-&p_5+x*cqJ+bErP>O+sQyk2)r>=!)44iI8w8m zvkkumnKv33bo(MkXO7dn@OUDT#)k&|24cqbIFs^}8D;Dv5lt3Uf2t_2d1(c19|Al< z44Hwboe+5A77cQ$q_*|H+M&P9&-<``$yt=oxiw#t_9@yt1xTGTEGSX z$C!G2oA`zV56)s_g;k05mm5fXE)hQG-t*YE0ISYXEN?Z!GiLMQx|}oSrB7o&`aIwz z&nm%O-6)WaT*ni9vKS(rUZJj@C|T8iA4SGYpzm-itaQ|*<7_^xzZ}VTx@85~MUhZ@ zWdhUlGz)z4d~6QeGKI0Uh*nrg5ycvUdtuqY!v4hZ*0TdfGLdj zfwSEIodXy9rjp*QdvIh{JERniQ>~Zn)PB=6u$6iNT`h>^500YYssvba)dn_SjK^Zp-CaqI(0ZC7A^o0##(-_B&ERO(TGbR$`u zd;=T!bqGEIoNsFvE12Q}t4~R>(lhphp{FSuXBP^AN_Sw_oo0|)cnQn8I5u4($1OTl zhZCJenf+?r4ax$t#4IBT=Vmhu2*4p!E_@Xr~AKoX5FhH%c?z3Y&?+mnxWI zIE_@k>cYgg3sE`w2hLvZ00+9bJz2(MG9g!p{l#ULR=#~irL+%Wi9;toUeSsHoed;& zX)kg47z|q`Wu6@lLSB}T6myDtFWe}M83&2v(I4nr~2t!^B zOS~-DbAz0h?C4s?W!EFZTV?;6IPcW@GXGzwT;ly{uz&dI857I9MD~I8?bKj=%8JSu^bc6@t8ed zVU;#6`)3TLf1^llLs(>KZ>kgLxm&}d~D##@7gvSqJ89`qirsG)cPXC zsOo|1%^<9qf1BJWVB!0HUB*?AkO*UL9v8MBR^PaYe|+`nii>Gr+#bU#HLT@aSvSz$ zCLUG}w$tb%m*K$Cw@`6l9d~zM#ZRxEK}(`dP*uO43a)a6twq9cFnb=#w!9$66W8(k z59q_BEEQIBc${aq`adxLI|21~bwlu~%P3baNE|NIfbNE`B=w;%zEgSe|>xHFvev^?tR)twi*4Q z-{j*!v1ui$2LxhP&u*}swFUchg17(=$J&WHfPGQBv1LXNvI$o}w=fLf+CRpIWkPI; z{4x+blYy<4{^oxlLa$b0l$3om9xO%h#36y$e-^mcMayOP+=I= z3_5#2sp775mfm$y(&eBBKBN!3#07RS(N zQef=If7`6>IR+~ihtLzN-{X;eS7=-JX882@EVM7{g@7PUd_AhjEO0i&xoU0r{!b3i zZSx6!THtz8+j<041Gw||+cj|U71wnvx(ij|u3&qz20t&TrPArW^r?S4nBRECU$)>E znW7g;YX5QjI?iG4&E1#Ih&bZoy1fQ~NP=2l{^Lp3@{dUMgTgED&DbL`xX(k-rxd-R?&0s3q9I&g&8TKY!A$|As zP=D9Zxu~~1Jm`GK-q7+DZg^?imMuo|D8aalny|cPd-V1#eKg7I?zS37h-Ql zq2C@gc2Sck-ahvpi(8k#gDeHmkV@y9-0LLg^*yQ1Q6sS1AV@r)8S=#BuJY_%|0ehF1(d~%=b`T7<3W1SzzO{#bgv_XK0_$@@~ zpIIp1c!h{v7H3xN>x8*kyJ6p_KcHfE641Ggs`^Yq8Rh#JzEqAeoCvIkY$L5W@(@fl z@*$KBN7K{--kMA8RH6SJdfCVD-98l1!10Mhuzn5p+DgL3@E2(3X97yMy*q%6x6Eo9$1_kweW2{V6EKISjg zK<`)4_%LJ)C1v7pOuZH>+|Q7uvSRRd8B3x(r!W;#zNGGV3^?sjr8ck)elmJE(5Zz! z{8*5SbHF^?7yPPycOh?uA((tg;oXeg4|`AkfJ6gfM!69AwNu)7J`Ef2%!d6anR|&J ztxyR^>_c#T%o;O{Dqy_98%O4xrUq#~vR6ScBlB{FUv)P2dLZqJ<-sE@z|BDRZcmZ1f zKQG|_00sjE4GU8+c&Zw^L9CP7?0<|ujugUgjbuzup29xQ(g%Y8DfaoxHBet#NIw4^ zg97V0py-^4$2T@%P`fR>eC`91lLTSn-9WPc<|E$R978rMXN;&MTC-aRY|%VTo6#vM zfvWqzZ04O)gmtU*;LWW z>{vh1Vg7!-G>q4{iUkaSe9k}YGr59#t1|dw=@p>Y2Fz#o%lLX~35qu8GxcvKu^-l` zupVoZ`JYcMrd!oBA?{lUc4~JMzepabS4qYt+_pMuZ!VIV|KUr#PkUY!LT8&6W3wn2 z_DkxpRie{SW_mllw|5H7mR?P3ml%<^*W94_oB``%;7LelKG}2hO4*HK&Vso0BczoM z;Zx4IEVuj(o@s1BBljQF;Xxg*Akz<5+Y7J(F2N|L5rjs33ovPY3(@Lzka<-S4~&YU z!|fHAf6f$`?N$87;JM7L`xo)|t0YY6)do42Z@hLnMCW3^{S>G@JInLU*>Zm;!r;aoWXSFx6%#pzDihaJ zwPIa%U&&eWn6r~A1ik{}J|Jz~vdk`bIe6_AL~Kf@P$lDWs@Sp@Qr3=_tBn1_C*|Ds z$77T(lQP8QC!En~BDa~gp9pT3OUUrOJs=Y|k)1Y}%29w{k+mt?;G9kb-{y1$NwGZz zPcDCf1^s-wvg8g}u2>B{?}JGDZNgbkt86lyqv_}3c%G`{7N%uJ8Q#Aa3g&T{F!cL3 zuFCs|mnX||r|?`9iQdRxFxX4Y#$Vw;Of=s`&kJ%G4JP=pHGUna#+zb>L}cG7Zi;Bj z2uc><&#Vh*CEbh>H&vMh8iUwjxq@wVok3n4z5uP=mtbH0a#nbDC0=`Y8Wuii!o2Qd zm^WvX3WmkAxHP%cLD>H;0s5l_ znTo@Wxcl@2I(NS*${IQVNmyL2o7+ShI`7ltT&lk|{yfjH)|PI`JI~Q+9utlGuRvb0 z4qc{<^6E_M=#jP@5|(%a&bOn%D2CE%&& z$1ZLC51GJcbXVv>tm=w^UH8wH37r;T1lzd{Meu&)|CPe{Q#Vi}Mv#pjSWitqU#CGS zd@4Bbh-Ck%!d#PVShi|1p+0pey^Tv)aF$D|+|PHjQN*~KL^|PM7SCyUG zsGb)e0y!_k>bk{aW$y?{v$zG@`?$&7$w^$RUJQaxZ37w}g7L5J5WVNFY&iF(3E+N~ zdG|eTs~LnFi*E79)y(01_YGp>Ccs(>5G;NX$8X@a{@Z87k}bWr$k)41Y!v?4Lb6#A zNaR<*@qlRz!x@!gRb4SkTLDk(jKEcO%2?%D26*Wzd0#e>*>K}M4xOKYs&ljPZiEVI z>G0_}l85(7`=HO(0eke;F&CDGur*!zkl4&M)k|)2o7vmAq}+xLyP*i)GZ#<9642;U6 zal}Q69r65UGqx)oeoRS+xSM@tsanD)G%y)tW*Korgr87WLLquqeW_hp9O{M%vM${A zM$Sr&=J$Pu=3nLLW;TZ^JrlvBQH$u$=Zn~%M$!BSLRYY5x+3$Qq15t`D_=9IoOF9u z!^pB+{BxXu(3>r!>`4Pa~WE&q8WTYKIhA?oJm9ze{+`3Cz!Ku5q3&$f}t%< z%TNeWg)Z1{pG&}2OuL8!dT+?^csOc%Xkgi%VKRJYIa-7%F_F_; zxrE6wC@YxEXno=ARBP8@ovk`qbGBfP?F@BRn7ORWH!jei$|sdgCfBG3b>zPeKFE!(;f&Z4@M#!z*?}_%<`tPCZLCngVEf zy$d_yeU~x<+O*(cC@Kj*h0;Gy;B0*=1XKjT(&^d29J>e^TOYwB(-n~NS_>1CHla`V zZ){dx44)<*r)3ZAn4@BWc)orQ)(6j`ilKAZ6PkCSr@9$?S1hG*@#}zm?8X+EQCcLJ zQ+DOMD%P&J4bEG#;ikzdp3`AVR5gsFWoKux4qU2I_--XBxmiFJRVCS{f8wClO_JUA z=NHNz)+d_pH^5`hy|hYrD-<0vz;qV^;>T)1-teFn7hN*U$#(ax`sB~m`_`& z7TZ^C0+YFIj92_LhU}a`ta|1!rl-P@D9Xb5rJ?loa5F8DDJ3`DOtI!C3mcN#=)0>5 zU=uflH|A7fSseEr%5%ZvUUxAt?kZLPSO~2}W0MC7 zAx|7!CmO@ZQZwdUn;D~at_xkVmP5KFM-gzmhTC=B$augiTxjBe>xBzJy6+mwI2VI$ z)e|iDXa%u00eXi!>kQ15LHlHNR_}um+&j4xQp60|F)n#ld3pfmAAAc(=Pw5HCEPPg zB^;LBn-8BnI2!23^YB6WEFLVSX4L?W~Oo_nAFvgMW_gU?ScB-rqygZJ!W- z$#m{ECJWssR^Sy6RqQBi!P&`^(Ba5MSTS}9b|^_OmEvdU>yYaNcD$vE2e#4sSv!%7 z>flO_=H#K7&!vUtLZi`bBxe&)+1UuMeL0GL9_sK@djUszpNPA2)VS)IGZV3N0sXBtZmjQR2Yf? z&1zSYX^=|{ETZWeq4zcssee&aXpm~Doj?_itW_|w7C!!$M9keMa%a^fdTsYxyuN)E zyp$0@>wDSc^!aIQPs;*pJ0FB~of({2!2-73X*lx<9X8|< zfz^|lPl_W{T&jRnX*Ti=!oCyhCZtW)MQ}jf9Sro$*qdE>R^pO{<=bk`kszH9VBl{G zImL5H?#KvyH@5;Uk8udAd}DKgjVFbB6kw~TH1lTr5qv0p6ox!4AjRDZ8z%{~HB(x+ z_W5#5EltP3fBukYA&Yh=>!Gz*kkPJqOr`{hL$yOTR;<~CD#|h7c&`BsGE&jS^BL|* zn~SyTCS=Q`e0bRxiCdmsg%47yP_aOPse1R6=1krX2mNJ0C{P-o&##8f`x1!PzhPWt zxEm~j+o?Ipgx0zPSZB{Q=l92v{yFMwaMU&8KjSf3nDh;@)vkc0q%t-)-UHVyq3F3t z5BJaJG1;Xzpj;=yMr|~X=cQ4Dr41tZZSxntx`zZrUgHw(xycx%W6C9{s!8Il$yjiE z7nv*I0Xu$dgYYh8F5$PA)c72Ofq8;7!R;N9th+^x+se^Np$tmJcLLXNgQ0?0xUeS% z?^GzWF8-TQ?mm~fw8)3Zmw;3-&eIy{4zzLD2Nj;z$?%{ACQS3A(icjx!84txURHzH zJI`Q}w>Ai@7AHrG5@6rE0J>%C7`)y_15Y#*>)tO}Zp)!zwtA1F-(cJB)`-nEtl{Yt$s5c`l#)#M|hyH@-ZcugP{l zQe&J}%TR-dkyHW?Va&x_xX+7^3X^0RQxwF7u5akmxej#81W#C=5D(8QR>HuP4RCk1 zILNGX0?ktyP@RLYJ}DB`wGg2roj0w%_Ja&Ztd&XWSvs#TU7X6GvKNN9kU=Xd6bw;x21*peLvghw4qel5c zD3U!x@7z0&A78yBIgVjCdS14CWVQf$3GJdL<0s+IrW<_KKu2h)IfEVtS923t8RmU< z4>lJpAm~wyuG$LB+d#&8zKbJXIr|mn|4qd8xerNeSUmn-&G8uHWuRPZ0;@UNOg7!A z0&N*@tRDGCWwzPCdu~(LvF4o3=(=j&e9JkoU!oNI9;b69;xOWIeI=^oXM^(bQYdpP zAWrI!%8NE|Hs9aBK=NH5Rs86Kf?syR@U%4S-n|I7olOA8)dt)dLV%_&i>Ccot#~TU zc3`wjmS=jM`&{i2pru(qc@=WjOvOwV)JheZVlD|6@pm2+t+GRxP&@GWl8H;CbjVik z?6RTYL>hnJ07hTx!`rbxlqa1|xA$tYX8*3!r|)X%VPkHpIBhLsg-8^XNUkzfL2X?U}@rfml3Y zFU&k&^q7d>pU%i94AQ~oM>PHUY*u{VEev>ngcwPM;fME`Sn>27b>-|MNnD#j+dB_4 z!~c?}GQ~C}31^^k_6{ss!X=OH&IO@tZ@TJsI~~#=LnC`5cwsOLe^hOv$MxR9R(W^c z%@rn$Nw5K}+-yrWJ0(EcpeHPy=)()SqR5C?ZYMfx2>GDAkA&X0qr%1W=*NdjOrh91 zR@Xd-?!8jXcbdP8sv7d4znD|`>A-;DeO7DhO7QYdMK*Q*KlrH>%x_nYr<-<-VPtFyzFuF=nUZTrRJajZ z)OL}bf--pG{5O8(zo`)PBLfy%^64<044JBetoc!WP&(EL<~y7CGRfBE3-3zMq8H(0 zy**3q7ML)at98*pV-b7w>>WJNf)E@#A1>a=$6NoUqTXI@8Z(nyZbVq2)1QgV^Z8Hk zTk}s6ysHW=+XrCD?+P*|6^@acKEa-8T&t~R0(*Ie5{8|3hu0A=$?4ijc=(ezYtIoX zm%Qu5DQ`rXtki!b@uL)+xX%NBhe~p8y%(Op>p|?ew3UBWZ29x2ggc)dfM_Wvh$)a@ z^#x90^_^q9pTS|o@{<)W+xIn|__P=YmJfi?>pR3^NikGZ??jD@`4}(HL#g}wm{b!h zrcI&^z0_SG?QApc(>)14Y=2?=iHV4p7PFo9O=zyQ6_xsz(5_lDYF@1fk{fMsddVTM zKD!wQB)_4x##cDGB#Ecy{hMD#I`P51J2veFGoUC<74`#?oL;H#_geec9GuntL zP+N<_4*=7%&qBs+KG%x*i4WLmSikfEKlnj08moxXvcKmrwEQgy3LT_2r!|@9H$-UN za$CI8nT(EVH_IpOH^Y11H8EqI712#IhhCElqGSp(ALcJY#$cACYqCL>P zebXPOsw&KivUKDtWxTjM>CzQ6D`N zZb};~^65G`9d<>_CB90BH|?4i4cixnqTCxz2=s}Dz*k-5>#XD8JSL4=AMcaG9s;Yb zi*w|bU3mIz3bwraMHft|r(xeEFugm2G$vJnT}TAH%}=&|E*}kBNSDn#LhG=$y3ultJ8nI8@-cjaKg?={3b_ZjSKKLqOE?vMgiOa6ZE?*zgY6INRh z7hZBAUE9uM%%M0i*}a0ut@{W+HMG%Bf!p%^Q$+25|A=gPAO?P445DZ2Ft^{5JY+2) z@L4oRVz)x;6Oy<&eIny}p1}I4M<5_H39S<9uscSC?HZYdvWZ*JT5ADQ-f*4R{*5H{ z9A!q>E1gI@I*h7=NmyAB0EZ*qV&g{#HjqX`jL2j*eMT5AoN^T{f0RH|@=OqK(*cHZ zWa{L67B0DLA`iAkCc;CbZ2Wkul*ArBiOYv1n2xkKV%M7un`g_h_MrmI^^i}fU-=*82n6x; zq(NGSNaiU^V_L95P<>AEHIyyQwOoel~W?|W5 zT9NjEAKQ?JZyki0!UqS~@XuN(;PZ>HPqvbkF5B3J3e(6?y$-#&!Ukk!f5+-~JP;gB z#6LCte5ndRx+fLSh4I_?^guy1d5I@Gv0ke!3 zbmq7c;CoAKcCdw>cad;qK@+S|y8{OLf5=1SP3+;L>zR~8HRS6;&dQvU0KR*wAz*1K zkvMn*jPyIWNo_n-wefI?+&XOcSj^^#?LzAn$(;GHffj9QgxBR-sPK>@MIP^_G6o}L zc>a9oTe}&_a3FE2dqExMTEXjsG3aN(%}DGIp|n#dOwM7egz9BcE;nN||I2gtA z{B;Z`RRNaJ5#n=V4bQH1C&;JzP#3))B=-l!{d-Q+>qTQU?&UHJI_dz2XX?|0tv1v( zq?xo{O6SeL?@m^4;_StrAAz9QCFnV)iW8>xQ4b+?I{%9am&yqv*(FcWqy7vOL}z2A zRsnsQtA!K54?Uz8W8-{fct3jw8}@e^20Y2683(q*!To@1+zP;9ZX~YrQ)X{m1g%5WpeWbJ^W1cq?h5DVIx*&KPumRi9Xbb1dM$jliNWCRbeleo znoH|gcho49#Ra=pbN5GaCUE~C*}6>t%)-8L4WR<8d>+M5xx~>ACs^RMP{K@R)Jf9{ zeeCw-KGRcPlN5zw_&Bu=t#e!<-TMMM{!N5s$+Aq|fF5*Ft~D*c8}Ix4CTn-ihU_KL zIJ9Wy|4?o;8A2pf%8*Jj>}Tx;Y0#XKB$7yz@~43`AQCc!NQRIhDTKm# z))o>!DTzudB`HOvq*S7JzwfvAlkd(sdq2-w_kCTJz3*api~VIXFzmpe!<2AbzXf}u zW;!ffYm7bFO6;@K!d&;X6p($Zh=bWHi67?SbdeDh&E%{^|LI~^#CQl%I8P>u>9Xst zT!jYn5zKpW8I7aVk?nAVni*xxhh_hCGU`G4{QMp2#Xe=g3=Stkq*8=Od`z8%}_`<>>k;^4!QNca-9k>96(qb=p* zSdU-n;JG#nOuOR2a!eUK;5{vz(t*pG9np0C4VnkF)FR9YC-Co4vP25=&FV4P#gP5A z__cuOxgeeI$UHStL!}?p*daED*fp*uFQ(a|X1qA(X2~Zq`IOy4qj0D`Y6@S3mATrV zr{K-jK1l1zA*0upLeqgW0_$&TILz#X;)-6zR_`P+9n>aoQu%k7o;SI*^eXs0c7(L? zQ}Dn%0o^{oTp+I_0-tk2N!Y*=h!Yp%_S!StQ)Pa?Qtgkcy(go8k{irZzsS$xF2jr0 zCqdcn9DF{5xbQ4O$E{Kb{y9Xke1HsIi$a+X45s+Val>20(COzL!rm9hvnK+{O=xHG z8t$Wm#51N!GabKYCxW~F2EmAJ52~h|1iyLVRFvNj?mXiJ>U`4rjfWO}kXZmnZfBw7 zBME%=DhpTJt%9DHw^0GDxzCexX-~-&KAn7-*-)bn-v@&5o@x=PmwQQ{t%}AKuWmx^ z+bVo0!Z%t@c96SgWHIcW288E4ga$i*kT#f0o448WdfcsS;*ZN{cy$qObeqTxc#1%i z-2^r`>;qAK_a2?-9N1|Yf@IwToF`HaTC?j(+l>cg1M>_AhNf{(G;0{UxqH#?_#F6~ zRszA#PeJYHW?DJFj#Rs?15cwqx~RVb<3_(@&e(5gY#4%rr&^#)5%K(;O<2-e1;t)p zN%qT3Vj(z3$!jN`SP(2|OrL=Tp)*LRm<&7Bw2*kXJ;b|j5=rARcaoC29Aw4LU}%Ii z`_W5?{%@@!vVUCAd)Y&RPvX&@Csh54{mV>SeH6#*X3`Dwr(o!|WcXa4j?I@pgUvE2 zIN!pzT*aP}aZZsKF1rnKt#t8t^*Idxm5*f#KY-uwB;u`B(V|SNu|>s)eK0BtFW?Za2G_l+L8>fYQba2A%Qpl-Rb(l(-~Vk zh>YZW>@A!QFTOs4xB3b&o2AUC_0jmvoZT;u=J7^!XO%Il z8qE_z?%zVvf1b9>jOV(VR&e|OOGp3L3S8UPc&0M^B{apfQlG6o)Vlf=l%@^P*NY;l zki-n~yd^}?6{SSGX7XkYT`g9+MH{C+>*P~UGdYjh$FSdGCrH(KVyzi=0v=K9 znEjOKDqiJ@7y?q|p+bZ$!f@=r0ub^XA+uj6quJY2bnxF>Dx3J6YPpBNndL%keV;bB zve=T-JMfCUlDvmPXY+8t#TCBGEl1ORJ-jWqiy8;`pp@qmnzuFN+lrH`>{e79Yrr8A2f_~`U^GIN z2@-Kbjr|Gi&@V06C*+J@(Sr`2*MJnASlqmXzh`x8fY81_B)8T8q+dP2gHavCC;B{A zXY*#*UBlFSr!%zkSpvD10Xh6;dEs zJKIQ0yf)4pGs86DXUw!7p0=Gbj%Eam(eJ7ZBO@12w2{H zSwWlvRN$Cz9O@o*WiQ=|g`FKg%V+Hi5wusnB653V*=()lw3=_?eM!~eVm~NvpT~jdV zU=OsOzJyc6Kf-8yIiG;v2PUh|0ekWj%@o+9*5W$2yZ##)k@etgx6ETzFC5|mFL1a$ z_Lbo01D-ZvpF-aKu*C`MC86?|BuU;Ah7xj{S?ejWxQEY?9TAz!Rh?C5%e9w-JU>fb zZYa*`J2&B;6AGBK{uu6!ZKg1edDJ zC#End73n00rxvyToW{MlJP%uQZoojT7qpxd=K4=5vxi=c0hiuR)Z!oD!<5aKKhYi6 zxIDCKSz?X*zDlxFR*qrRMMJ959DwcnLea^s4aNoR1mV5oF=h39x<~meaVFQSl8Qz# zeq1_JHVbk8SRV1(59Hb$MRuL2HafN~!zJr~(Q6a7L9Kc_oqBaHnfE}B-8}0J^8u2v zB{~Ksj8o^b8uFnbDUf^`Lpz@Fx<#vez>$IM`y#4cks+;iY1cbl5`$m!-GW$y_X`>n=RrazeOg(|49coEd07`t3U z502M1GHcz=k>%s0Fi`mr>c$*}EX!E3?(b)CnS2~8o2o#qZV$YVZZxfTBY0OnRuI?gtm z!6}(Fk)eQ_FjV*yLjE~`=Zz!O(|A9yAp&w`P7GLA&F55RD8bJ>U8p-=il@bHG9CQR znjY#TEBoh@B`XR^ZP!B>mNua_R{GR3djV*v{)d^XGA!E;-zGcN76Q#O<{mW0(w|pl z1WR6iq-$Sa2GNsFFlSL5Dy$9%{pd@e*;`C!uk(dX_GcmHlMASvQy|yQ{Ew2p0n%1b ziZ>K8$P>QLR-^krYOy1qPaI3(eZ`5iuks)`xmiHdJ8AT`zJ(Ig0ivI1k=nm9WU%8r ziaKW!6`2O`xu?i^Tdn8v#ugC8PDM7|rV8Lec6X*W8|R1Q(|jFP?%-T@){-qu zmvabcX3wQ99XVuHb|`P4(E=~g%V;u?gR>U9upSR`dAjcne6!gIwoJK%%UV{!PS@#R z^6oYathZsGwxnPfXU@vs>8G(F^Z31wKT4GIdzFDCtWO(4n}}%G>?h3$mA!(t+&XNn zm_obD53u7#HwrBORZ(NU-LTn;H}HH6g2&}7_wfcpkICOA5*PrFuO-2Ivj7+?pU(um zQ{ZyHt5P?|9FkmJ0SgYFq7H@2IfE1R@NkC$C>WR$C|Q7uvYJq;VFppTGamE)N;w+fxx-7!&LY@g)JvU(elq-1UZX8uwb;+tUAp{avUBQTc zdo12P54V=|g1*FkywDPZHxDFZ{#Q+0^Ff1s*BwNx52@oqr!?GccLjRp^PlP7UVNX0 zXzO~LIqmrfLN1DtS!4)a6$XRH{7MquTn^TqqgZzPA6V_W2QL@oKw{@CX3~xrbS=Q(^M_o^qU|7Iqn3@#?&JXIqC2o}D zHl{$9)*!u|DN76AUO;lH7SyFXNPJPdz;mM}B-{}|-j@~}2xD<9mciO5v9#ZFB9=Q` zg=O87IK}EkfvnSzT zL^^3;f>1`ak8E8h%eD$x3VJ24VtslAoaC*|kFQv8YV{k*yg94M_o93Z3oC`dwwB6g z>xEd;dETfpAq1OLc46oxNqo`B-)pnxvEJgXMi1LU17_9#fBq}Geb2D1#qm`N9*Xe{wSvs(>O$5vA#-YpJnXF9z zA#63f$eSq2p~}6K%J3`CO0$^*ZD3({4?boe#Ne*I ztW*9XXoyi}mz`NkA`a^b^u|%L>dRy-N{9!Ako!1gY?w(nB*~q)x{2sY+kukKBQWT2 zhUhdAHnu)WpqjlIbXRYn(Pder+NT}U4^9O8@Rx!M2AL?)Z%?h_?a?~jhCcmyh6%d- zlaa9I=e^f`nG(gL498tT1GP_dbhC zhNUKv46Wn*uJtHpL^ptm79UI#sv~~_)!2d`9H`%11GK6b`o0H{HTP0r+&CYuzf79F zTG0o4cDC`B=33zL&%g`2O4|MOlU0#TG?riB^JsDg5OYU>(npr@^UZi-b?qX#To6u{ zG=3v9HwHuJN<;R^;omrVO&OM7J%z6C&r<6ngLIa*I}}Gw=2p&Cu`1{rCd02XKqo;H zPY)^5+s>Ei#`(5{?Vk-Qg}P{5=|)~|-;VbDOvON?ioAQUj`?+prIBjJT$6beR?f)> zr`ixmm{$qHe`1)T^p((I=7wu)B2kCWDnDHNlj(S-i$zu^EEV>wXSdPQXtp93GEdxt z@|-KgPdXsNL9zRblk;y_G zJ4HUH_z7D}f}r}?Idm@L$m6it9Lk@jdYo-Bt8H~4{2(~lTc(0`% zpA!|fwdV-iY;8*KeX55O7e3PiAEvNNzpsTA+;_TcK*vgdOEOwrAJ560Fk;tUy$&;f z=%TmxMYQ{08T?&aM@P%%)6ee#R3_U&dK7O?KO6wvjh8TYTPf5etzlAQqCjopZ9!M| zOsYLo1ZT(anUndwq_|cFyc919gcGe0L{u?qX*H-PE=QH$vBZYYf~>nD&mBnpj|h?# z$jz3?sPg6=Jelo_@d?jhzO+hft# zuAh&|TR)&^kqk%0H-hcLi=ec?6|FA`gY{7c!xDyQ=$s0Ces&^E&AkK;y5l&N^wZ#K z+Ql2ac7WJKBTmme5#;?V@Oo+zs!hBEUa!PC{k{l@9mnU`PMRD9UX*Rnr4Kk$L@ywkd zEGkID_p8hW1CRN0{hC1NyTWH^SN78RiJI)aSuU*F&P0@m*5!H)%29NI61Ou^o$YLl zqOq}4!0q=_tDcsvFxEdp|NSS#m3$Cl-z?DpJ;twf{I7PBYo~>|8zhDsmP>#=>CMo%6(ORS5K7=%K^A%(*X; zNBP)nDU2<-hjtHEgXf855Oh$LivF5{(|#pm!8mu=8X1j3=B&VjVFq-<7OEwOh{W({`ShPZvRaOhneg!o5- z>An|MU+UE01XD+&l5gTTe#WWa!O?D09j-C~Vd&^|FyA4`F1(UPEIsWYuKF7@(^!rx zeUU+A|Lme&Z#Ut7-mK@*Wd@y_^l_YY5)8a(L#y^|%n&h$g`1c0_D(fUXTYE8Tr$D) zV#6fUVF!f2*oflKZQ0oSmgvVuF$G~_>~6yjG>Kb+eVZQ<%~{^`%c*0KS9=Es=NnPq zqnc>!^9;v5Y=eYNu{eFgQ{pT!hubLn$V&dh%Hw9w@N; z7KdPjeE@sGryhG+?x2Q#G;bO72ip(Tbj`9HFiO2c6!!!W(;dQWIrk8+d+CG0DV8OL zQrw8>H=^9fk?<-&n;-`A6W%b__bg%qOaf4a@5UVYZz9CeF zXCT7g3m@3Pl6~T=+;imjGYj}ue*`IUFGjuZS{xy6kh0%}4Y`k5h3*o_BNi@{%FuzOhQ&l7>oVyNU|? zzpe5(ZSNb5KVS`e->I-cb5)^zi7q$pz&2m#b4%a zV0QXFwA@vK7~UAyaPbu^%nl;q7p{@goaK1>WeJ$yngV!}2Wk~j zO@b{gslRd_99k<0Z`!7!&HY$n{Idh}+uoA7Lwts8v=l$i|6VC2FvEz#i*SKIzpA|y z0|^81{Qh zqGR<5bZo9byJ3AO?^ECglQO~ocna(np9Cw4rm0rf4`h1qCEAY^1*xJbf5;igtwvnUQ3iRDu8CSTJrIh8ml@h2F6wyXZUdF0O-6 z!4lL_p1@A%)}$*;J_?dPy1}QmUl19$5WNPALH$$}DBrGuhr+jL;o2*-*Kh()$=C!+ z71#K)(07P1U50Dj87R$-hX>nIAvTxaiC-AUE^F+Od1t*=h59TYP+4^Rpmjtk0lN_wt=4XBCcmB z2sSjf5`S%HIC@wIjiwgRviNbNLhwJ3j+LfDUkh=Zwl=o><8$l1A22L0o>^_L&-s1_ zd|f)3d>J$0uEdG6D+KdUxNrrT`6?RR9(kdL;T7PA@tC$gAAbKcB7+n55k2eWY~b5c z%vbit&F&BQs4KTgG8Z8cH8$)qO3AA-h1@g@R z6Rzje)Qh_z|F|R@ck?bJ?~X&$D`V7KrVA|E8iHTd^A?9CoZ-+-j1LmW zAJsZUrDiovc9!C*MTTKW!v^G}L+SMJbJWB_pRL;dh9o`S!4=$_&o(L4Gg6+DsP?T^ z7%*%`iD4}?GOUNY?w`TrWF%oQy)s~rFza(y3X8;VTlvp<1C^@Icx(X!xn;-k2k{5F z)f1@I+WU}WG>1k;@wwt{XF%B`5jvl#Q@(7AO42;_W~vMuxueEP{Bk*-Z~h1cGUIW> zk8Ye>s|_c%*MdwzKS_{WMzj0O>4Fwfw1`uJl-tkAd!fH@?WPWURBQ@_59ZN==vvU< z*$Xb&e6L|eJL)Fzl*l_ZAQjK|2R#u1z*n+TUrYH*8e zvQhs`3NBkCPBoV+LFiBe4XxA$pWt`QWQp_S=Y1_^&-JgcRi&S%eQSadwMH1OiGuHc zvw#>);D3vrBt$$H!+!iGP1cpDbNU<6^$3Ooj>gdOuNm&=?tvFalu-C8|NF^^#OVR{ zu=M#2tGM&2&uC!OH_yc3 zybSD7d=K{hSM}j!HVl=Qe}w6OCV->z1^n=W-+x_qmDG`R<#_>m z|35Fl%w+q)gZoW(Z2NzIj(F_$+;wn8pld3#kEZtBq2d~y)I{z%IsY>nj=6?_^CUgk zJ~NJO%ob6?jh9~$PxM3~5-6KV^6?C}ME=Q=E`+HbiqsiW!_K3=z)#PNo55dw;8aTD$ zK4$B>(kj6v66JIr9Dm2*xA0UH4UQph(~9VMh(OWfnV^4hJ=C}TXZ4%^>UFtK=0@$D zS+^P2NTbF&qMsZ_ew?(!>uqOA%&ij3-sDxd*M_%>X%AALt5R&{Hr}XWX^u>$9NQ|a z#|Fr+K#%LckaJQ;%PV6rGd2T-tsT(Ic`oPfcOE0AAL64Rj;Q8w4$FGg*z!d$G5wep z7TDyV^EGL_>Mjm-Z!}r`i*nekD9UwzWmwy-(WtbLpSMS(g4p9QlCW?aIdbk3J}%e~ z7WcfMaHT3%*^ZK}o0I5UC4VeG*G{XSWstRs2HY`=`yhNfhlp1uR~E~=k)GN({QQx% zT0L?OXT>Lw{yi+ITCf0HcWuX_g$vl0qjK!vvmn8rH@dKt?=Ir%Yvilqm{sS%dKBkp z`Ooe>gW@|+F>y`?%9JWXh-)3%Y-3C28s(~7_xEZ2+#xtcVlxn(?lJYtAm z3NbYDPb^B5l%aD(B`WIlF!lFLS(k5~_)S=m225Uu`m2NaUiuvVP8)#gnzv}FPb{qb zP!Iby#-dC24fu7J-wDe&;%A{SrfF3IMo&}cZj~wk9Q}-4bu-{dhA18K>SVSaC?npt zqO2B*c+lOx9`Npi7#Wf-gW0de!NKAtJ+IIpn6Ko>ZGNB4?-K9e4%?4pWL_>7hA%D zX^{YRcV3e|rWQ3Ph_Ihdk7t!EH{q=P5}d(E4b@z#$JISKPYP1q`Fm3lCO1!oh4Kf< zsrgZGRs5Hrx%fKRMreaX_Iz$y$w^{&WEbQ8ONx6nx`&hYY6JyImiu3uCTF!)1JZ8) ztn^>tPize}d9vt9kbyXoab6N#GJDX*J{oLmbO>+s!SRQb$+4M6-0w|ii0{$WoWJ>G zj8Y5bdK|k!WrHI9xj}{o827@EPc&F+rr>`ICUULDS79je6~34;7rMS0FuInnvD;6V zovS9yX({@k&FXxt+bRaH!gQ(8^?ov=VJ|Aa(A2Jb7LL2QP&&7M@DDGU)+us}teK@0-ZHj3!@Ax>42kX*wD6>HGHp4a3wD+! zoEQ%}4%9w~oqsy1jCVSidTCY~E#+sJGqllat_)}OaXeJMSV8X$#i8&{RqpicHu7_V zHmjnx2z>MHNnTk6ze6_1yi7A&n5* z2m4{e&5xD64;B-vE4Sg}nqh3;)lZk^Cz9FBYC8F46!~%O2;;l<4t-T%$NsodihUcu z3-*|+VA{R;oLO=kzAxB_doT6S@RrMP*m5el_E-bh{Um8bbD>f zckEd1^MvK-x>{G@d}1cNTe6Ro|v)@?t*k`EKz++}cHHVKFnm!*aC=!Q?vD^=e@(R`>O$F2+PV_GrA0YsD#6t_E@q{z zv&qlD*>u>Ow;9Tur25fE(C3*q_4BQQp5u0Stf`Ks3g*Dy6MsNqREqO|&D-t#-HA=f zeO$wLj00;Z8V8QTuOlz1&yq3pI#`Oo2iFs^*BNBK{vurGq|aok-6yKsdD84XQ`#gS z2QqX2!8+v#7}8?M_6dNircVN=wNh}7H>REc(*Px>WVxdDxuj=I24iK_xkBahxF`G~ z35fNt9A|cnXUIIG-_)WpM&}~qST4qCj6EdXZl>J5%WI%>r#EJq_2NgMKrJC57s< zI8HwZC;i=o!?qjH@v|_rI?iF++`4Jcs4abW=r(9t?;)UamlT_qG0MvIxbuxWZ;3w$ z1uJXux6XT5`Dy{4SF?vi(Lj9prjbtOIU8XK+qo|ynIt@m z81l&$n)@bm4=!awN~SQo!!jDe%-_=Nm`VZHavhzXOomA=Wn) zGG0o+^i#vA8?_O=?WW?WateODaS7`mKc>s11`+m#@orT|NBXro?7CFV&j zB$wkNp+JLwe%_wOb*|bB=1()hPAas)q)*wcz;v1$b|S(C)`^ z0>|g&0tc-P{Fy?SOEtVwuBCe)rszphf?f5FF^!kla!;xIp`iX3j{2$)R@RgJ~h;y#| z%+oD9oHXp{#Oh#4_@)1us3>2;YRmOhET9l-b_g>fJbAY3-&|TQFo)|KYe?z1Ei}kI z3RXX4(a=a74Ay61-g+r0wg@8EQo|{)FQzNHE@P(TBgSw;53~DX9B;Cf6MQ%%%(;e6 z!~sKfj5#37rrzI7(zl$%6IUj3W?%MlC95n!p=UE}&f@0;Te}#8URieTmZR`Pwi*Y{ zePgD)93x!lS|YwD6wkl-hMmP8IImBYHR9*+X3}eM_pVcXaQ_+BwWPqV|1zxf?R4S& zBPYm|lV_XeIFLmmCOAnT4&%lvb32cEQzg&k)bGn4up}Hx?N1_gvs?tKcdB5+N`Ek0 zT}jtPT4B0RAe0*?z&goUTszO^N&J2eelOpSd#a~#eW^Woa$^|gtABtKU0*S@Y$asT zJ-p3&1)eO;w90oE;{rduhwTCro-CJ0zG|7W%`UA15Aq!hLcdg|*t_71t30i*bByf& z>qVklj=lSb>LLL0c0nEMb4t2khu%%gu-D5h8%LV6YlAXNIfFjEV1yq9?U;XQH+6A|gJWMGlB%uu znUBU^$m!WI@U94!N>sK63*yKh8t^p{`v$J?Hpd&# z@An@$;&vX*>~5gq>0DI%CQes=Z)Q$!?xKyWa>;PY1Jt`3j}aNOU~tNF2;R9G4%Vaz zqD~p1c+p*+Vyg~|4iwP64#S{QHJh!TkU+hyEjiJFo3!UdF7DOxgS|U4=*OhftXxeN z^rW~7Zt`?}CsqosCVnHWbG~3pyD>D*oQh#Drs1QTLTv4{dH88%B+$jfB;sN|G~GCd zfoo=<)`&Z^M_L~j?h1sbb*a!(UWy(8{%~2mU$F1|V=_|x5Q3Zf>7|o0;AYq;2(TYR zU!g`2J6A@=zG~3XA!Y8smHz99NH}bua0m;UM~Q{S*+*e~kZ2RK+ak2V}RtD0}^-2v;)s1B51J zlbCH{G!Cb8!TkUCSKlJma<&;p_g`Y3FHD4n&SZFTz7*AGZ2_wvQVuk?X;CzoSJ=Phs@I}fwk_R*VpCkfj<49XTS=-&2vvg*kW zBL3^V;N1f!7~;)^dk#iI`e+f!EgE7zALn=ZhtCM+t-Vi|P8-77LwsgO_%>Xv`i_bp zr-IemJkVH{gbVIU)9s6@P%jB!nq9QuN}()wePRNPMNC0%My+L;0b_AtO_lesH4$eo- zCHEYQ&`=pr$0(M>Ei_>DN1l)HkLg*H8m*XC37>biDzehjC=dv^=zxjfZL7ao~COJo;^Fq@Ei_1QM?n zgUbK5LypHbESP%^y;h0Cg+skeZGjSN@+FxZ2<#O+QD8{MEM2I6ZVxScZ{dq`x%6($ zR~jXrh-D*%5V4_&`o49?sln}7?5qX~1A1(RZz*wpbp$JyF2laa^_3|tvb3&F6eLe* z;IvXb)Y%G^+V08lK5G%X;>ZWaqHGGf@qERvKkqWLS8fMiTVaeDuD}aXZ>avRT{L|6 zDNs-OM_v3@LY-e5oXuGSzdx$du)e=!Rn>QRFsetBN4w~+iN~49CGn*DbrAaK6yXYo zHy~6WhSQhi;fP%(80Sm|^PiD8Ph1U2=t|;0?7{C1U((G9-z@r8K7sgFRqox_X4>&s z5`^yoo3P+3ObhPEz($@Q6Z{aq-{kka_meP>H~cMr*H6>Uu9E(L4a_q?{(U2!!6=YN zf;HEjK+~b8(og67{>iXA)X~7Q(K1)9p^ydKovarltr(ldqLv7tMJQk4%ccujawu=Kwnxc0GC-$ zq44k~680~bPF+(7UsKKqOxwmt)ACH3qEk#K=P0uWJ};!(o4#URau5U$y0aTEj_`eh z%@}ie0%ptB(j^@eIhBJYurDX5t_Pfky=l10Q3_ZZmj3^>yT!elVm+<$$yYxTZL@;Z83Xh*ohB2KajJd$`n=UUV zJALP%igyd-r+=oyOZ(BGRFj*!(S&tf(1g?ZTvbl$db}Auj}0CAf@}Kn(MF~Sn#X71 z1Z`u?j^(?{1-FTUjua!z7_r_PblKSkS82nOCuFBqI~c4OB)fg0(Y*95h?~xW_XiH6 z(ytxRFmelE&Hv!m_$Y96(Br}k50Kkyqi9LNcs!E!AB;N5fZf@nROe|R=qlx)o`wb) zp8B0^cl-_iiwlIE7ea7hE1*x&EU>8K=a2p}7#=*4n{F9GH^d|AHC3RjgDj6Z=-qd4q?ujTH;sC+krbu zp_3#tbI0$cjWH%%-J6{dviczo4US{iq>d2tLOz>uZ81yeWl#(d2G!3K`0v6qnl<$W z={`4$-rO*rD_WpLgCGqH`uX$7(>pMhJ(F$6J}f9B@KCf8zqgriGMVwvR9^#?roT{c z$71f3l^DH#BOd$4$1%elr_h_f!%O;|CYkFP6kR%jZL;DGiQ2NDSRuvQXq%u;Y%mkt zdxdn<5-3xDg6;!^lej5&VLyM@ z+VHCu>*flhm1HpM@%S9--7&^I4^cS%#uXR2^isdg-%)Ag4!QI40L@PbCq^H>63wLy zx*D2u$;F2Fs63RP0SxjzDR*}0Y&I<2nakf_TR`wx2^Ma8OJAjJMn8ctT6R{D#(Q#{ z>FaZ_)=8FgyrIM%O;P1^ci+Z7gE&m^(W0h&_V$FUA!|Cggx}Y%r(GGfMDM^|diTUZ zzT>%b!)lT?REsjd zWw@2{*U7t+e(;JvFxZs;27wIG?aQ_!W245Rdaj{8k;~zUtb;(I;mMtXk{v^Zsv+C$Q;{)oo zEQJG>jxeD^gxRK@02W#saHp0Om)p1mPX}*+-4_zzVA*(XQB(;i?W<)r23Mkj-hJ5J z*gzBR$zrg^Rn%x`!mhu8RKor_l@SX9-8KPwxZPt8=>JDvHh-Wd_I84Qo`G=OKpDQ~ zoAU3rSujN_3zwQD*UWQdC1;3%knsxaSL_GBYkc3otQ5uKSL~eJ~#0-(7Psta^Eu1V{gXgx#Lx2+yYU4c>>Iu@~Xm{&sXYY)IJ(-o!9`g`}Rc z#+3P!xHqO&sQ62VHNNW(0b(0LDdGV7))b?AtuU_N9*)9AcKAE674q(gq50-mm?X8A zk+~X1ew*JRLz=&!OX4hb&0T_4Ic89F%oTKYe1U^+B8aJWBYv;r`A|#$QerR#@|KQg zb$r4FY~@8PxZDY)Ui%OFDy0PaS2= zg<4 z*=s>SQ3Qz#y^Pk%{{+8+D?rKTKiv4{Dh>ubg~sCVByA)O*)T7Lo-HE{DH`0plJ8JI zm$##KMOW&8)DZ28%=u{!8ZMjP(>do`UUcNJ6|OvCj!S-9 z(S<8+G2*wCu~Xd?T*wCQa~9A^Yv)5sd>uwg8De{6CUyR)4^k0Hu;K4@tQCzD98LTyswD<5)m{oZHi#0a%7I{?Q>dW&AGm2fffe&~*h$7uDraf&R?E9j zQORi`+Wj%Z{%!5V<@8Ui{;`TniInC%_iUyX=>&SjvvFy&u^`Z03y078p!l*1YQ<-n z()fJ%iRL~$eGwtz?>OkXeh!RW<8hh#5OF!|CrH`J^K&k?!jYt2dX~3Ac&#>M1H$=^ z;L!rCofCu*nFjqupKw76pSSc3B&&La*s&*dc-k`v_o#BjS3-p;^i6@^qytiTlbE#O zE1JpoFbij2hlDE(Oqa40ywJ&qr)vyoMsX6fM|$I+bRyP^76@eG-@+y}IaYtEAw9jV z6WX4pVP5x1no=YId4F1H&5tuo*S}t3o8bu7jeJ*A<07e>+6(gv#&L)2CvkO;vtdSa zEDhZ|i;bzcj@-F${Ais6F@KLTv1Yv_?>Nu1KU@VFTNmN(qvKF_{XWQScuorL_EMFv z<~Xu0g}K#MfI-HGv2?ZoAF5Q5hjM*b@`m4Au78c1w<2&)+kFV`U5G{9r^p>66?o0> ztYkip(3QW+wv-YFmSZZG#=zW>b6}tQ7^3IB zqSd_VzQbw}yK|K*_<0B*c1a$w<|ppz(aYGrBjR}ei#OZ{QPl3)kJdAN&~Jd^J1gF< z+WL^VhzW7sN1`!ZwvgD0^h4q;39k3PI`>s!CHP$`gM)AVpr$ts-rsYFo1$A#IPV%B z(VGOX9PZ;l?|YI~n}pr(jlgC_IvoGZ+mYA!&`f**4L3E}{KB2kT&IW6w%o&TrU?$W zo`t3Rvp_nxnOLhF!O{6$FxgoZE6tAbJCWVQdw{_M3r$dD%@@=cm~*n*JD}xn4%m;s zM+4TCgXO#bL)V#xQ}u>@+bm=zBtnLY(1?9sOB$q^kP-@sN=2nn3Q465nJYvYN~R*i zzON-yG@+>IuOtmZrBa5X-t~TXKfKRzJl}oT$6>Fv@B6xbzw_jCJL7B+)ISF&9-oCw zE?4_u(<$^i-$~m*Iq$=ZLs3$xXdW-C_Y0i6qtDW@Ew;O~{PJ#Ab z8DMN1=XPK3&(tuU0Bf{5{c~NF|64; znYBxON<|<64G+tM#2QEjC@1BXkqt+SBl#)7fxmg7D z^~KR!J)RDIh@HvW9Q>)lJ3b0so5Z-UkA%>>u0*PC3OKG2 zfMM0wa8)gmwiXrBzs-EGa5lm-sYyf(Vv!sW7hX^<&!L0*m;nA@P5h*9xBE8 zupO4FzQu`xPUP`rdGz`qz+Bw54$6*8kf%du(8Jw`J!p5Gs4lOBkyj?{f|)1b$%G=@ z*7lzK{WU^KiweA6(m+nVyTLsJVLbZ?z_Yf$i64<<{H|A#3{L|L`qxj_9a6#2@AqMM zmIU)}nJC^A{Y3+RvUB&MCzKuW(GN-w6Vr0fN>ZfZ5Md zi0=e_)}lv;5!v2G4DD9q)hbQkl_Zm-Su@bv@gFp7jK^g<%`oMiHykn6fvDS*uJYMW zh>Zj~Cf&l|%j>~Bv6^1V$)JnscED7zM7nbOU%0uFW2()v0;}F=7&xDYPW~bEXUk8T zHgK18^-pBqRZfLN5lEyy+LPTAY)}$!g2ARNSZEZ3fw5a5aOqQQSE;7PCyeQjNmIe` z?OlK(W7^i24(`4;aM7kp@(1QKTdF~LzJC(N16S~^{4_9C$U|lA#~P8?wxTs z9wTn9VQi~qKy9rziE`hCGYkLFHo3D9@Y)Z4oKRsyn5TFyQI&i&?4nn^5-@k#R?ZyW z1_Jx5zuwyHS3KONG9lK7G5<(cDffWMtDNP_RHqP+ejmU6!>e_a-{sgt?) z@^VAaW^B+fJQJVK2fXZ)Ku7Xj=>wCkxQcwl`O7}gC;Nq%#{4>>I@bo)FXA}i1%~*% zb8khrffE#B7<3Qj;u_;y5aVKs@7Y^)$w&&^Aze`DKY{$nEaA63E&|t=EE?>mm~woG z`nLq&l{yK`35>=5CES^DPZUV>3UIrPRro_Kh>Bc@#_*|P%q~WoEgnbIKQ4{=8i1@y zDVC1kB&_HXdT8uF+U~+}%{LKvtTO~r=~rNiLmBy86UfV!DWz|gOk`_ceWiY9x1j3f zMX=#Q8TDRf$ex=l%Vr$Z#Q4Jz@M~Kmsgc)!)!#y)hqsRQyM{sAvpi6#=NJh)l^I#s z$PczYPxeSHf;^2eboD2cZd(KXYx`ksuNi%yJe3(~?FMnTI!qT=WEL&*CZ~mD*@L0l zOj&OlJSjB7LC=Yx)}Rk-hn{hHthu;SLYEO%Pa_>)^q}>#JoAuyZ`oV}CLy8)mCMeM z72if-?ZNx7tGFGu1jNFy)GO?LqmSvq(I`7l8YiCSa`&CzaC7rVWIo@7z{(SBPb1gs z@Z@H(j|0Isp&vsdpW|s0d2qk#4eYD}l=29M{oHp|Pc;kj4!MzeF1b9>>I$m1SDiJJ zjY9ufSD^jZ4kF>a28mWUh81;EQ@KNMICUax?|v3CR(>J%uexz%`CMxCPzGP`yiEMe zvSHtuKVz?A;X=Um75?yG93L5 z?M1x@@%)Q1Q}|T@N{}gI0r?As(Y${F=wCHw9&H>$KPfRL>5K<#$mzj1q8H$%V*_Tb zP+*x!s+-9-SM&nSCYI!P>Bqu1GsXe%L;uAx1rjj!hLBy_y^|rxc{O$ufRH zPht3gF~{EXhRdos7XXAq?#1xUd_B1A;&v&<98+tKGb#KRT~Uy4jw_=J@v3+jX3xv#S=wjv zHydQq#~*jVX}SyzzxrCpsYiipCua@w)?iBW)nM+k08HE^N8-n$L9lHY2HgMA_~?Zg z_cw{Zv!2Tucg^OnqInQB@CU<0o>XK=e#gM3wdCevO32VJ*s8^K_=Ue=^cTvnpU+A9a9T`n~w`pa9?d)kBDx za*pRLI)zOx76bpCvHUk$!tCfOF?bd;8-1Fez)z2O{)RcZ^v+o|+<$ijYh>kd$4Nwy zM`@rIluhlvo`rA6XF?FG#2ik&$4lQ>Nt(t=@fpXt{NY{=(r&^0ZJWfH>0JWQ_o;^t zZRgJLez$lf&*~t;G#<~UU7;$;m9VR47th`$sbX|Qk)0qX$Mvw!!L3YnM!`6N)I5I# z_PyW1{zf$}J6nRvX(8BY`_*h@Yg_m zt;=QFv=ED4XV7VJo<_)dx{|l|Zzmu{KirLaUq(v9t*~n3>vY&+UXPR+eMj)LPl1wuPPSa62 z&ak_7IX0IoLH}cAh19KtEyY>rCFUjTuBY(FNz#e}(x|&0){<0a~%5nl@SX zWA=+ln0w3sADR{Lew8g|O}3T770F1(?#3lNu-OH={z>DxA5rwn z6xQWm;{FFts6AnnY&6S()n`5+8(=|=Z7$)`ab57w_CnXPL-_SzEa&}Lg2%agda`j8 z+FwiOxi0$2Z}@o*-VDm2%hM?0{dpKe=ysTErUb{VuY>0HOW?q{mpraMLQSjLZ06T3 z)L(BdFK+vD>dw8h-A^qdZ6BQRxbPDq$T4!5bUrPV%?Ho8IGp(OF?YVn;(8P>X9+P?8;TBRlgLQ4$p-l zG0s1ro`gXTQcT9Wb})7}!=o~yjLs|r4AH;Go0+f0^#%T~U+{nH3;chAL9O-GYM^$1 z4SL_d!k_2bM3>f0W8RmB(?zLXa3WC+W4i{hU}!qpZ4QRDG0hOJanHi*%OV_K(SUL_ zosc#ugGz6RgRY=YRL)>FKC{jP>4j!6d@7fS&3;Vy^)hTy#}q2An#e!-?>xk6x1)GY zC@p$=k*=@cx<)T2qmJn?pji?eH`@h^hvhL+(uQ8v*bNb}dx?jPJhP@m3Lc7Vg|5yG z)YjSq-b55*P# z+?aZomQ`&>n@JbIF`Z+o%jIEH+%UvuMS>s!S;-XP-apo8a zu#17A%4zt+QHedRHIAiMgE8*Y5O`9Dr;695j z4>N>vU~qOB-an+sO7BrY(>KBJrcH$PyEg%IHQKR4el;4Hj3TrC4hlC1LE_td8m)4f zz6_ZL!zDBMH^i@_w$F2(<%)NldGZ0QgE)-w7UdOR_yLSM$J^KzjTx;0pdCpt;++_E zv)zD?e7@2Z54O?6n=9~PMI%_%p5l94G{TY?SNLig0&7433jX!*rs-a#9iNNPA$%sI zyHN(mdSmF9C}F1YmON{^@Nc>Op_^1S*%Hzp`#MYn_ZYxlTqH4gB)wCMu-XX7GbXHND=bq9s0J}VA^+S_NY!8 zSXY$O_s;2{|KDajCwmxERRwW`Gdgc59i=rFj-dLxlgRrZ3+30ogP6bry0&W<2<`d9 zzv^|0ul7k0V)*v#%lcUmvGE_VOO7WmJ-IXXY*p4;MU?62Rb_SDo6zy79uwf`K%3Uv zz|~c@sFUnS?KPW8|6)CK@lwFFXhGKMk~Lo;z<~MoE0qwwD!W-+8t$zPBd7Wi&adww z>|r@3-R~6cm@$Bsidi@>Ap&mhdI~i!#97I2e?e75hVg3=#sw=SIp*QViX#!j`259U z7@cqdJib|>f5%(0M%xD0o3-)nTO-JU{nwz`a}vg;bdk-jE5UC<7$(UFL(RZU*7BSO z6_j(w$a|I~q_cu9-NbcG@8%MpkW{W;x)UqZ=D@s3-@se&B*bt&3SWKBBcz>$Rm&GJ z0UisP*u)8pQ%5A}<2cp*KL4S#)@^cULmkZ9-%4M*sYB@IXAm1u%h&k)j`*yq2F=wn zaNIW)jfO>-xhFUAc9ciM3MeE*%oCDg_T!vV6X0?D7dtyym>YfHQOKqHsNH+n+j+TOP?>dqgs%LJd z>5Zb(4dL*(Im~%=6BbNVhx0Ir5m4C#?a#fqoz82%y<#?88Wvhm=i2eo*t?q_u*Od5WkXh&=$@1V&i9{RU7IR_fAX=!K(roWD=<`;dF?GFzn+1%h_CG&5;Qj*W$R8Nd&}WYi zz9VZ?xU%NkWN2aDV~R)tXIq{L^CtL^@N0>f*mjN=B9;hi-A+Q&uTL;ZMvalwvIpH$ zTTsi2bJo?3Sh$tfLcxI`)S1b37lI?;f%j2b*J%Km`)5Fv?@Wjl;yjbbV=#(kaNJ-n z6Mz2&d?*{Coj#7NhwC$XO|Tb^aC^G7mmbg$r~54mnxc?02CT*w6ZYU}A{15klhg^> zL_6g&bgQ)EU(F?O?DINIYrly4-7z%R;Q?K9E|-p%-Gkl-K3E|kiF%u#;83L`1Sda$ zg1IYT+r#ZVn;iori>yS?#A1jVp2kESFXnIil7(Kn*&LsA40GSs;8j}AJ%)Dc~ZSh8vz}?F%vzC(m-WIrd z;}^2vT?$%3INHy%!E)(&_lz(jIhH`;wZ9FYrFQ7(EOA@^)N( z2tBeKhl!cMerb*Y(*{9yra(N+3Wy*cOUgJ_fiY>kEk{zjr=#QHGxSctCvt3l7?)$Y zf!p1q$ggt&*m3C+nB2QVH?&QFJo}$qkH!oV*3M+jo9#!GF2^<^X%5`x5RfzGY zD(UeeCrlCjN42#iSe+;}e!OulQE@+y`M;G=G}<6 znUNR^hHRH4P;)p*%Z_SMou&)?s~pGs_I3pjFZRb(50sf3xBkPO`&1xm1?StD|Cskf zrk>1s5CJ0t3vf8xnA&hzwT9Sqy65^z_@br<-u{WOV9!hlEtY_uPsup&<`wvJ+4l8| zzQW@gF{Vhl3d$AN)1KtFWZeNtGCZ*s7mGKc!G>FuT{)Ghn0my#fAaYqvv@XPRGzZARJ{s&e{oh0G$d(iQ5Kj`Gw;*1xQ$Q4C3R_ftN z7`yj?Dwb+-9;E@0h-#+uGumfj9|a0=I#U;GMs zlcMNreIw?{^$X;-BM&dCHRF+(`(Xbj4xPWHsoVMGdsAlcwHzC-CNmUR;}Wh3bBYVw8VYa=f$*);LADetnWKUlpR;@Dr$(e^0kD_&TQ!6c33mHx!K0R?S+qbAIAiGr&nsuHxmVd$8b`Hv5)q zBNs(21<%zotj6X0WZ982oT+=Ce{i`5{NkK6O6n5qgXG85JHnZMnIzBdK@U>*FqtV_ z?FTa?#&|yj{9(_o3RpaJ5vW)IziDa#U8cFMA}e_oX;S_G-YVK`Kbrz8z6itL$cyyD z(QEWq+hdsH{)#sJGsXu-YoYS}HBd^I#f^2fq(&(coi0sa`8^f_Vq?Tts ze+@bc#G`xNL--13$Ys_N9oH>|ZEM1a+NUIJ;vAT+Z zYNYxTK3LqK`<@jL6sZMk9}mdR=3H~nbg|920|wW5)2m&!s7e=NQLR6jalw=cUnIq4 zGOh99dOwKya2Phc6=C-TF(^e0 zyA%JzO(WQGp#b)E=JKaLm16TgcM$830>t(okI|hc&2((VjWY{#WQxOl{o zW8ErN(API%=1xD9YMaRNUIs%}*a=J>;F2pxo3T_r9_=@~AiTRp8d}?VR;#Z=xv&H) zn7fkP`0EI{4lMriY^Dt|>SR}OJWl-*3BH$t$^A&q|NYSik8R=c{X;pXnNb|k8IWc} zeZ<+~iw!)fsz`7(_zpvp9z|`P|N=mM!fzDqP|72>4^{qMkb@KgfN6m zu)^q3L_19dBI_am^_O-28gvaAlhj1CuDz+;|0 zE*&l=4HnZGy@W9`pl%K7t5%CQ>Tx%-LkL+Bsn?#51gq!6Q1yV=56z>HXyy@QRaukh51XBgC~#aK?iKpnnqub4L{ z3^&wxf|sNZ$E@2=gC;3s+D0ub>g(r|153fZUlC4U*g!1h?8@~NE3i4ngh_R{1XFy4 zY0>g?ka%7Jo)tTRYvC?N>ZlxbUh4vO+#apE`w>08DhQr!ox<-9oWOQhy0JYGvtYvH z$>`6U#Xdi}502lEVqewlA+u9c!Og}Cx%B`vYD+SX?;5$;auyv(t_AM}`e^?_h3oLX zplUf!vHD0A@%3H=H|XIrcXd|o+kS&M1n|VCZTJl5<6nAj{zbkOjU>}u3tHT zt)1E67PJ%`U6-;QnxQl&<^i<*iX%$eXQ{&LB0_JR#MlpKKoEt|#{UkCJ-dO5KN>*c zqav%XcovdmJ;=6vj&EVa<;7Imf#!}=DW4Jkucu3((tRdY=tQ7d;5wKZz;$6au7<%& z^|&>L+hqi$S1dj|n{~dfj;9iYP?XftSoI#Pd&Y%#o=U>N8d1h}wlewK!f~uCi?Cvo z1S?z81Y#8?%o%Q$shy=jBT^XBD5uY|lJ_CsE((OH40|o+X zt5cW4+jw0_5t;)tl9poY@@J%m^P9%6@5h6z1atPsf1p1s%p{BXK;LCqwzJ%ddHt~% z_SX+VCUvEnPDyZbz7qERo5uA_Of1$%?8PDhE+Ertz`o2lNCe4k)R;3Hi2g!$u$9kG z9GC?TDaP<(z5pI7DZmvgEFtiC7U$BP2#ap)h0ZnG@q^76y%nHBX2`ZePNFwdb_sIl zsT8_1cs7*uenHE6Bb1qHk8dSr!^b0gXp?;>(G(zPdw3xY`1OgjoH+^^!}_f5lqOUe z*I=dQ3$dYfuQ1-ZkrrJ_ME3a&FpR8#kA^eQju(jvm0}=r;4Jo)*kiT+3X+lOk13xw zVd&sBUP$j#{Wvpi_FD9V9b2*OI+he6^DdaAE zkG{2uXxhCW{C7BE%W)-Uc9Jst`nVJC56AE{yW@#h)pE(MMsJLD(FZf%6|8Kx3Zs2- zE)7cj%jKvCD&~mXLF2~lbpP5{I4CB|-V8HB&9^~dH=c(_GnBcU#4i&2^dpStY{TB% zJry=H#F+$pUwn9G0@wL>V%jeL#+SR?;K2EXxFa$JisC+CXxmqsbVnTr-%Nu#E>|kC z{tB*rHI1pWUk=)}b(_95x_c35yD^*h-r){yI(UG0F>@>US?MGGm<1Kr z1lZH3xLmC3DbUf9f{~x!K}p(@ta$mH!yM0Bg z3@jckh25@+wEle_T2u|=mle`9JC0+VY^~s{#=XZ%Eq_Q>G~gdeyUuY>k3#N;V_2cN z0=IByiQe&v?5kmQ_%nGcYCWBaue^)NHsc_RU5gwn-V<|}ye=6O9Mj>{f?ocVj%Ltn ze?s1i=tGCk0BQ0!1>>8SP{ix*7ls<_v(hSbQ;NqKV`8lB>Sg%5Ih3x;x{Q{3 zGhqG7t+2|-2K6J_sO|e(80i#>$>W-|_00u5w@IA7e|d#AHx*-5OE=i4&tl&{X(V-? za}ZP8XuER;Zkd{atDdZu=%8rciU8tV zy?wY0?FO{a|6Mq|yndZbiA#n<@jA?rOE=LwI0XgE&%oM8yZLkfH9`Bo?Kq-i%A9lF zg^zjYQ@KStBN z?i!4>)jhbMAb}zl%TRN^GaTsMN$1EP$J-JIU}|9$DruPDX;n2aJb50@aQ~ATsoD%Z z8p5+m=(BM7+KmMpDe)dF#^Jd6F#5Y1|KfctF5_4szsoBmEEO2lO-o3Pm?~>tJq;~4 zeI@C6VWjgSmxXouM#VhG;NhEi%;CHU$q7H%$!)AjwyZILTY6hwCOFt!Adji*!K#8EoS4l@y)c;Kgyzs2tnjK2{!oe zB(@^Yiram);a2HApvu1iS+R)MwRijF97#rvqn61C8ct zDkUUC<@hJ~E+5pg$8}Cx;osZ!q0!)3H0;^RNLp$T&Vu0Q*m~rAF znR#dl4j$Zw1HqheH3R7b!vf+q&6IKXGKM!%vE{9^IH&yjgKX#OsZ7h&uRI?OZ#=P2 zkY+A1LW|Lz7-wlt4j<8HPR5^th*?X(7&)#FS%X>D_9&eaO8pb$nSw7nOD{MoVf2E0 z} z>07R$(we2Lp1dJrzA2kn+H6DIngI3W9#~x6jPaVAP-nFQ^LpD!^E_<9!-YS&d2}_r z957*(UJl|Bn@8xiQ-k;F+FUy9B*KW*PbxpteHvf$0xMXjdFYXvhmNMTbiqOivT)fs zcvb5~+UHh-a7zXn?udcBujk>ii8@Ms7{Y+zVY=_5KOD(>MXhFR$2%N{MXqU#44ddM zqFfHULZ*#|4sVBFbz5N%$048L^#Zk1PT(@5CUSeF1-7AnFs1);slZAkv5UEj9&1tzQ74L3nz=4=vwb zB4QenY(xAyvUzX{D3mfR`_`1TbDB)D{|wNY#0v7V=p(hVu7PttL0GhqM{7Et@Veuo zaGVu`?w0p(iDMUTDCRPm)hYNV_c@i!SU_DAE8%NlGkJKbgl5TdeOWV6ur6B%&KG!W z@y8I3?cYb_IH$(P#_c!*gxJ9dbB-lhiuW4BiT?ax@O0q#x9Q7ZAhVhmobwyp90;@@ z=!f`gD)1`!4zAj98C9DTz~}5O&bRakjAyKcUlNJ9%PbVnnSZ4gR;tv-d=@{LKY_`5 zI#OX*;|-g66BxnS-=H?*v-uv2ONbR5FPoa`vV4 z=89>s%B+vbE_zA*zjWc$>~t90QifG+(R|N$J7CJ>iyT*WACV~GdPh~Y`1A25kX6{l zbH92V=60^eTSqmSvDHVIrUDbzaK~I`+4m%3j{gTNj_h}~_l3ato zl3R#}o)en>x`2lc)Wc$>2+%s8j5bQXRH&~MZ?+;|Y^DhmRL$euzuLI1ssd*Hxly5` z&;>ttJmrb~6lFKR3&qWMF3?#@4`6;q47vS+8;Lj-5a{oben0rZc5XI%1KjX&*^O$;fHLm|yh#3w~`Oy=8 z5_T+#I%M49xdj{t<1J744pI7~vhO7+@n>%N3ZJE6qXTVh-NBmxvD*CV*JdV#Z|t zC64h}4IYyS(TH0NRaZ7cUAqNc^FbcP^@~A#1%rOgxgcj|z+?#R!#kp3XtE}Q&MU07 zNGd%F=Q@m-5dAiy>9Po3%g7LO!tr35Um*M20`-1Pr;-t2V7@z=?2icnRgM98PyZN< z&t6Fdw=HM2oPWd2qf^0koi$@`{h4f3HR3bfTuyY+ZAkA~$vl3&2{&KU!%n}YV76a? zdD!xf+;+W2EJUx8r0JI+s3j8ZW)_jO#!UXuFC9DpVNi;h818&A(g7PDN4Zt2@bcb%IGY$IoJ8^Kd~`82o5%8;kEcZD-~V7U4Z&^J29!Jnx4HThYH&A5PVaQnmrB2xC?hdKTew!e7GH(HMOswE=kBhYi%Po zH~2a4knmGDRB{P-cg|qU3fxhu$q~2S_TzP3DWMBN1~R@bV`h$9fNN7ID1L1Kx!*ci z;`WQn#_q-J%vvI0?n{>Yri1TnH!SKpiuXPp1p|Q(yD2QBP;j;Jx`pWEt z2F{Os-ai;pRV3MCcc)kxQg5AilXW8H38g_)#s}v1;!KNVLo*8)N&i+N2itT)Kj;u~Bp;>RuRz}{nDIR4j7=+et2e(U^Uv1bX*y#WwcF`0SD@dV=U zR6*Ediuzs|v|@f2Phe6cscufg_LHXA3i%jyy#nr;u0z{O#fnD`+BoUIt8lxmj?8#+ zh5t*b74yv_!6o`C%)JlnXjKNcvkjvra*NU5T@(B7-XZ?m%-JZVYTWx>hnXZAf;swL zAQ_#`yJxuqSC_P*rN>c}%DO+W>}Lem}a z%+4tIw%nVT|I(#_{He?_S228_`Pz@Sd9vR%V#d6@}sWY)%h~ zi6!F``J42}lV!~51QmSME>=fHhb^T z^FabAF-;$Xn|{z|F7l{#AOaN|UHQ^KZ$s9hezf^FpE-TyJf!yY(P}wi$XUD;i)2mM z_Csl8M68WyI4tM24Bde0vK6%Cv;h0#Wga%(oB{PF9HahKI+j*w;hmo<%&L70X^YKL z7z=l0T|&Zmna?X=P3}F+|6B_LZ64t6XwR&@_#A~4#hE#+x2W>jPrMV+O3e9`DZEHw zZnxhgLIv*^(?XvDo}jiYTP~Q0!k<1_%xx~9#UiuV`oB{#C;u=@Q%2BI=@4I5d_7bi z3&wFz7d-XhAl=S=-UEB@aryihVyxGLUu}ZSzx=rfYOmJdj~(TpR(OXOyUQP&8)V5k z-)j(c+>nSIxd`nm>d3|?6lfl+ z^mOJ}NEHedCqa&i3=V7hLeCTd^e*LtRGSX=zKp;F`7QMQu4&B0(_9xMI2-=UR$+V& z)}Vl|A~r001Ak^cfGn$*sC1(bl*VV0s1AErcs`8FhPd@CZrlXX z)BgxJ^r!PGF7VY<0ze+q;Q5heoViFCPB&iTn2)MBa&i_e^Lh=B&w8NO4<}-n`h#3< zW8k6Z8#?B>3TTWR)UC;dZNKDLD+whMp5zLB-ePRrpgf*hU`JKH$B+aid-mj#1I)2; zU)W)tibeO5p&(=_HV2l{(Q6^-zeJXq_#~r(76;=4K9>#aiGstDCrO2;Jnwptjw5}-hWY0-5%(SVK~-d)p!uvR5Y#KiG^&?l zKyfHoo?3#Ts=BmgffMO!!-lYI zkpZ5uJ56`E#Sw#tPw>igL0qucgo*L;1)X^@B)(T2_wSyM3+`%RXq^&7f*LD2dK~T2 z{4xKNC2M8A4g%ai;IuzGaI8LwHi^0M!xYMhJu3x^TKwsN&Jh3Jt<_L-`Z+S)+%xO6 zl&x7(Oh4|LhEi(hQJ3q5{d+3GI$BKT`t{)`@NONR^_s&tbQj_Bj+5j|>q(G#V+#uL zWz_Zc0o3ZzWp4M+U~|_jrI89IWVOC6;F-YkLA7ic{iwu7+bc3!UwN$mU4%hhGwwS! z4+j33GZGOY_~0(Lr3;>7BquRa(Bcb8ZRL=9nj+&9#9a7EBkF|3p<%MDaj3Bpp3T*`)8J@Yj4U+oZ*K zUR!|be-D$1>pfxav}kH<%Q?hjwvd{JOVC$PLkFTAVgKei_&)jqkq%>^yNux4m7JHQ zaXPL(*hP4XiQv{D$an}}gAT<|s?}eO#{J_oW`ixJ+Yf^b$1Td~kYL~M^@cma=J+V| z9xdpkSMfh-2 z1qqIO3d-|#;S6UNG_$+{KA%oP-)?WZZ}vvq@LxT|t$6{5KZ&s3rN8;-8Xgj*x)Ycp z{sp~vR`PxZMuLq&4B2B_&1(n}#;yb0tnO_-cyqa2jVpr8krl`A*@BJGDmFmsZ7*Qp z`B<2LwwCl<>4e^RMOI}R7cHB>QnTISr1W40>MB`*VRjF7C0uW9!aERei^P&z@w@lzTcK*j;;CT+(Th*iE)HCq+`xHjI@)Ych@Z|aVoQI`a zOW2xVDO%GX&(9I}WImtS#wX=*L}le{w$M10UT#Z5wT&MtLJkA@)%})+&pJ-Ey)WR; z6r{zMKZ4+WSE5||6}H)F;FrF$@aLfo9pPIZ`XS)q;GWvkA zvP=etue_^?k1+gWJ1r3v!UWb00~{w}y5dyEERt(F%AT!|DmskEJKo{4H!{pXYcTk% z_kg6FSzsLW1(IA^z`kcWG~ShF-#@SBJhNiVsL)qfWiG)u{g(q09$nD7teZL?YlkQc zXQoO}g8kniZG+V?Q1XR?__poO~Kj@IVNTN4+)i6LG@Fnv#ZO!aMksdNGs;Ceb4sMwXb$S zO?*ALesKjS&L7?{0iL=P`Qch$a!RO!o@I#H0#wtJ5|C4XX&{SeH4Hm2ACjJL?qxy?J@Mg?7ZFU_LTz3pfvSlD zJ9=`AU!b>!#2hFBuf<#^uCxsfqBeOhzYK4$E(NPNBi!9|0c~@>pz)9P*}GkVy&q zN#`BR$F?IIaJ1U(@99M_LLR!*kdb`Q~S`i|&+J_#*r`4Dtomr8qvfmOdM zlf7G#jjkW3$BuR47qtj1uMuXBzexioD+5CxltNm|Lz>~ZoeuW=DZlJg2AJ{()_YIF z3q}%nEkc>S@>_&;es~*aHHfhluem){@eD9l*n$<(cDQkPgnwvjCzaFIp{IyrQ>@dBQV0IJz;g+8-a7KRRQVboNCcYI?FEYrJ9 z>d#?Nk8IVioLnC50k{SVO&j|xse_WGFG3tZ22m9yX+3F zT>1gGYF*|JCq-~P(OKXz#xYr*I`CRDhT&m|4VFs{;;EG7Savvv=OCNP*9(q7aiL`D z+}?`bS3|JDG6~lNU%}|!JMg;n8Cd_Q=La>JyzlF%WK>GhE=n4TPfLnQWrUCs z4H8)qGAbGG`#M5KMkx)76b;dm7Ky&+`{VcV`2Gur_c`}{U9Z>k35N$GV9lIYRA}%Z z$oVb7i)VRYAy|g*f{ud7U4Q!c{WWOVE{w;Y9VUtP3V1DDgLU|I3zffVRUElLku8#p z!*(K$&Js^?XPXL>^vN7oJ{4!3zXjvY{~}3IsybT7In!510wHpDB>j4}1^rc5VbryK zaQwS9xJG9a+xZ1B{qi(+F^Vxp!yB>SOB4(w8otnt^+G5F7p%fcFGcDp6pbIvyYYzjE`ZBXq{>PkhyMJ*gt1)en|BX}HoJgn z%@?}RKAY}r)MgHUh{G9+Yp}%Hg}&_xLA9yv7?)g*GiCEpXhkZusXYsI+ND%&tucDo z@R%@nF6Xx0nn|0}gtZryU}|I}Y!z0;9fLinbjCkgH3*#CuJp3)PZ z`RFZwk?hg1L3XVQBrdv-!FJU&XZI3#Za~S9st&A5*}`+&ri5>YV<4t`n8Z{{VCt57 z^5DcT*u7GjxqN*ArnUy->Tg=eFrj#G=XCt+cb2byClbSU7xES^z74B`*<=)9*PGG%_k(!^B8|hwIU6_Rt}XN zNM4#f;&~jN%xFz7MkCIjQ=6%XyG6{=qiQPd)0qi#+n@2agok3#v2IW(xW!9KujI!V zdtvJR+w|C@pRi1#m&;rkqK?Z1_GKyO@Ef=fhV6Xt-66&_*7fittYlc%J#PGVYbm_# zxDtmS?}ftC1UzL1X{4PWP15lK{gFa+(cpG_&(xsFtC-TnJ9NX$57-vg&eQ6Tq70Wm zh`)7)Ts*D~J7zycGhZp@C2u2ctGxstLZfkCl@KtOs>pdgb$09c03z+@rY&wr!{Us1ghxpj`L5*E7VJns`xa4o<@B5Mgw}(F9@44=5x^gyYG&+Yi%zEa+iZr4nIe})KRAz>y^B}c!J&l#( zdWbLAWAz_nqVU#$AOG(VoR~NblKeY3mz5%jY7zWsbd6G53AU!o8EK+5kxo=$U5(!X zPd6Q_4rws=H{C!NUGA>bP)T<<`O=|J%Fus24)irQLO|DN{B4qjH*QXXTig9W`O9hg zq;@UnJIgcuF@tnRPYa5C2mEPn%Q#QYhVfNqSdfv2N2SG>y&KO$(`pOWx9l~{sS!cD zj>BNpd=%rJucxEcTu0m^lBgHwWB3Lc#_P^Oen8+|o@?`781+xX+pec+nsO9B+T5I- zk*CgndeFyj&{kosuCj2ZjC0Sg5o0g3XklW`MqZoCIsCkAF`V;NVk=gO6NT*)7`L=A zC=1z57jmqq_4e#V1vT)uKC+x)Z;!cIM;P8Yi2N4g>2Y*54|U19K_>A(m^^5A-j2e7qV zNAvnL=E06NoHt-8R)610p7@?cr(Z`wap@!4_wN;~n`(t)3*2$N(kymH##d62C50m< zTI|R9-b{@O3rRa3n_^`b-tMEJXy- z|1}X@WeVW;w0?qL2TGqKLd4)|wM$*r4(pyAJTWFjw6PI=Cbg?y$p8wWYQUKtcE zF~z#xMx1e~1S)HeLt?lAt_!?QiY=;eWOyIwcsrBATR+Lrm^obUi=aJe>Q;XANT{T`EneP9M7?dzCa$(68F?FMMb zUV+#d2e`8&mxY*b!(aJGomo@$g$9fQ9`lGty{$iB&Dsz2vien&t>jpAIYZF@;{e4h z2k5H#L&To+kb^OQNXcC@rrK*SsL1(2D?QDOjtjDH+qC$-gJGOURt<-Lss(e30WdCsYNx>CVa^x0o{V5yv@6jNr{CpXA-BsZz^s=NiSexW8bmTv~ zFTwP$>_ZXde-!evNX(Ck?Dz8qSWwF`7cQKF zPpD)#$hzgA%#GI=JYy2GpARVUrH3v^N&;h;4noVOGsXqU75QEfBst~}j*8u-?>o-Y z_IH7p_RI!l!V1tnRGl#k@ul;8@9}dv$K0{)8fcIUbkv?l$4@u{{u{wjg;!*O{5nW1 z<@~K~>*24%ewe3xkBIKlrs{=3G?kGhIxpv7UN#SFxgM10o4atuGYu~6ox-ta!)V=Q zQ?^sF5SEo>z=XpMboAOFs;cQguI+l#-)D!7U`h`rEku>W?eLN7Q!Nwdp|z$Qlf|Wm zj(%N3ub$B3<`i}~h0978Z(BfoG`xAc^X`$rE@%2!x|$iJn`3(>%Khz)661>Yt^O})AjIZZUP9NJ`L+@7T^ynB}mFY zg~xhE;q&%NywD|s8EYzF!XqP&Yj!TxR2oIb3*`P8yC~Lrw68 zCAuQS;@}jLcIga8Eb(X3%>cY-exmEgXJ7@_i`YD84I{9!1Qxjppma+j{;+#Rch7xp zRnuFEt^dsdxg>44Tdu+sCPkpjvg`Dm@gcAsjU?H=aWHuKH+nyh#SQ-ZIVaOvBBP%} zWzO$I`$`R-{XStnyMF;UH(y+_uf80E>u#a;p()&V%9wrP@)WmSi6{H|Z&nDU=1`Nr zr}?+#qtR&Jd=#nK1r<>R{OBX%Se`4*2+T^w9Tfs)IrjwF&O_rU=KLQ#)ILNMx+3te zNd@fPwhMBktzgqDX=X*}HmVtJ2>Rc8X!+mo=%RU%=s0oSao_i3>*sR>t;T4gUKyU_ zc6dU|xc&UcpV;x^8rW~FgIcv97(OXLO+Un1PI*2GF+ye-cVQO%PVYv`7ExI4Y0Zj` zDd5eya*WvgPCTgGL7KJ7fbSCp7wcVN`mRVc>wZJFh7LjF*m|yB>j655HpC!*{UqK|Gy&P>!yACr;YET8N+gZk#=K8@(P%((3XobS(V@ z>&q7*lPbd234fuBWL0qQ$LAn6U5hQx_Cd3k53TZdo#wyH`G=Vze@R!%V``U@N5%Do zSb-Go*x$3DSn=A@EN-lg#}!Mx2D+6Rj~r-s90X5V^+_ zL>E4Tp(IV#?OhaYPd^2vUfEz76$OqaGUHr-35prel{pgA-Lwpp;Nd&h7XHt&%pZp2{`2TD}c7W+j8B^;xJL zRQk8QQUsZrf6KhbXvImb2u3ai~$n+t*J+WN|!~;pxF*o-CVS^c+tNT!Yv3 zGeLBg26@{f$E+YKu=Mu|jEZ-loi{4bZ2ftB!m;K@{xVz^=o@hg5Ml=e-;uwky)oL< zj(%VNirxu3OJ;N$fX-Z9h!nJ9|JxDE<$)HHDcgA9zL(oCJ{}F!cN!N2brSrFG!vyYc;8{wJ z#i&7X&^?IR_mIEVUYh)@-GDCdKk=8mI!AZwoy4S+7?hc=P5WPD@@Fh;;l1N{d&jw6 zBEJLhUt=sT@Gi$KCNWquc@6e0^2ffeZhlq8YufSd83^9pie?dtjQ0Euu=Y_VI@~aV zRl`L%`1~HKeHFrl#he?uI+_YHRkY?D*UkBnO|p5O`1Z>~j$eF_w`NR>_;tJB<)%8g zAE(MV?H5MD`%Wn77YhxW)4*@pVt!42C$wpY!kSO>aP#$xWX88v@}}VnrdFRsC(}Z3 z{H)JvwohVKrB7yEx0zvBt`g=~tmXTPUM6P4_8{w+2OSf8=v&Wn`tI^VYT@j{ySicp z9(y9mjJ{fhh7Wg8yVYy(b3y|xP2%|Pl?QmiZ55m+Ck8LQ2!xT_x3GDZFIX7=B)QQx zxM6k{{5WzBl`1MJb4AT+oqQ}TB!|I5N)t3LD`S5B2Jp0d&p-1{oLP5z2hBBb#^WuP zpl)wUPjS6<{iGU5u&(6wf0JaZdcLDwUKgp~?FQeqyx>s5TToH$#=$ca$m+T`#CNGY z+jSAgFamJ>^ z9+RCH!}g=;Q1l{}-j@rZYPYZR?EDAlgG6oI_9GhW=V(A`z6k&Amo(gJc9VRR%Y$;$ z>$u>NHlr)LmUSQU2gy4Rz$Va$eOlmz@k=z`O;$?d-pc63ExH^o0sF4*E5;#%PhI> z<#nR?&I(3$Er6KWJIJZ99GuZOO56&>&^z`Gsx^yYPTF?Ze|Q4)d1vFcor5&ER)P7t z=r0LX=I;6?!mw?#0JA1=Df8@uFdVaxhiu6S?5wS0xbCST$^1G@@9WiL8|MbA%K3oR zX^C{jq1TXnjr$EBU&PpEUgnkVx`s<`en!>8KWOkMmwsoSL*)w{%ocu&<*O$$W5e8{ zIA#HCu~P&yjXhWuzk&F*M&X5WVN_x%I_217t~EiOryOf)q9qhGDYKvc{DPhbw^f)w zb2Gt9R&bwlefRfzb2Zj1tiP8EVu#M**Aoet+W49bf6W4?ic&CpJ%v|2KL9f&l}XWE zQ78}HR8jGp>kyvHp#weF@k~u8Tui@%zSR!c`ZJB+u+tdrU$^3p`dr??p#jd5XhBHI z6a4h(A83>%Le+_TJkQr5WT@_k)x~^$a`9~p1W#GZ>b{ocA9s^x4yz^7u}~hZGJQZt zZ35tdt15|EU&K?xW}16Tlx@AI2N98B7G8?wxYT(soM`Unyb^Ml+ha$2cYG#7snen4 z6E{m#`M{SM7~=H>za-X&Z_u>8`gnT|g`tQ=_~Cjim2Ef&4P)m(J@Yq+wikijK@nD9 z(ify&x1fWc1^M^QaKX3F#M985|K3oO)oqo=7N3=vWT^oYB5e54Y&i|K{8NEk_MtFW{G^38?2* zlf1*X(Js3eU9Oly)bbIuJGz5snW@Uk8c{eBm5m90wrDe=NIzOCvQeQ*>_xpF^q*Kb z&gaMRj=!zOnZg_xGou`%Ux%UovO#`R)DhyEw6R>aEr{ngZi4fHT zj`p5~rB{z(&igK|*AtBk_y@_n)h8gUrHwj=@k( zYd>#<=77`u{nu{slU%#jMzit=|a7D2g=ztqg~ z3}0sbN%${nCCJ`hgaI=+|II%$!fXnoPZtkSHv^<&x?E?Bn>T&k=ttvU8PlES%gB2x zgu!@W*4=qIBXnsZ$3_1N0n_-z;KfYlVb&}7>l27lUdxFy%|!j#>Daw%E=VfO=805% zL(T+B&Mlk{V)768G1I=1eGQ9o>QXJX^Z0fgzqAD3Pi}F&smf2oF7JXdJum3JVMHzpGIY886u`)LpK@! zCcN zgDP{=G1XoG%M?{%yJ`(i(n!WlIEg#;>Ch5#FykMtSQri=pK$SNZ4B&HsGn#1U>wplP3vw_}9*Z zD(l8#(HCKMlV@H<>u*curT-3$8O$P`vO+Y*B?gCHcfguwm%+%IV+!{fLC3U(%!AJj zD3m#qu^EZt958zri*I-Ndm9>I&sjAnC>4g}eWI**sRj5wlOvzs7U9sn9-fGLG0&CQ zgNsNE7I&6f?(H0<)qdaT{zfTQ%1)5!6VhZ8Ew1xEY5HK*gAb7MqLr3Sn@GFzvO(1T zD(UuFlz=TOdL|{iFKFhs|%Vq~b@?$k@5z+uUU7uB}zeX3<6l2TdKDy3u z9rJ^kL6>Zr0ekGPp^|4g`L#A15_@%t-QV}5A=iWUZ@y0EuH683KXfrLVJm9f5#_7R zIRlTSoM};xEMuG*g(YM;v#2{6+`^t=#gimb_~IoToHqkAGpAtka60|!`I4J6C6nyo z8LZ3-Pc$o@#d;i?jlNE@=;0HMx!iNm6w_i1=3S#BQfELRdnv&{6Qqi9Fe6@pjVO@g zX_;j4ET#E0`Idb7m5JxTD#a4#S4;%*P8crdeIrw@yF-4~R#>C?p3DtA2G?KiL`%6c zdM_c>>PvAwWG+1cUEc&4kJI-sC1EjMx4l5*?%2?sYaGbt92Jfqq=k;Fg@9zmq2-cK zoWn8&v);VrZTKa}w7o9EB@ZKcdnQa^d$TN=jI~#}-f}Ta(!0Ys9UJNQM?$!_DFdG@ zEJ8Dx!)Q7^gub7Bgm?K@1sb`o1&e^GET=xhv_U`eJ$pInwK<52>)LVC+$`vMZ-TC` zHZrT$cEQ{}O`Nb&hP|&)Q!$V^3uYE_ynz2j25`Ip?f;(_@P7bGk1*Djyk1MhBQy`{H)n?*#-9hNh z5$?3Tj4@c5KtuYK&?3K_IIMjHl7=@SVjV|{2*1mB-u4LvBsN2_%1P)SX@r0o20YR5 zhj@ST9n@HrNCLhJu?@?l$jlr`*e+ZLMaM!wW5k}f$H*AZ%njgM%RWbA)n0NYQ;^*v zCCoP5oyLg8G|^z8R5-;cXtoY_l$=v{LfG_;4#h|*$QZ_2@3 zzW|VPmSi{CUqG^=lg^tvOhwh=(W!(Z8T~npn>C#oV{ca+IQE#N&R4=t+<+Q52PWhR zGZKGq(q#Ec8W-XYO}~EOlsDHZG~QkVm7dMma9b8v>%Bo0ZepT^mPvnE~w+gjt6(FG1;(0Aqb-2VK1IXQ4?7)Rgt*$NV6HL%1r-t3f?CTiOXbnUI$K~ zy|xCVKfi#?R!j#on#8!5nX=2c+f!x3PcFTDk|+8_szRk}I;wR|hCtz;%&n|c{}WO3qw(1U@g0AVAHX!IN@X; z-a54jRvE@1KDfyb>?zl9S@7Qo&sgU&#Gwmj-Kggc91)CtZ! z$=N841|CArn`k(7p^7>?R6+kbGiX#l2zLY``2$UaU2|{>XXSfJT0c85wHj)~xICXP zR3*c5^EalWvz2U-)P_~DFW>>rgVE28yeGxwq*y{020w_guGb%e-9Ih|)_#ljy_pVe z*Dk}$wl-ct$OY))Y)m!nDc~o%9?qpt!!{qpvvEIy+v7@e-np^zt;&{OJ!}A(j}w#R8SX z#2_h}YmMuclhXQ$?5IH?)=~)=o%#p@WK?)keXp=}(qD)&UjT`(-N0>?3w!HeG%|H2 zkhiv;nl0bT)7jWc-MU4PA8?)sIh=#1`rNc9Ut|u{ zt3XItG0HWRb7{uuX-oo^qsYo4Nat>z&etkYu6a2%8I)#bWnaRg<9&2#Qz3tDa|Bf_ zn+2uD!r&v;M}n+Aqgh)vYO{)<7;6k$>&$UzO9c)5s18x{h4HWZ7t8x?lbDZo0~oS< z9P&~Zvsc^&h(X{l^f|W-+U_!p!8Q%*cA}3jD^W_{+kS(z-Xz+7XDKvX+Ak3wLTrC?hiYvx z#2L%e_|mgw>B71^Jlw3ptg;j)ZFOUi9cV)whFW;X#2R=*+DYUyPn5r^Wstr?`GnZ>nrYq5&JgQN6{CS&ooV zNffp{qDmp0h4uXju*z(LrO$h)u@7P7xE55(&?=68rb`S`HsbT@>CjPYLxb)a!B?)y zZR5i=*H@aNvWY;&5iK>i|CV%cs(yg#JqZcFQNz=!MgD4QZ z4lNrGV7|OJMs9nB*2-65>J0%jx$>8Mc?85Su>;%k;^FpeEf6p-!O|58IMxx)r6cFj z>bMn9c0~l{Tc5_=0h}%GT{Cey&7y;QCI zil*=a%U)r0+D%Y7SBvdpJ8|8m0n|Fa3tgTSz=PB|q&McLRm<~O64a*7D^FO!nk5Vq z??=n{{T~z|_0exmL~s-03@tFULk9gf-r~s3Q^6!mhdNEz3I^-8V?=u?SS{4!tdu4Y z`OF9WH`fxMm{&EI{pBkj-^1>)U%+q z{I`|#9b+&Q$>OK3W9e#**Bs?(2#W7>_VcGDAlubX4%$)N{@NOIHf{r7`)rVuaD`vT z;_2A#KRDAw7bCAX!MqLc=;qLFFykw;qRWzTYpy6879L01&8G5HBx^8vWjEf^muC%b z9KnWWP1aLQ9hT)J(ALo+(!6FZCU2R6_0>~A>`x)qk1k-lg^z=Keitk=Ur&NMA3&S)r&%GCoueX~&yjC)3Fn z(U+tBeY4oK-`5~cntQLE4@XO{IM_ebMjC54L-gKKBGY{n^@9(DOGVunGX2F?}j#Tm^4s1d5RzW7|&~LhZaunLd^;j+05J~!u zYT)sS#e8oE8Fr{pla34&lFq+Mto8dzP*JXqU)y^tj&2Nvk5eDw_h%SbIUGbvtOEh zXRA&(kqTtTUC8!M&NdjHh?a^4#M7i69Xk5aqvb9N{)y-1$t{Bk3ESbUw*>oyy9xIG zszL9*Exb6JdV10>haD|ux#looayidhA(;J)UtTy)7>@fK3RKuNlphw7qru& zwSvr=qfbC#dRlo_T{i!dRy?fPUr*v{5XVp5r6TuIY5wP#SRXFPJa1c%p4C%1%glbv zTpj}*1y8|$%}cAA72(Xs-jk>-9Y;k^$m3n9%P?sbOI!W!puvx~I}+Sc@_PwkD$!5A(PbS^o!=8Oj3-uTq%(Bo)W& zZqd1vOLX1}K>t^t(X%6yXlvVo){h2!ce0n9@B2=>Lel8AO`pK;@nnb=Zv^I<2}tK% zz^Z4_@b<`h%J_=0u}k-1{LEaK+$jR7vYC(@rGrx1QT&GOLOkL5)3{rqBUU#X(!QBD zP+Bh=T4%)*AH0qN;x}lHW+;C8F$LL0aVWT_2~}oaC%x|w-da1rxK9mcX=Za)Z%tM{ z=?vtwF93TfJ#g5|{Vzy`QN!p~-olXw4PZ>hJ;YO>FUUk-UNDvQxl!nJor<0D1u!o70Y)dS zXQFQh(Lb?V+HU)5u3ec=4_-P=&pCOcxa$b&InRedxhfKSAfBj73%2$vN4uIb zfR6kOv|rqdUFqG_;c*^Xdo-aXH%+;Gz!qORZot3gk5TbKC9H~ff{a%gsJTU+(L3cv z)%1WdtA9nT5<(EY-;kY!k2ouQD=hf%4U3O?K&43})zhlLa?vI#QuiKLj2Yw5if~Xr zJrSI4=wZrJ*`H@sSr5|E*24K&xCCqA(ZfKjWgR^g0VV2JoA~zV$n=24RhDM@sf(0Mqbv(#9 zEg`(h@P$W8kvPB$pl?2?q1NFv z43N6Se;o4H>ibGjc0gAK4Okbj`_+gUB|$hm-;T}6Nrs4EDfa8M6U3|_4AKJf$P``! zw#iiE&%X8a7!|_1w=SWhY7uXtj3GFg+pp<**0nk>Q&j-R%gZ697CJFgx0DJP(D^-iockmMf5z}0VG zV&5k_#&6zbl=FN|IyGF-W=$rSrr(Rb#b;po=Rgv;;tOhQbte{|44H;jj$9G4i(Tz$ zgy#?ZgEXf|YI;wYKHSs<50knqcOJ+=1&{Tt`;9Q3^}l&gk{wTkwjZHpT@$eXei{sL z=LDZ)i^2417d*)>1kG2IvDrb2XGwTqZ$mEm7~)2D*otro;F}LK z{-lzDqcfq+>mLkaEe3>bg^ABa*kP?2EQ{t6Ns6Daf8}8)epQTrR~wR0d5Mapi_h|$ zKJACLs2g;|Oo!R;If;3Gt&8uP`VGf^xTjCF^D#K-jn=5HkL z5@&&tOes$~(G>g_I$SoJ!_JNIyW*=uvCav-FrZkHt52-sxqsj1tUZT z3PE>)8<<3&fljZle8(SVQ1-Hv_aj4zmDVeS&bN!9Shfz$+O5cvYZ7dlZWlSW(-)t& z90TPz36AQW1f%<->I!3Ic77#AM3`YO8UHSeE3& z&Wn4{XIli6_q*uBFXE4;j**YyrMsjg#nOI~1?!BKEt zmICXp)Y?Zfphn#K}_QuG+t;)-KW`+rskt$d5Ju8^;ZOK9uQ$GYAooG zB$qtRTgm&arwIZ-_c49F-MD#oH~RI|kO|qv#PWoda*1o{@;91-u`V6EL28m;L@-59UGYknMT}lT_1a#>ZDA z?5Y;T1htW!24xVe!NBdVD=22hrPY?3;_aUssd{1xHf=fr1#8a0hTat@Re1tBmrvpa zr)I#Eti2Ff=|x|031fpUW#-(C8SGT$>xhR( zJ<8l~X)i`yY2`0*+lRgbx~PBNi?@FA3o>vj9*V6rn0@s;T#`y5$v_Pfcq26F<2?|# z<_M#SFCop~BxfYs!mh~kMtkeoxS>D`69$s`kAyD}7qu~L^$!8V()nzeuz}UHQEhY? zlw_Uk)7kXD$LHLtetnr8@`(8gFC%zhi9Y>TJS8<25G*xunP>K0vhsf4*?zCPi6dV=> zQ=Q=%SY`AA`%C|Tyzn&qsr&;S6Rp7HyE>>;)smTo*3iC79lQUWLfgD1a`nv&oKb#{ zterla)|~%N1Fe1GR2SC-uli0x)q)|iy%`MB!qI!bEZh5d1sWXRiVDHmc&ua@@$Yj) zUza-Msy0|@%-LAqf52yEA?T^O6>D;$V3NVEiXG?eanglmYN0d6zdlU?+i@{#-+quU z9B~l7Kl%pQXC#>CxkYqFhCI7)n;%@Apn`dir@(M{5ES;l$H&?4aNi?s+?OWK(r5B) z;nOPoRUbhPSq7qt-zlhkm5gRjR?~}I61B+s2zu*r1ia3>7_D6h0vZaa|2c-Fa=#(h zEfik6WWoI{LC_3YghRH#PN93a8;vnCw1U^`8HU)WFVOx0jpr7hSUOBO<7tpKazun(fn&*N1l31GR$LM}y+&qsyp zIEAxm2K+0-?`sl>!07h^U!Ok5=>pz&@?v%YWcjG zxatOSjjmSiu3KzHhP7e$>7BTE;bqwBw+*K3zKzd>bMR;VGw4y)1+B{}a9NC-DfGI6 zf?yaj4}L>Wz#u8o9kF7w81Bp_!&Wwj5o^IqkWqRHZMsz|{D(e~xr;dZ_;?nC9$UxR zj4wcyt^=-46T%gDa6s=AE-^Pn_pyoS-W!KvhKevl z?LJDpnZX{M_Q=xwg95E7X(Z1;88=xPVBJiP9*$E{?vxZ1*j~cif5UioB1hT{`OJ~{ zq*>P$9pv}c%{YBdGNhCY(1Sh=6_PU16>ryx(FW02P#pxS+&mj{eodgAZ}$@qeQ~&| zZ-Txz_JWW0DgOBBjWGAs2aeiT3?DV-V`tSeEDQJoDtioIwckOy-KdW@*K`zg&P=iD zT%1s`&wLBE>)ql^RaPWs@G}1Ra1t`UE5YyQrewtZHpGv=guh>mFvCg;;) zc6EL=T|7M!PCb52XNeuBh8J|Pihl{p*A0-uh2J1}{xXoM^do!cA0TE^Vu+z&I25le zC8Cq%U~EGd7G1fDNgcYdb-{JsHC+wL@aM5s^I33Teu}QE(P!P{C$j!KZ-aO3E$a2s z5=@7k=#T!1Y<6k_|FAUf#gJiQmRWafjb#weM4Y65n|?<7uTyFt3mjCfTU zg5_yvB5oCpzdeN^Dv7fd^iE{e?o%Q=cnWt^hJ&f24>+m6qdTvJ;6b6Ca4L8e%aSgB zn2Z>#-?o`bcgH|v^Z;Z*5K+(-{;uYNN^^K|oda{}-3>jIp znC}XD z99ti_fqly%n*Mq(h%YB}irFbR^xsVgpDd4_V->{EGlpD$`iX>!I#`*>T7XPjF3JUD zfb$YT-eA;rxOsOyygT_AJVHk}TFza-(m`r4vjN(koySIl-8lA3k|XkU(Y;n#s2^_4 zR4*+iWWsH9HF(5ZQRfDui@s7@>p1Y&d4u=5#URD!2UK!9(-x_x!tkz9w6~xP-tBClc7L1jie4;fcvF?yhnbHC3l$z`Pqc^iz{2B#L8>jXpL+ zS;Nqqy$}&+M|9;iSoMxf*pe9zl6roa9+n4t3dA_-#aF14m%%-<$a8u7lmGtFBlxjv zn99;Ov^(GqsoU8>;}t(b3t5drH3mfIa}=-eVFA3Jqd~MbtOQkwCfv0{hBF6Wrz=`6 z;fZ-H_#_{L?{74)@6a|H)|CLK&Qycf_eh%GS_!@n{piHPOvsCSNWr5A%4P3C&!O|! z|2r6%UO6T_E`pw4e3aR)@QfbYrObQSHIVv`KU|jcP#+^$; z1&@zo3uhbe*V{u=m)@s&O&hQ}zMX5cMG@^CRbX@D3=?Tl2bn?A=yQrQ?70n4{h@lu zDgTE%6ZEm(asn0Lz6tx!5K#PS0p8oY$f1wEWolWf;3U6-HOe)C1r6iCHqAwkSU>2H zjN}L}t+3U-2v*4L#!$;$s45i*eal&Vkr;`#+L!Q_-aQ6gr=#e9iSMD@Wf@L#uck?IR_K3i1zQ{11TGc1Bxaix zsy#Uh@{{G5X%pqy=RIQV?HvnQvCJTXdk>&bmmw?Z7>7~cEXakWmm$09AU?nUny9QT zA=V$S;_AxD?9~g?z@Rpbv_3cuCcUEUe~aW;J5P?HWx0{H+To9*b1!f^rLS--V1Grz zjuiB9+l@h+uc3+jVX$8qO&+XJ!Hd7rz}!L#o;-4-5956BS4#mH_xodXQ5G+w$sGT2 z6s(u$Pmrid|G{_HT&l3Hovt)ivfMzPn3o7r7~=H@4$$-4;BSLM*T&=jk^ zOjv_evdq8D8tC@K4PWkMU?5e787{exM^rp%*@=^w#(so-seHnS1w+wD7D}fGK@gXO zJ@0e{Qg&)X!TLB_|KtuDN4!KLrOw)70ZlQ_BslFtLZAEESTMAz`a9vVXaayjA!2?U0Ga{+&rBe^4h_l zY3Gm9%Ay#4E(Ed_-{FLa=TE`>OaYmSra=9HJUr1 zrXZNiSf$D4T3?3aS^iLRMu?f2n`yQDQ8Q^0w}ajp5%@wR9K%0{<4LJWwDtBJrtnJ+ zK5Gbrf`EmrMOq7F6cq3>o*L7kQE?_-s|LQjv}eCC2KYskrJv?=-!;3v;48BiUZzcF z8W&c8sL~*5jhKqMPMu``ynSe;dW4sext6tkn{T!0qBCn_@QjRF&W4%Zd6@WeF%8=# z&WOI@_S7Hz$YH4&y#FEX&7-l3{=aWzPLf$DAwr@M*R?-KgOEz2W|g5xNisBvR3a(K z6eUq2LPEv0KSu~rDM=wi8c;ME(y05~zxCYDbFbf8_gc^MeE&atoonrLu6_3YeBSTZ z+h|%iO}cpv>;BroIZI`xHzO!i0%cdmIz8^Th5{&1cCFV;_sKMSansGcSz=q2)Gs_0JV>US9wOqQCf`zg&jbAJ!A` zp$LKAP$+DCk`7Mp%OE&n2HPMunYaYrL&dd4f+^SrX&)`gU*{M&y3-Usj{xKg4^qjp zacH_J4W)K5}+e^(466Q$m%lT=>3g zIyR`OLG;Oq%ne%+R_nDR#?3}@^^FtxD=S9b_FaIAc+T$w42KC{EL9-v}nO6!a2IOaD<9^n=r0A4BG1b@kahV z+EraiM(sQCl~`%l>#A&%4Fzuf?%^lBNgAB$V!c^C(o`4GraU7S~0}Y|BnoTjm$^**|Hn8`}{FB z_#OJoe&EPZpUI1=49vfAnyw1_P57#^rFI+QU>|c1?;9_G0L2+_f8Zrfxj0NCSDXat zW3sHowGdkIeh(~r`~wnGbqIZ~$ZxT;rk)4>v#d;di#3B=Ni)6l#}7&-yZQEBp^R0Pzt1@3nx}F$cpR zZJ;~1UW3}4z+yaj)3)^fBJB)cV^lW@<2 zpyYlTLp_husCOSx>X|e8-;5x^!MoA;sv(ZQ@RT=hNj@HLI*B`y1@u5t3`w@WLAuVE zQNO;^INnGerw$fjypRrmzs*cAJM9A+_q&1}wx&*>&h|6ojc9r+Pg&ObRV2SfDqX<^j>s7}{|4Au71 zdB-22WvL{yU0#Vf`KVi9y+9W0LdE!gF&!L{UsgAd9rxhEU&Gn(%M(~-UINo=i1*Td(_${>g00NyJ;z4=|O5HDsRg8fNa-Kz^LO95~v^3o@rg zqU%FW<-O0B)IIE`qkqNNefKU1g1NJWag{LhanTz*H<-c8&k`X{-cb;h7)xr@<50nP zHCb%DmAcGp#aQ7w*rwrz=A%wnwV&(p9d|$(YzME>6B%psD266Sagu4+ARnB!BwO+kSRw zLt05Wur?-eYxjQV9N2Ag65ebzfq-U1e4u_GhM$Ll#e7W&d^#VPo@jcjIUYyX6~n_c zd)WG!63fLmOVqBe!Sx!z7CNU2)+)b(h{=`o=;zU}aAVgGnWqbhfV@azeWJpBFu&ccU(YU$+q`1=-WO z#9dIb=p~9r88e<^Ug22Th0xLUtHdvrM=bLXfOeb+>vFUJ26jD1xvInXD3~L#{XGpG zT*}eksAh6h&zaK_)kA;^9|NDc7<#f{d zJKIX-hEBkJ({$>0KLS_P3Gtiei!edCei&_}h3=9f?Aq#f^j-OZ#4UY8wEbe~ANN_X zK;$L=VPGo#GG7c^1b;BR&6f3F=nJXq-VtkDPscT!g-0_su@3Xb}HHNv%o#A8F z27V?2}?gWQyUCWGwXN zh|*0~6C8`+Zz{*%_8b zTJSRrVj%N=J+=fD!49cSXfs6_$3A#Tn-<=ouNCdB;#9<#Adw2#G@*;k&!392oh@|f zmdPk1z6#WCy(!IE{uqqQ5^$f768z0w1y(i#T+)g)M5ycWBpyBxDA$<~h8~x; z8hMOlYsb4yOIQ}HGMm@ak^6d$yyMZZUY52MojV|XOxD$mRtTwx!-8H# zuoJb1yg+3}H}f{#R@Fg%*adTEM15BEKvC(V>PRriP+;$Ulwu8o4}h)EPl4gZEi|XM z7{weBq(_T!*{f2l-=+;hyaF&tC@lGb9Rybd>#I_tTDXx-;a=Usp4$cejB*qR0WF` zq~djn)7YlOAaj?~P7bDnM~@}=obAAkXPV(g%M|vw(mZD1UOVVzr-G{G3}#nm5q8== zz{Jud>bqez>{?C9rdz(H$K{mq-MdT(OVt%@zHtB>zU+l7o`315vKb&}TMa*sOsBy~ zK~Q31&Q2WA2CcWM_)`!;z2B^W%7(k}twkKSS?Lq(xh2db}$)#`oV=*#skU!)S zh|gZH1)IeRI52nu_n&x%$IsPczqtf%uRn_3TVH~0_8QDOKO5!to(DO{UEqAJ0bLeL zLip5dvUJ%>c=a=mdX+cpg zTnc}z`UrFKG(gv|1txuZ2)?J3LB>=OqJ}2&doIba-K&kDH^Lg{5FLCGGX*XwCZb0r zM+~vi;ClIrpjUpJbc=2SzdKP7wojAx;2gYPyMSGK{|U*ior-EMTFiuf8cbyGDq5It zPukvogYNxRkX>F*g!?nFbzdlIc?2Wb5QQe$+;a;a24kBTs2Qii^gP-Lyn-n>FqPss zE*WtC{G|e)VjWK7z8a*;@_>%B!cF&-V0M=TZVo9FDF095>>^DpjSj=g1G2Crc#ulq zTz1=iJ;uXh10Fx^L$_R+fC;9Ff+S^MqT%wFyqKuYO80kTyaHE8my1W)b2CwtYi4@S z{wL^pZ3odseC&L#gNjohq1=Zb@ciE;tcZRJ57uqR4|lWi;OrCd=Ta+1+&YWPpUq}- z#{VjHl5t~7t}lY!{@pZvW+4Ro3_$Y06x6sgj~yrfhjPS0Xh-o~LvZV9~NDKLJkEoi}kTufL0M~iPuAX_BQk{i1?pWu5gS#Ja5 zAI|BYUGmU0_6U}Xhr+YDMz}-M1m4s(5Y^H;GEZ5J(RB8NjG1nvuw@nY-CxgrS90)o z)^zYPn2HM|Z}87zHMtn8Z;T3J5?B%J26kuHp!wh{&}*JWmPiXz+Y<}$%GqF;Zt(>quW-8H zEz{nRCBcG)VoW_FfKb2AeEGnP*nwx&3Z@+ zXK^GlB^DPo)j~+f0M_Q85*$c521PD5^n&GV)VSpjT5%p2%ITzhChBnhMFUK}n}81N zd*~NcV+OcXBL%lY7&N`cU!5(%aM&Qs9y|j+6?b{Rzn5T@Pz9HQokM1*+!idVQDW_9 zF2nfCLIQ0w4NmxBKrRm)hYU3%zLp#hqTa3$Z2pUAzAKFnay8_Gwx{4>2uJkw<@($c zRzu#xP+T6+iRXA}80{Gk7qqW(YnUIXYL0;afH=7k$$z0*Xat~I(Gq+>imH&F$uxwpFhZgw?)wC!mUMvKd2Iy@>+gSgMQc=O@HqG zj(O3xxcvS^Caj9#*!IJCWZoWnQE4Oaq*Sr==MV6msmZGPdy@r|SCWX$3e44ix>zPP z2}jd&dF--Ieq?$w*>Sm_S)wd_( zXu~Rj%;8jQQs(?vPiA0*gbHKlJcUtCNrv9mY&sc}(M$C(uC8@sG;6G&abGHw9@M}= z)mPAnMFPjYM$DGSp>XKdU5euu;(?%p*tX1zsc373KKT<^*QE_Qvj(BGupXUP3bP)@ zrp%5F>Ck4Mi}NlA;_`F-kUKt*ny>4F>6HqE*;@%dZXZeL5zc{s*N7FC=@#UN|E4={ zdcv|{0k5+~3Qdk5gZ^wkzN4)SC`~~$Dl&%x<1G;K!Vtzik0slZmO_sZcb=4U;?2-o zO~ad}GdteTgT<*2arI~kynAa6MshbGwc-b@GMNshX9gg|>Ktn56hU(DEatwe4tc>l z1#^FSl5)v+7}<9M#D7V$36r{L(WFDDwN@4~)NNRmR9LAr<%@>bANRu1*dJdxYhV`&{ycXkW@CSQTVo=v- z1urO(V)kWgR-vyLi|_3Rg*ie@YzmjEbMFR~cgR9s*e zdKK3jkLko!{lEEi%I8v(0bS0cD#z)XzM`357M^N#qM%Klm17LqkC-m~grQG2ARj!Oug9WKP-`*fHe*`pcw(Z`&C7EIbJ#3z}hu za-m?MS`z<}j2EuDW`-?qIX#iN1$h0^BVPIrVC-v&?TfbK^6?)@j^0Xk!*ppWFCX8) zfGu8I{pBZ1Yf6gCt4YhqOS`!SxNqO$?{#pSw3NJr|F%6_ytnymFp-w=-RbV+ z^bAR1c|&OtNs<3HNJLUpT2@}>zpneg#&K6kZunngXZ{CcXZ;6b&HjV2=KtTvy3LRl z_ulR472q$gE3M7V)ujJ6Pyf#YlpZ7bUc${|`zr2&|GF|wSd2f;efO>zyS&`}cKh%42#}Za z^bMG$<+jarmzMuFS3mddhF9K8aL?|)&>sJXyDtiTFHx_ajjtasf%*48qLB%g@WG`$ zJ%3$6l$dVxQVzu50!RE{AwzUajYxm$34u#YI60TzN`z{R>4o7y2)1n{rvGkG%eZ0a z)ntgh&0!e3-yHw0)n*d58Rlb4PBe|v8fr;Nte$@ew@cJ+D6TWBVG5f z^7awDd{z{uH@a}7)_gE&yvvXGX1HYMZfuCR!tvX7lj|w&*!Q#^zxthP}YpbUN;#wRe3)9*{vSjWDlX~tb=rE&jVl&o8YSL z46AZ^m^1`Gqz?5bp)S7*orY7eLir9Ugw0rGVISu|9;g{S_F@~bC*qPfD+a9b>mx_RHD@p2tl1ka$mZxLFGhLGD06Vdy# zF!3AuhHuV`GmjN2Xw#%eV5oQszdJ_r)wwY8Va+2P#g#k9*Z;yPp@(@|*8|8oNfjn6 zBb)kiRIk^|a$)7QG{|3m5SJIMf=_ZuFi_S_e4X9U@QpH?>$e>W4$q-fs}m|NiLx54 z-uPyjEVE{gD0=*zMEoPp(c!2zm_17cS1UZmKSRH9>57T8szQn78gy)DSuXhIPNCX4 z%EY&{3eLReeA4%~#$4i-~o z8F@1y+)w!IZ1Y&W^>rP&c&d(Atmq{T(}Zx%L=!ge7hva?XfnUFmwbCK&lU=&LYpz? z)2Y>=KhFS$y}eK33ls2p>~X4Tpb7&`kr3a>d5<~DV9UR3aAv1Mm!B;YDW3w%^lGtY zj}%Vv5ykuKYe}VJ0-WShUsakOqvCK4sf;wm_^pW4S!=Sz}i)s-uhPvuQy$QU0KKYzxRBa3XH zHKzG(B{|L;sp?@%EZ-mtW!__gRbq_p&&jMO{R498HDDh- zf(p{QywUW@jQg(FFiGy~W&nFOje@LKldN_#RyG#FAU*XpcieQL? zCJuI&{Tf)2x+X`W;;{{r~ z(m?z~5)AXCS)Z{kbW&C+{QRuMPO5Pw-&*C-Va``t*CC0Q4f!x%I1()b{ZKo8D%(34 z4?Aac@)h)-KxJPxwB{$%REO6fZEZ`xs(F(4^ChvZXagB}A0^Q6{f8FwJ0Ma?7MHJg z=MS)l$(8wj(8PI*>le!lw6%`%zl_yp->B$dbihGIwOG0|*!3Q1NMAtB3jdKAUmrkn zOgWSe7sBQwO5a4vk(|P*@MK#h{Nk80TF-7^nb9P{$cAr_%v6B=rU=ST1Mciq5>8aUEkN}%6x8K@ zP<5v>;H7Fr#s&)E#;|5EbgzJ06E@=grbm#O)CWzilVI1iOj>in9(S$~VbWg9Go@Bq ztYO4VW;~n2)&2fLvQ7$`78Ssnu`*aV*B1u|!f??i2Di@2;SyLLP?d%_s@MGk^gPXA z{NFUfUM!-zW2_mTu{t|^`VtoWxQNXsWSOBWcgU{%Of0=^ip_8aB|fahjaAnnqehtt z6_YHzJ~|aje;R}Bpc%|PcZnQq9|Qjk#n`bY8bHNChACY=f=~aR#}8-4P^|tWT`GHz zDBkZv2S>i(OXtT@<=6s}XpsS43SLxS-xeEH^%|DJNI43X9 zF1uTbzpuGrzTP29&#AM4OVrWVonz!^y&`vaih+|{Eq(FzI9y+r2p(w_XzL+P55%5^ zTR|L6eRPO8yUm~_BE3}Hcs*9eEM)!&n*fzs$N#D-!Svaj2kR>@dC`s=@f3U(*mHH) z@|)bg{=PIja#RoHro9b&d`#$_5YsAgOabe$G{So4&oa_8e?6H{>ZST*LO$`DTJ z9mM|Y8yR7)7W_PXCSecel9r8Gyh;mm2!9d4#9l?1RlkW%6oiw}sBhf6zMMa4iXjej zda<7!OK`!|JdCXpMsLZL=^U%lY?4*D5l) z?;nHTTp=Q(a~QKGrDAm16&kbHmY6O~fyUG?@Q{6s3(9Wc*zoaSw&MgythIy~-%sEa ze~Kpe4w6fG(oAslW?268IPFm5XoV}&@nP;wP~=iq?6-S@Sg!?i_*rrq?`F{0{DoG{ zYQkZS-Eeoi2>j*}0E+t*BsR3*jMwdH=)UeIOH9?#&?)=7T46I zp-NLY?O8L1Ig}+#f?q|`4>J;}Sj{d>K0Oa)Dld}dS(gQ-Z#ciR;}ocVltDL5-VGsh z%TVoR6IrTY#SSE`hWn8p=dV0}CL5iu zD&Wv)3Hdx=fQn;FfS19?Iaki$ks%|-XSNQGT-zvck>_gVlhYyffEe>;eUrdu!e#O> zcNR1Wt23Ked0fo(IWprXH0-kgHuy*%pnD)4q#*VK8buS|r*%*&126Mn#;scRt$pl{K3bdIqLSNaA0sWB< zNS!nn!#?M39ik#{c0)6H+naHVi27ihQ% z?*H*6JA>}x_(d=2&r`c0HR?SLk}{=}O6OzBY{F@ozw*CPbuf!c1ie{TNr3M!%;Zws zT=TwDb6qWB25)g$#8Zt+{&iS^736>+4FUrSKXq_*IYT zXH`J)@m*-M@&|`qI&8dg9jEzyKr8P3C5g4yOM`_uZ?i@eWZ56YJ!8@#&iyEA`d$M| zZw2;;p*U-ECKxT*M6%3vr{KHh5hz+WlX;%l06p2+^ta1=+W5EuNA{IKeZ@&QH|UB} zPqpwJ#eg_$RmO4l(;>q2vS7N{erm$`k;YLiL7<@}lQwBN3tQfj^q|G8{MjkYjI&qx z#d#_)-E)9WZaEHjE5tN=yVUZBXolQ2H?7p?wtm(JNMTRLAO9v-^*BPQZr0@1qtlh#GA#vC6rfy!&*uyM?B(0zEFT;c7&+ajW@-xw|Su9zIP9&bl_yH%O5k^)T5K7~Rp zC+L{D@gUV1&hI$23Z%YHXGN`3=%Oj_pk%O!Eh|K0P+?iFLnkrCWm9r77-L47X!4;3i?_Pp~0_w{s&PV zm}2po&c4j`V(fkJqwGieJo-NDJQIcY4`!2|_wR7^xGKIL$^C z^wvo+DSMT0m4Xrb<5&)jcDn{&9FUl<;*zq48BT*a%6D*m%k7bOqGE44wmk`jcNLtz zUeXn{tMh1FxHefouL@<`{4g%zDP2}<3+JDy;80{Ew22Sm*47%qP{A@7pZ(F>J~{H6XP2(sl89mIK9FT=^XjloBK~p z-vb75y=XQwOYkJ_6jbNg!ID2u@m*XiI<6|jl3xd5cUUp%xc^4G@r|(Mqz0rOcfbo( zPl#_$8qL+4z!;95&iBdALa)mi#EmXQD~nt#O8JMaC(l6Pr}dz-=Pd9oqDbVuU68w_ z9o8$0Gsc|uY}G4{VQD`IZ-zgS2jiE4Prd@+&2~ZZ;8HHQ=kEN>)wG z!-wmS!@k%>c&ud-sFz;BYhmILX}1P?J$Yo;vTB^${u-@coW>RlNyf562)~y0(X`4` z+LifU;NhH)WU+Ncrsa`Kul+2+GJKZ2|?p3bJP3<2GNb1-7`3fam1 zu(|&&|FlLS{P1?;S*r92-gbH68D5te$`kuATgvboXox6)rURxAhZ$eXo&>n!zbxUJ%*Sk3A4vjKVgQ(BzA3FB0c?L8ZyJj z!N5?2x$N^2WtRLVFE^)Yp)*_qBe+`n(Y4dq4?7ir_vap5$Az%uVjyaoZAE7n4a{;A zgP9wIxV>-|-y&iG8(+cgAzY>Lc6tJoC>b+~+;eS>ZH7{7VDH4|5z7Eku;_eB7DQwq zHNOfzSs7&RLN4XwZ7SAssh^jEPGNJoA`D-h#8}@RL!Zn(i7yul6U*7Ec)Elziy!ZW zzDZ$xm%d~Ch>Bxq+w&T%I1XELW)}E;ngV9~jA`&0PwF^FgT)R8OY_%&`F%d54uwIy zhoT^Vp%!zs_XF-us1o#Q#?b&dA?6jAKi!oq!34?9Vh_nb#)~llcu4LCS$1d)V=AJB z4~IBzskA$WET6&pD&9n?;XRPP&IJv|h%!qwoxp+9NX!sQCi4o5t!_Csk{z1cz&XYg zzaE}|@BS^u>eaik&0r#X@mV}nxcx$Li?{p-Pk#&4|IJ_;4m?KhU60YX`!}%a8jR?z z#Uz>y#-mfm5~G@DoKNK_Uz+RLEEuc-asQXNV{(L`OssB0Hq z6_}F4z5JgDd>f#?$`Ht`|2=H$>^>;&S; z^{NX`Vr!8moU$C`+c|2}jYs*gEXV-wOpby!o+>i;yve-hS)lsflSZ$*2ia>XvBBsL zjbC|+v>Z*trdRm-Nx8Lsx()Q9}Ev>q|@a#=>avb%F3zA!M^NsA0M(jz2L8LIo#r zM715)q=%BQLwe|?aRcf@Zt`wz|A-3aBSgKlgsV^7$Bi)+7*!y})<%f(WcE&ilZ6Ly zS^9W>;hB6`Ue29W!`pF`)nW8+NwD{=Z$RSP8|dSeOs_B#S+T5O4BKN39+MYB=(m2d z!@vd{XDtHTb^nO7??RZ4+p+yY5b>CQ8^V@y+Rpq&zELx$_maNNj|{T_*X0oy`1r6u z{mm!toz}*L#btP*y_Tje&w%)4rubpiIQI5K2{f1)gr{w8(PiEf7>9R9(NvSgiTNj> z@k=`{Og(OytG0{O?PB1NmpA%EIWyuMBO^~b8-LqO!w2D2V00jk|7I@dHBxINd)>xD zgkLhxUd4!A7MFqY)s=#RS+8i4ay+@~&T*4AdZEqaG3+YOWss~Y%kOhf=IY&{qlHQ`PMeiLRCyi9-`R#s z=vU-@Y(>3a5_o1aFz@!HgSY-VrY3cspzHE`nBYBz^|VxFt3JIJ=sTQ%%^Kxsdas%Y z#l6F+(ov|n*8;{1|HD-8V`P)tc`E&PEuP^S;+;KFv}chTZR!|-`poMXZ1L>zwZ3s!|)z>JK7|AZ&Rl>471 zKa7MornQi~lw+M_m%zi-i7+-+gNi6c!0Q`#spIBYc;J?0qjkv_blkw%zkd(8SZrbFIHRs;oBJBxq%Hb3^ z{L^Gq?rbDG9W)tJJq-xIxRafALDC;f-C1vwjLp>DGp ztn<&t>g;aJuRc$@FQ&sP$uzvwuE_fS9?#qoj)3d{9dzrN$BN#p1LXuE^q$34xMGT3VzX7JKZzSR;lL7qx!8RLvkZLW#%r^yiWU>;>{CJ6q z%Os$g-Ay9pbQC&Q8Zd=MoM)VO49(^umHjTyB=HMryu1ar@w%`|y8tzGO~GtW1y$Mb zmt0kpg5kq&FyzrMLi7&8r|q&_x|1DDHQkGv9{G?|Da`(xslk|AWMNo)8(DkEfaLX5 z;i{iiaN(^CcD#xNnfiKU!W;RGl;eer=(1nyuks3#9C7Kp4R9}HGS??lXY4K2ncarY znBu>cj~Sxi`wih@+i`N^hY;iCJC(g|pv4Bus(|g9Tl`t0VyvI224N15!PmQ+`_9INzaT-_(K`C$nXgG8JaF2nfea2$|QTV}XagWWIHK=-9a!;Y7mV2t5U^qiwc)~>t4 z-?-C~`4e`L{BGlc=;c+AESrW@IEgmU-EjV76t2;|iWZ#TZN8``WRKBdM%y35`YkT- ziK{R9H1kQn>Ja|++|1;?zf0Q8R58@}G)@?kf*z_H=@C5_o0L6hfnApOrtUbB>- zMQ%F8tU!De7LKH461oZ9yR+<>8=8#0Dmc`&0F5S$g~+)oY^U-SX#eqwE>mj6$Sg0A ze{RUE5;0@_6_DylI+KK%8)2t;KHvFlJ6ZZy1|@>6QEXi=ty^k^vQ?%au67r*+nx$4 z>bLU~61oH#50$a!@ihMZcShLex1Ra%yp@LDt$-{+K6x7 zdOwWMPO&{jLkp*1Pw`BQTyhhI3wDxazMP-?fEi{l-41#YA?T9cPh0NZf{|zgIMm$; zjmPG*mb(4aHO_)PHk8iQ?^Pgfycs)bPd-#1wWuythujxCMk2Miwp zeV;`%V&5TZ=5B|pS|4Kd+XnRUuSJXMPjt(g@r+iM7v5_0!W}X(0`tNWP77hj!a!`EX3BVQYuWrNUv8ZU$7VkzrepqlOqy_sl=X^(zk&(; z&A*IAfv@4lc7nayZScJ-9Oo(@qUmNUKxQ}y-rUt7W23&{j$Ou3puus@i6YO}<}D`h zVqjarJ9=x?AM~HrPc&z~7R;)S$FAqsVJP7TL{c}HEXLJ&y2Nqb`A3+=E@d9|45NAQ z1R@x7itev9g9Y#|2lL-A6RFn-C35Kg*?vnK1adc(vLA2N?%Sav6 zV~+eTh2pqv_^>pFXl)zAc>C-VcqmOIIc94~t*;8IyElSCf2zP`w>s~}E(yFgDH?a= zJ)lOCZ^7SkI%vzPGHc@=5|=rg2fWFg;R(mm${IEH!m4JB4v~c~mM#$H=}brOO=Aw| z#)0q7rSN8V6!qaa8+zMkuwsLHwExTy_uZWj5|=x{g4Y83dL+?^tD)9-gu&X^w>U2N z1SY~|GP`!&Y<0Eij&LVg;HpY?N+d(gP!%D< zmuN%cH)5r`9UZgJ;ZU{|Yvogg0ax^)n&bNX`g{Z~%ip4(`M(`-N6#cfb@ zbsU_FDuQiqkKounGR!-EHlqb(b&vO^amnqwDWpOtR{gehv16VM+BhG#->BoxO zdhnxkA(^}<3GFAAqDV+QBtbeUGkHoB*<&=cTA#ko3kTx2ouzMz$&f-3fkZA1PG1}g zX&I`%xQcDvT2OU<9j-f+LwnCuljE-1Oy)1{JRzS*oyxvK;K+AsD!Wp!MpG4TKM>{1 zEbbuj^QW?78UnE4elK{wItLeWZ%`LkX|~`{DL>vQo@R^OrtAV$Y|~hd$CSSkgXA07 za7LcB^zq{PyF3(}@(4%8>K2gWOEI06R-`w6pFn#9lxS!Z;=VH&j|Z)T$bzq=aj6j- z@<)%3ObR0DGtbe|2t}Zca|A`}-SKln9C1im$f!MtfuRk#s9wc~lb7a$yW=no8l5Zf z2tP{NzX!rP&y`Ge;%zJu?!$-S_1LAA3GieX?gSU({DW3ZYEU&j(k{*@uQG(MPgldz zmSPl{=?{%QJ?OFj2&6itKvBsie!RU7W453S-PH9s&Z{+}7Pb_BJ=S4d%~|}qj^mwt z-@xTNd?qo+K7rJga)_1rNCyV|Xp34dtm!ceQi{0HP+J%zC; zAt-Zu7)7MD_s3krST+XZ~t?G-RYNUtcn~1PZQT}xBz-wBs z5rzB1H&V6E>7c5whd0g#GnPBMCpq@B0^0lv*zE$v z17lcCvEwjx!Eb2PorBL8?n8z3bC^`AZ>6go9ckb8@7O<9niUz^OC$TQ;O>jTxMNi* zv0D=ldq0iiztt_Ky|C_AsE8>#VB2aCb9C@S-CT> zFh^A$E1(i)Tz!WZp1&dUIQ_j|s17^!bvX0{J;zPOG4Q8+87v4`#Ab9%Va9vwVAux1IbAU~n*D6xwDUM4LQy+LrbPUjGh-70;B>*B?QJNvGn2=dnaE3;SbZ z>3-Si%(#+MI9fYQKcuvRvHV3WIV;TgYe&N$Lqu@{eRv!uAdUH1u=`atxz)1;I3>J5 zGg}+h@=IX4#yDV$?h;*R7kF(Y$t0DTF_S8N$V0ca@KsqJ^Y7l|SUuWUZJUoSffUWQ zI+9nbbns&B5iUa|6MlLdf^m5zx|naoxl5I3dS0@ig!@+7aOcqLrxxIw?lbgi?MkS) zXi5(B+=pErHX!BI0`hC>xxSw;ogXI5*2F&}JGnZ;pJQ=2MK+HJE#sJSt_hIN?Q=f- zaOC`>3M6&05033}qboYbuuMfUXh&W`y_8<)P#yzf@s@EPp*gmgk=+1&Ix+;s{=7}`Zqa*FqaZD63_#X_Nha*;P7{(C^8HFMvL{>(~ zc%S=7Nkzhg+|em@j6?6G>O&keQj{YR|k>Y?9*0#I3^gM+%BxYeeFaxGpcZYrQ+ zD=nCVA1C7TlXIA$Q|;ul4htE&XVLt`3H~~TL%6y{l5MQo4|?{Tf&S2Qn!8~>YUC}3 zI zQ7^<)caHH?e4c!Z*1(u0Dj2=%CQket%bo4Sn24*h;WPg?*2pcuT1f$r(2>C%=IYF| zw2ct3^$>0SQ9-3sjw3NQWqvkp2Nn(W-yv3%#1ruVr;bc^4>g%!gKx?AibN*-rVa%CnZ6q*6V`R z^^@_qsY;cE$8_V?odtCF0Vx#Ov;)2+OdyBzs=)WmQ_Kp#gBNZeC3YfrKzD*3uDoRn zQ>vAiKMNDtYI}Eb_snVhaZ(CYe&pc$MS8$IW8j-E!Rc>Qn2H6d9OJB=_J%)){zx@u zbn9a(;+}=Q2j(+PH9OHM?FY5~x)&X@j>8Bu0%6l;a-U}&uSirM4u6^tYTs*dZH@(7 zv|%RO{c8d%X2HXWt5?(ACkAj{`Cn3DzZ`5jYH^M06n5ejNtk2ohZh!TL+2iZD5X2d z__$(Ha4j+BJ25{86G&joBZvsQ3Li#TXwGfGzg-dR)ea?izW*t{9^>+7kc#b3tr^{L zmNp5yvzD$rS~UA2|L5NjE z7`e8r33I$PVJ3;k4p}QQd{GWnwj}drY(D`de%+95?@ph8_uwm>tUx8hL}*CUguh7S9C&%( zikuJrO?$5-!nKC|wC5zpD_h}#^|mo&{mkD)%)5suFF%e?TP4}_HK)kZ3Gw`+^CvO6 zNg~kVK8oB6j0!k^;CH-J#O)0%U+0SuyJ}A~5iW0p__f+_B1(f@{b)M#GE#tZj9BBu zUPBa<_I^_;Rr<8%nvsRqFvKaFBPGm|RG^60x)2LHZ z2&3G2(|e6R**GBs#fwVGTW)i40;b@b47`S>A`+s07tbyHMK-plQM>Z< zJfjQT4#RnrrxySy@{I@bDL=t4CJbMMWnf8GIKTEoCoKLQhVM)T@%2k<>^>d|eoQtQ zkR1R!ZtmaV_Jp3{oD~j5f?)rzgDn0}g;DPH=DB@~=h>L%z1hom{v zwhJCOB*G7kQKT1?&(WoMUTnd(5PFjHaitWS;%V1lRPV52E24k%1|KQ$?*0Z``&R~| zFQ32wg-P&z*$mvu`4>gg&*N0Jg_ycXl(CZJ^T!=F@b7B;MEh!eD(BfwUhfwmwl{Z! zaF8K;XCxOaUVo(-ku6YSppCn?1w*LfH~x|>VdSUDQkvUUjw?qFW4KE`5ej-lix(_p zMa3ra%r-pY=iA7le^M)+5%xm2b1yM^UOKQL8W?p4U&o+BORe9-Pl?t*YlgbP9u@*i5{(Y#VW# zqsBf^H3I+M!+aG5OKg?t;k(}9TyFoeNY469xGfL{eZykhccsm0yQt#S)iQ94ea9~xcRP+z@02?FHF+bnbnXMe6&djRjwaX- zHPc(+4!FCH!ggRakFk`gaAD2JP4t@I-jGD!v{+`6FJ{RLG0QsIg0 z+Xt3f`KTPI18EJn_?KeG`6}j%@aKQap|NK#*6dctQm-a(f2zaodNv29zHi4EqwCZz z&mW7D`U&mhcD0?0=rr|o^3`S<4rQc)q-`-hFlPj8^I2FhdJ-1=7li2c=a^7Z&3nCcxs!(EG`vtgj=3_F+oa3I;r%*L6%vP-Df%GlT zI6U%@CnMWvWo&E<2E*1E@%@-pn$%wC`L`1fEBmsgXJ&8=;VI1HY1Oc(wSWewn-iF>4gS(NBVe`K# zkc~*kv9$;3d95o{4!4sP$6U#)uqUwD_#5ZD=>uolYN=7ZnCJW^72chGggzIp!R&R{ zQQaYn&X!Oo+AaB{!zb+PN78F6qMh zsAf#v*-8^-0x2`JfLW{Df#J_3*)hePwBth+=4kWzr}ut^zVXFWVci$77urrOB2!S@ zq!`=ob)mHS6zmQ;0lPmQ!QiYo&<=Wy=oXC;)8=EnktPgP-opf&pZr?_c8FvQGh$jX zytxrK#3q5+N*8h|cMP%TJiv!Qh+f=`tPf$&d3d00L_fKAF$V0#XF<u`!R7UR^47>5BJMvU$(wbtm`2AX<1!5+hH*s!aHI~Qa?fhYmxb_@8t zR0Eujn4q+fG#hg2H-Cr0EItQf&m$ z7V5yg1}V5Pk7I>BU4@&1PGM{D8aUb{&Fnj6EQ$7(ptWy6MUG{S@FHBjxAj9*obt#(BW(F<_}VZg``@D|Qy=Ka0#DZ>Pq=l@wjFY^wxJ z`4@)|bXVi(^;VKI%-PVhfAby2y_qntcl4EU753@bV>DaW_=i?RorV31@vzj$4Zs=eb@pU*EG8KzqDt;WtSS` zS>Z~5UuMC?%@Jf|48d4-J+yJ>9xZL;TzlV%t)f0YvYv+g+3_gGaRXB(UB^qiG9V*2 zl6G$>q_@_G(!koe^x3&pSo(ZE3{;)P&)2yfm~cPLxE+oaKc|xui~+|W*CG|WxLjg@ z4!c(MA#t7>3*F8RD5TJb`@FA{OK*y)YeO?`ynYEddl5RUJxv5ZMPppyBK$LRoYpv7 z!k*SlT9-=kLRblXHzJKNI}#->Y=*Yl2dH)`lD}<3Hy$`r14&Pp(CGb$NfGhVGQMtMZ{L#f_)r%w;f9=owzTd4-v3Loy!Y2ds`EA%E zFU8Nea|%XGPJq}dZPs3(5mY@`usmUaidTP;!S;ACFP?<%tu3k z_Yt@|dYj|~R^!0~dtqMPCd`$(0h`wuKn+tx$efetd3g-)^Cs{V*G*)o)gvr%KSSrm zor2;ZRZ_8a5*BqCf|~Jsyp=jYYYWBTy-zT2*5ebHCu@xvhHBu~R0>(<66jx|K}sYP zs73u#*sfCoWnNQR(Y=d7bgKqaR62*jUo}t!M`(dcH8Fe}K!&`W>2+@Q(f#!Xy2!iY ze~x*mCj+=G@i%;Y>;iT2V(jIfTkvVKIIDGb5_|OL1m<8-EwNlUhxK`Ojzqkcr4p{d z-cRP{7T2w4K&KQs4E^I*Tv5eAEoG+aqZUXD%%k7r>?(c1V z7KSuVgMB?CF!<%HRa#*-+TAP%p-qp$*m5_q*eB0)f0Sdjbo9Y(xibiVP+}`8?TJ~l zBDh6+fZ~_QAjQqe{omB16qyO73;T&(^9Jsjy>B7J7uaZISC9MjRpg4oB$<7SOuI4Ft4Z6J`E4ql+~r!4s^9&c?>IQxyhb*e^1 zi5^H2FD5rOzUTG**$UmziW)Xs@zd_f@J%QT>;&!6y1kaB3_0R^qe%D^bBEgP>gI=O z)m!AxqTAKMhWgQGB3*(_&dE9=p zn?~kMV;^%qhrru>^k2|MdW_xirvC=ijZUQNei44xgf=J}Jr8kv%;`C``-D~I6Sr-u zyv_}j6t@*Xz~WBg`dE{vxuh4eCjN$K2|I9EHJ52Kk+!0Sv#_N@A|mfnh*4x(P**&^My& zX$#Ipe2l=0fmn?DRSL~x;WT7_I1Z3qyw_!~uu6F;?B=%PxmMdjKR$u@HmoMmZ%%R! zF-JUa`k2=*7>YSdfHh^k@Z8lN5SqJ_uRfXu-)aOf?eKl@(QAZ|b1xf0cZg%?Ho|~? zG_qM&;fK|o= z&@6iy#5wnAV8jquMO*WuuS9~L(IFb=QcgOLG@%qXx1)2DaNC+Z^z?~DUC&!EA;ceM zFZIBL57BUaq={s6{ovxC(^<`k(=^&%hM66E63$q(L72dA`f7hE7zI+uYOBQyHoxgw zqgeD#)!`WZ0&Gw?w;NB+hp);LSY73BIK~sF!MiHxXmUMw=AS_~7v6&-^-s`)slhd8 zWSEbC^PuUO8Q5Ih1&g^EVul?zx45Rr8y1pgWD52*6bnH1?>P8m zwt&i-IwI%+MRSlkrUsDUW*hfvFrvt5}?Ie4BWMf=)OZn4=u$e9j?Fr`5%t%$V1Z| zV+~E=kFYR%61yu>pENuuf&kSf+^569ni^4_h_w*)&%FR|c}`%fdksEMm1GhE{_&fy zyg~IJ+vvxzy_nN~5<047m}fU-nbuqK)a$tmzkholSi?tjm8JN6?|ZAn^3xd0OT@MO z`#3XVQoW|P1HNUSqJBdg>TTxsJIYW{inC;5wUk!n5$wRnS+yba`0{Tg45|Bh2n# zY`Y=M%9(uwKOHar;=`hB>zz_85)!00H!sBDD0^bZ&gDEHIS}>7ka_*zM>??453+U) zQt~ed%Kiy6o71oG)An1!x>knu_GR&Qj40E?-3=M}Ox$>9I@@-UI}@D~WiQD`gQ4*a z8dzw?WR$-o=W8ONx^WOa_(hPNSxeG)l|pxnA}hX~52?D}&~dvLJjzUgz^UKikGwr@ zP&q~Z?g@uxFGKL~n-2EonphmELgU&bQRu)C?rahQlC$p6(o;PYqpqU-p$LBJLv>jD zZWv62PI6ftCGsvT7Nc;0?r0Cgl*w)I;`DS@e!(-`BY&Nrm(mOM{Y@xV7>vWeW1uR$ z9EZ(}U@TmQ>6xZZe;zSqpV{b=%HnvA2_*Wjn)8G88* z=L3-6!U!!s3F{-yA!}AdMU4KE=4G+anQ{^$?s!7Xiwe|jR07#D&Ib~F47`OIHq6zW ziOH3OEAA=u%)J@RIejs9s^wHPEItG7H{PJrjXC7L{cL7eQXL5R?j+jY@u*o#O!%@Qejw;r*OY(B2jY6AE@<<{e)wm}$VQ*1id6 z&Z;sA>)rS-(=TAqz<&*ABCFB%-%0E&f6lM8%K~A2NgQ?dMHj!vlzPU)-KExO|0u`m z_u6c%opKAkvs*zdY8)PzRq^^Zy71nI+=p`WblmmrQbSt84qP9Y048g4$zDkjjGUW* z^Y2EWe)4pjW^#&t&eLJ9SDoOVJ>&Gsw>;Rh?lIN+(sG;ilJ2hUc}IBq8NE?18K?!#%g z5MskTW9>mXYm|KZAdH(U2k^z!9C+NvIoa2>;OSkXaNFG&@+5_s=9WaTsJx6yGAlWz zYy|$CRE42aPNRL0F}(9SM-ECGq3HhoytzRNs0-;lS~Qcmohd?d&m;-@+e1-MG;_g z>khwcpazE;_%QaT1M-V+(w-@OM5>@Se2c_MD61dB7QXx!WU6+UEM%jXA5!c`p-D7Y)Ut7 ze@oOFt;r`TG1iy+Y~BVK(a_?HL_wp2ytMs+D}-v{rd2H3)^jci=^Ol(zl%_vb3H$t zDM)@kcts_aPhqz2{Dh6Tj(9oFg<^4_ilLgBVK6)05hsuU@c6n3{;nt`sd52CZNWo&`j0SsN#{1|6vdL@ZRV_| z<3;-UPdq5`-eC&};1gl4Lp1X&-gtKc41T4NgB6+BA(2c;rVYWTGvTh~Yq(qQ03ssk zoO4KsT(U?8TW+uXc6S%aR^LE+ES7`e5=Fi($JA01xd5e4KjNQnFF;NFA^E453f3PR zF@NAXR9xH)p7TxNgna>wcs~M-4$cYx`VdOJS_}C{n=zo}AHJS=o}@J_Bt26W(!uXz z80QoU?{_YOuR5(%Ky(%7Bl}MM)@w2IdKa@|>kZlS?#Hn1ayVbo{|-<5P%yt~$x5Q` z+0#(>H4sX~Bw20lUSggv2mxcwD7h;UhnA{h#+IWfdEAya@ySJ=UHVFn_gBcxMQTyt z-UL|sqL@S$KBC7RvvK_WebRF03Dk#|Q0b9H;F9x=zB&*G^R|cc@`F#{(JEnRsOE!V z-b|~XGQ+gMU?r}0K8Frj-bAN20fr~YV$zW$lt}uC(uI%UoXR*3Xa%CsOBtTi-nmSu zWhhii+{e|W(;4YPf+-ay%#qWRV7Fo#mdxmc{5?wWexf!ud&W{-p?johT^MayriwcU zKH}6K9yg0_hH1o@3hlp#Yuhj3&Tn1dHl)hUV}%&2egRfx@CzMz&-v&E>#<<64aUv; z4+hgl$*#gMGUap>?z^%Zb6%)}tBwdv`1&3N!kbCUr1xkfBm;i8Tj=P{aH{Q8Lib5# zk+!QbkZ=D7#!PPD-_eI4{HYCWU(8@CU(Mh+Z~l1nkvJT!IR>K_>cI5B624iWX?-#Y-`J{$n$o+Bjj`()6?zjcx~H02%f!x+PY7Igbh73$Fd#u=PU>F&r-xJ^*yb-zZ_OB*1*2mI!wss=aBoj zpZ9x_0thb?#VDO`#IU#@RO=cECl3U%{a*N$@5S7U^8nxGqp)#XDS7|JipiX+$n5?% zf%#eQNk{kHMc)(q@$VWrsF7*Hovg$zzi{X_Q^ zEMc{G1kmcNVR9ht2PpNMGt1&qP}SFtI1R|*=_3;C=iCZ%rFt4@Z(oa_+826dlr`n%+WR5>-P z(?**vSK_=X6@L8qjh5<*SjSF|@wmwkWXw3n&mW4#BS@|fjbc##Pkd|B3M-ea0*BLb zOx~&QaPPPz)9&mKB34awFj-%{rM;^-UFF8`*=&jPoal!EA-jsp#r;`TucyT zJf3aCbGMCQr~E@G4Y!4tKQ3Wih6VPF-of=zmGEHrF}lv1fU=XiptgS*oINDS%1R%A z19CC2Qso-)QQ??A89nI!wGM7<-NU^6a2BswEM(5iScX)@T)PxLx{f`3QoWa7R)MUmM3jnsza`{KG_Stl7kPuZ_#NFpwq)%(C41d z#ZyA?@`pZNSl&k(zUvWGp7$UV#y{eg++lJ?PZ`ejo+6eR5s5CA? z6-|raiq&i=juK>VYAz?D7tUh5DZ}{vn!^$o7C!HI#!Krw4JpSqQ|oYJY`^ywL-uUP z?9RuW-%lMq7uiE&#~0XqP?t4basV_w^}<)<|Ipa*D?Wc-2P50wz)B+wXA2TvK{d7=f?GxtI!+y2EEcNpnu>T#<|+Vv(|YaD0PIG3@oI9!rXb^xdPie zQ?W2AAKXEMT^D0gTS(NKl@PIKG57oT z(OQ9b(7iknPK$g5p?!vQZgC!LzJoE?4Op4KBIgv7_$L^D~Y5n(430&SaVDrKTE|$$I*4~WM~P@NBfvK zIy&Pfb>QX^jBz)8%e_;l&hetRLNjQGy9eCT@rPNAB!oB^(e7wJkkv@XTlQD5hMONU z0tpyD`~WCMuyKo4(QuhMNLCgGZAUv^&e8KACVh@9yPHoFRHraG`4Wtws4RG#Fo*qe z!tgn90T_xq!jXr|KyAPiTg+Rj`VWfDGbFa(J@gT)7Nl(rh7}^OPn10u74&Z3+!MOQnupdPi8pb>j)}sv*QJ~b@N_{rlH}f z%aGdriC^Z`Kz^5h#K@m{*m`d{3CMQDrvF5lkB1NAxi;=@9?%38gN{(qRMDx8TDQ@yZ$ETN(4m>3B$0rf(tpfOXBoo(#>QFCtH|W1*k3#Rvnd{qKa6yw3 zy0!Um{|Co-8U>-~@y{2f27aP|dmUXVwgiXnoq@FBJ*0V34&ko-@H)~GJPSCE#`Y%I zZJ&cZ;%%Tj>pmuM&w;WpYp6;4a>F|QZ( z%!KG5Q50__@Vs~y)Bn$xspE1x0;wlpFu#dvsddAmH}Cj`;s|a()g~aJU}amPo>rf3I=b)^+UK980u0HIcM)-qH29-t&__ znvi+lXE2gu#%yC>NQ35J4$*!!8H1)u!lzn}qwibIx6hb?lYc#I$et*TSBm2?BwLfG zH${ZBuI(bq)f^YjF%cMrKwMp~&+73PLfb7k3>w#i^uevfJ_ zdnQAXt zd+iE&mBHn(qAo&z{t3{W*#ioutzZ(Q!HkLIpwVC>7Hv5MGE<8&den+74QR%<&9MYk zfAOZy(&Eqb6=WnXNx`j_pS=88S~N%~pYO^a;x&k;@$$O^S>f8%s8V&2zcWLXjaoGq zzmDF42af_lwP!xtZMK9RTDg$F!u%mwT*k3ZeyjpViF`-o?eUSb00I$7DrOK~=@=Wc|)3NeI$k8f?$Wc|4U2e?G9^tye zL(=efXde9a<{aZ&%TZfjh3qrj1n)GJa7w2n6Y)ulP1+I(*F!hqK2Z-`uVKL$sREWv z$;2oB9iT$tF63ZbFS;p?kY)F)Y3hr2RH=6moZK4m?Wbp;cvTis%;Jc0!w!C}M-}!D zZ6d`1wa^>79n7A{z^irv<`0o#?v)*-t$Q4-f|8U$^y&rJeP=oU8&|Ym8SZQ@Fd?3| zlODjvf4LCgu%uI1|=s)DrGB#DL@q8sqi;c!eEmt@@^Rd5;VDXi!gPuh~D4%^C0XyfLsB(vxqrtD$?22aEGeGeh0 zSB;xHJmP6ZUV@uStLP1v>CA2&DGa){0B?p0P=)!QAp6e_vQy?e20xa;cPG0cdYd5X zcbtU>e+c}U=!;_=Igsjc3{r>|?$A%fRh8)|Z7j)tzZ-?09+~4be;H<=HIdxbc4!C@ zdBZn;CWadtvfx;%4o26aHZZ?CkKPm!D?QjydY!xH1Qx zJ{M#MHz`1wJ`XVZILhyyPo`^a=I+Zq^yPGGGI-mWZ?`##XR2Th8?7?&hvFRex{DB9 zyElh_Zv*EhzA=>wmYt%@zi42Dxd0QnR|IW;=0Wm$f9N{lk0DDeu*K&gbkz>gNbO_T zm3|J5N953;;UZq=7lQ4^JQ#Uu!{w1~&`a&HIKJo_f8L*VY+1FPUA=UG95D{#dPv1I zHFg$G<#@az)}`2c%?Cs#J>>Fs%FMru8N4B*VwiS93+tkn_bgqU1s8;-q3FKlxMDY#&)l$uEbW{MvdL{6 zvz+2>FKwVXjmS)kB7GJzaQ96)s!3JjycO$_6lRi_X>WLQuZyx_K}t}zG05_|#snNH z&VpvHd&2vhkA7~p;KC5x?-F>> zYr&JrH((xb%)$Ng6VPO~7rbC6z}(PjXka7^A z`kii#cVzp{%*G0x5%eF4AmO({fgf-RO4Xa`yV(l7nx)3*k`HvhNesjvya}(D5AiI@ zdPt*Z5SX4%!{1?XAUR1KYj>yfghicsHq~2kFj2s=izfsc?cF$gM*@hO+$Hb4-_z}- z`IZ{L%~$fAE#@;y?C_hj@Y-Yns+L~i`bgZlb5xQpTQ(0rok~PImk`i) zk!51e#FFI1c(&?4q+fZ3uLPSg{OAbHas9FOC{ zA{R>{bGQJ!Z>tg;eL>bh&>GY;&!gxBJI-HH4`Ef6cq&$gj+U#E!N4x^;RfYZ?oYtl zo@F%t_Xbod^Fv6TgWl`ZU?_MSQ82xZVqPz-$`T}K(t}h;^yG4C#lmD~avns#&qs@r z%_OY*E?Mtt&ffJOrn=LuSv5zFN1$o|%RJ#F^thm=I71Qbkia11gas#8e2S)F+)f z#=8=69;QssM5&bPV0k1SKh!$HP_sR)kUEQIkt%TER4QDy`;8YOA7jkXNL(Xk@qd0@|y>oh1%+=0)g za{F7E46wQT8O0CX#_w8Z;j`}*u;A{;Zl)X$$m=3Rimbx{*Hf^_Z3d$^WPxEuKS-o( z3c1I)qoVkBke81m`c5nsT9`5hD^>Ud!{u0PNI+uZA8r86aXq878IMj)2#8OlYXfeP z2zMQ3-Sw@I_TV$gtY+BoSH;McJ*mySL*S;A3>sz`{3OoZbIB(TQk9Eo&%1?~{m8Px zHT@{uvyZ`{z+xOv2mq~^eB8Y-5d>CF!qzR7B;iX6y8lt40ekw0ulZ4!SW-xW{k8Cx zN;fpil#ziC_fUL~0T@<(goErOuBShPeC}C7g*s;8qluTHF|G)ur<)PquqhC+wHj)L zB5>SJ86@WvfpU2+=e8JRHN|4qT5eP`%|B1w`_+Q55xXd#$1 z4ukH~t*m>KE=W&Vi<)u{Ap1)JUYW1udi2fonB!^q>v{+bj!(kRZci}kNV*jhJOM5# zCZeQFDQ}RZLBxm+`AEaHf`dMT=Br|Pme>tyooQFm=U51xl`@fZ&a@=SyR_KskOSnNKqm=W~!9nw~tZ*H@s$H449vPQDR4%s8FxOA+O8u$x0ptrs>e6P6^gH?jo+8xfLYk8bRF43H#%3;{&)w+uzp1=EnlW z)BPTdddM?33Sx<*Qy%#frqA5}kpNZKDmlJzDm?LdL<({>$W8y_c=53$bN&4^a8K)k z7PrIjeC9T=8$L!Zok)ag%QrleC*veaQVnbMn<3-D5J5K^UPs|!P%z*!{rBgP+Y_@e z^u>CZVX6VG&MHjy4ds`$8x0DF8f}$tT{&P9Jo}y?u z?A!n&+m*OGf+F#|vJ*{D0^1g0OI97rK;>trn+6!TfVg+T(g$S z3oZrMYiA*AJP~neDRwm_L*$+1@MKFrSrV**jw>F(dV`pTNm7w`q-`l`9_q!n27);I zof2uVi^2;>AMlDyj^d_$Un!$d37#=WQATS4ye_+kmmRw3sV$RXztSu=c`tz2dr|sz zD4qY7RYdJy=h68l5A!zLz~7(2XqIP4ID#XDY|q69O*bLrcN`GYN?LFC0?uzWp`GPz z(6RCw4KBIIbBXf6nLe>Fb@y!cT%Zy=kQC38zwX9=ra4Xo%m3h3>l(5h-_WxBEU>@z zAMt8z;;9;?@ITx#fKp}%+nUZ>O+A!KTiWAr>tSw|GI$)#(&gAz;P|1&NAS}tRYw1C zDcOF)j515}aWPv)qFK&CgqPs3S}Xm1FPSKPdkqKL-Vp1@9B1Ze4PAdd1l;#XkmN(0 zOIB|S6mQ5u*AWK@6s;za3oMvpV}W$>&j%z&?l+ioenb1zS(p|S1~v~lv+s}3B)DFi z(cDl+y!s!)={7NN7?h!!+m~Tq=ms=bO(91naI2)$9Q6Dz9-j5cGnLmluXpSPB+g-Y zc*-(V;pWlxS9Dq1U9lj^^)$UUzJpg`S72~M1gZH?3U@C%ho{BHan49^gSy^%xVE+w zg+7-yGdl7GU)*_F<6XEWEXZ>!Up$v&wD> zgx-K=5;Ok_>FoT2L;aTcI4GY6jW^-T8Yw6XT|%`94_{Bphxx#(adE~)#$A?C(*|#5%;^Ka6Nu50{kKj zP8e0tuE%dd?fpgEy2TrUUzd^`S3TUYQixIU`G+IsJR-M3o4K=EiG4OR4uT`TkPB&* znEvDl*G04halLu0|Kw&Y2|SG}4yD2(ug5U9=MlPis$+0f9f*1P@;2|UhA--3D3N;v zXT(lny9NX4;2a;^{rn*&O_yS#cwb2G!a%Oa9u4cZ8!%4O<}#kM*E7%aHdCVV%5*0kN9jd3Rf{|)fPDI z_8mNfER=|ivAH=#aV3DxXh(raU# zFzQ{w@u>vqdgDb6P3}+8=KW>JpOC~4%&H+N)!Q*sWd~WuJ?HHvYJx&|1r~RFqmn11 zfKz+G>U9H9U6lOQHm zk2eT1IZ}~8dKyuvq>qN(Q)R9{MN+9*KnA2lSk0wGjCXLPkH1|{7-jyuOq)jmuYKS=vS*KxOr0G# z(eeY0el5f_OFV>d-8JBRt$>Pr9p+!X%4PXYhA^}84jfZFINx31>ZEP)jf&HTVhG`gf2ib$-^-bz`3`@LC$y^J2>S53EP>~VCg%+%Wt?# zu6M;@Rl{WFhMX_NBu!-wwEs`hnMYIgeqlUwM5d5J$Pg8Z!hQGAWT-Uwni|lkgwkA; zgbbk~Lr6*kLLn;Lcb}3(s7T4&AVX0|DoXmD-{00<>n`g(_q_Yr&+~cygI7ymk##Sh zV4{`*Cg(`8`Ag2>?i+H9#{)s`ekjZTaGUG08JY?mIu4L9`W|PT3}IGU_G02lA^*hh z<2Zc#Iaz6%4+o1ZcwH7Bpl)@Ld0vMQ#Kz~tV#TwZFJ>I$=Y4|OYhLX1Yk^oO9*J+; zjoBvY7!o?~FubpK#8Pb|HpBNIJlraRCya;i!IWvN%X4lI75)g~gE$s3lZ<3b8qD?O z=7F2(IH+`$o}C;)wyl>%30{ubuD2?TO*E&8ojVPuE}SGwPUnJJ`d{MZeu%fJb(A+h zF%st9H-_Y0!%uZTMBpQ;3J{eR+@ zPhI$HnCnAuwxnr7K|DrjoFr6mUj5{Uu&p!+6UNWOu#F=o(N$o6I}(C+Ch*EEb$A@~ z6KZ}I!rO-xRJ-UK+&bL_m+w5oc;{#Qg(JUMWQX(?ioRJ_I`S6MNn)7-!#M7u*Jw zssBh@tTEFz^9km7_hRIaFwWDlkS!ioN8|IaK+@n8)BMUAhfSM_x63_rnb<~lO$h~6 z0|i#(Oe;(XrZ>nNUhIzYq6tzqHC zbEv$xh?xGHO>0#;Ky_{|9!y?`x90pu8;iH0y58NA@ADrK|J0A5-*y1n1Vk8*Y00qS z3;t4u$gd3dYV{i8iD9@GQ)S5l|Oq69Z23QwK~zbWtS{SY8G);X)8? z%z4ft?P2WwOK{rOO2+Tl5)b1!bVc4Wc*n7NfX!V==Ft~^iTe*H@cH{3m{)YhAjl^hH^cf5fI8_#2+axom8djf=m zMd_9Z5m=XRiKYqr;L^Qx(*2_ZRYh&lQ*r{M?D>ftVxzzzyB^-o--iKxqU>7k-njg? zBKiFG9KJedj-Im*kWC~DQcgbv?-m6dvfjvkaZRLOOqys@kRUAf5WOGwcsn+K6vj^F zL^4;Ehj%9~#<9-1%$fWTeD9;F=C&`Ev$p zxfpzJnp3%NhvByDeQI}i4r8;R6T~YO;nxyH;MGp&DcBr>)xK$%_~$hpt3689&Q8WZ z(%Z?1%0yli$EMfqJVv=b82Wzxb$D#+3og~R5dZEQ8mq#_Kn9-;Z{=lIun!;_uMX0z_AgIELUUnCuCxE-7he!S3$wY-y!aWFc4-X#@W|! z?A>_SRW=n4wW?s*%3kt1C>OD)jxAm~;;~AnHbBhT6Y31Ki=rY$j zb{Y4W#6pwPUZ7LW*c`8y@M`8Jba8qOzYcOfIC*_=S8>2-+XvV-=O(HxyG}I+U*O`( zCe*UMOwZjP#8%H(u(uHAM_Uw;@E@9teSawKeB(ej*Cy~veZRrJqnboS_cp#h+>B*$ z8+dB76xo47H#R6-fSIyG9s6G#=Cf}1h`Ns^8qVJfXG5RT2Qh~*|28)RPu>e9OUl87 z+wsi4+=6ymcTg?x4(@sSfER8Y4EYktAfDaHd5IQdY>y;1p&n~AN|?@(7ihjAlazmm z#gx+fd=Xj*+P@CK=X`N&ydlRf_WeTSJr|lkzZne|45C3rSPS;8dRtn$aX ztn%*1klkX$uW%M%>!)R)_45MI+IWUv(J9Tg`xatyuOd0I;u~6D`@s3g<3Z3~g_-UI zxIn%ZT9bPr>R3Cx>z&AXD_^4K+gDIEGaB1m4ba0;mzHU#K+2}$xZ!OL>R+|s$KQJd zs_b9X$bSw=2R_j6KhA*tZY828DhmVVmLz=Hf3U|Zl=V(=pnvKf5Sb#n0}z8d{3;759`Z$QY)HZ%!{fYHmRamBbadcD+R$DKJgW?u@8 zE2-zF_uWAGtPFA|@_iqM-T2I4X2(K=HGkB9BRsP#WdC$H9gwLu~*Q~X1E zH}c6d?~NF{_ziXkN%2<7BUpLNMBA<;(tA&ljmhINE;Fxi4C@Q9Y1tEyxR6Kl)_()f z{hp|4&ByFZ4WJCa}oj-_?jcSM3TxG#a~L)DPl=tZO0cAiV32xQC3vvZbsq0hdX zU?sL57Q#h1B=Lg=pGyJ{q38Upje=Z%$}1ermxtUoW#*6rkV*e>d#t}xX+FnnF#f^k ziFe7v-ptFCQCJE23M%;eell8@Zv*+HDq1o)4APHNpjqqzI9N|*+#cS;B}rcV4(nxT zf5;t&(=WjHR%forNv%AhY+=rK0QyQya8TJ)=)#Kh&E!~TD1ps{*Q$>b29g!4$}m5C4&F_ESDCECT8?+0{h@x6g*6Mc+{LRs5lG8 z$F;%h+d4ekD#N?8@+#(BHO9_@HN0>7ry$(<1__anV%+ZL(+E!MUr@XcthY{rRX4k^ zSTTnuKx=6lcQ;j>EP$~cN-o1! zk#zJae}MvVJ-+?oqP6?(+f;XTnhVu9D? zH1O23L(t*%ojfz^AyT2H81}{vSG=1*lk)G=hRqhJCi#te_a25+wPSEd=q&CDNrTV@ zZ(+Ro9Z0!~FslvE!05rZ_-c0mwsXI+cmGc11!Wdmw8m?=Y%r18s2=EhAY$Shxk5q?!@lvBhpnKapx9|iHz zOJ!1=_ZVHSCcu@M*Ks6akZ!$|Px@v#;L>aPD9v#mR6pmC8Y4+~DKCK$+FzmJR~TJd z#^+DboQeDYslj$j4PKSUB4+*H-%#lLAB0KA!tuNi%y0#GF`dh72TWpChff1@j>*5U zU@n+UMRYnK4uP-U&^TXhrpJ63BeH`r!Hx6Cy_JR%D;D0TYBR_0Tp*0WG%&GGfDDIQ zcu4Ii#@>F6NiqI#S*o2RCBH<+x#=|i$78sdEI}3j>a!ix1_Yyig0Hays`H}wo2_N> z{Q3mCvT+V`P=yC&E%{JxbOvrX&c$q#Kb@uWLbvR4}W zwCC}iA4TIFr_)g5Go>Wu+d{S>laWlMGtI53sZ}NICY$x_6jbL8A z2BS8LvXYxQ?|0fXrtOLkNeLHa??=pJ#U`Zi##S0|UiA0SBxlQ3#@(SSI^R)~=sLD!_k4$h=NdoUwUSOgFpEAbpo^ zar)6Xtz7Cyl4d@|{iBmv!8_5oUga^!eL6+`zAfg3(K@{FMk<&b;C}xL1(?_GhG_MSsmzjNJF&W=3z~$V;!-*j z#^lGKsqiML$&`gCwvo$4H;~FtvTR>rG|u2jV4-^^8SW_ri&7i>URlFCyq4nv@*{}S zvtQ`eJAo}V?1b_B**JJjgUt!dMwgRsp`kZ{^fYgSjG$KR`aKRG*B^j>_lKl3X%EpB zRm6t3HIQ>$ie2?_FBJIp5ceq>7$84{-Cc>KviUAf(#YdAPZl9XmzKiaOAPTAe+hG@ zz9YvAdLW`N1hoto;Dd+Pc&T%1L1W=8#;Wr$R_;ATt@8DltJ`aE{Yw^SAKnB(e_lhk z(^dGO%mW9_*Q9leISsATFKP4EBLa(8KxoW2+I{~Fy2Tjs56qD$?w+C!()s7;lCEzk zGCd1}xLnb2j|f8=LtxIu&4g)~MXyv}#le^kTCXC36@$BQ!^2Z>$kYLTrGB6`VybX# zODsThF_|Fc50<}n;CB&yCSY1F=I)z-8ZPIk+|koGI;6lZ$~lc&&RR2bYA!;6unD_U z@e;WE&c>zk=FFvcUc@K(6=*e@!h6+HDqWukQ$?@v#3rBTd*AqvoIauq!|L<+BZZlG ztKJ_z&iVx}qWsJg+G;RA<`NSx-$4qGx?|^_Yp|vJDI|3~!D)-H@jpMfPejvq!AdsO zTykS9`ny+ATbpb!vzx~p|7!p$q@MHTW#i;y`M|UkqozU>8dNT(8N+jd{8@wk=`ZkP zy(qE$;)IbCFJg7kTh!be1!cDz;Q8t^)Pw7jjWceC1ILDF)15!`+j#E?{XusL;eTNF8M@%xldx%sT7nQ-Ui}srZl`s6Y8YTk)F(EQuCiHx09?y555dq z-D;(+859k*PqCp|T0G|i=@9qQ9BkF3;Ny=GGJEY>u%4BPG12+B6bv|x*@7xu@4zyV zE$k^D8Fs&uINM`hLj+F5;p_jV;;%PCY*PZC4t}uVxV_mVPsRzlpOoN}KZQ6h@(Pc7 zd*IOGT#h9%#%r=1=4GuoNoxB`P~z!CCir~_gjQaL_<}U(728a9tQaG+x$l|qd;y#b z@q=Aju z@0;L}BV6yMkQ3~;n8eiP+C$oZC-D1`_ps;5JkWZVg0_D%@W8%dc+YG^F@>un$1t8g zdi|SJ^p8Qkn=mFn%Yy!A(&!iW4}1!Z$?VA~@LPW-mh3+TRbuUAZKOK(w0?wOK_eKQ zkp=QPyI|z&U+O+5lzZk^gMNz-++L}~78N}Q-yttN(N)Z!`#Th`I+fGPFi|ci@s7aK z2nau3jfSOX$&ENA=1X+|ob`CXA2_oc`r8xmd$c#I8XN(^ojy>NexJ&>ABDRGSI{PV zIv&rsLTc&^*g@+QlzDd;tOrXWb-N$jT6&RVORr!wo*V*9u|+-gpJ;SE9frcMz@kMW z%&n5<;;91J0ME8!$+Q@h=~rR#z#LTE8UaRr^(EbZ3Q3ptbDp3339$Lz0Vz*bKt^a9 z==44&eFH~9h0BTS|GA8v(x<_7=R++1bQBoROLT3s4s-p`IUIg14xPQ(*e+?$xczRU zbLA9aN2U!ZE!zMlD(4|kXwE7rdh_C-LqBOZL62GC7#{ z2|8D4FlqW@q_QS~6%;puoO|+c{8tYijCyW1L19qgkm(A&v{DhJ9y`oiel4NbtY@~ zOP+M#N}Td=jrqNLb7HVzA{(xTa5cLF%jNH4`nGetKS46kC2fL4Lz9#`5_V+AA@a3B z0rvUF!%DMEnDs;xI&K$|)h&iB)5`HhEi1tF;~(NyE=E(Hn!+DxVeqL7$EZMII#R4a z+0$GmeewbR7MVt}YRV`cowys#AE`5sBM$L(#9GLY-L_Ew;WWyxegG;PGr7Lgr_^#I zw^NgO50xhqpjl9#?J3U2pqfr%wM3P%{#QbF{=S83+?~g&Op7_}HUXo(XRuov9;1H1 zE{KR;4!fsv+VS6qxcd5Bj*+B6S?4s+=Lk(IoPOUO`Gt6UzM+MWj`3svK7|6W3hEJ6 zz*~F!G*7IlnC#v=3gO?J5JPJ)zjk5>u@!63UknI z7RMhSfIr4M_$Xlrb2`GQ+pQ6N-KInyEY5}U!AJ;6n@T*LId;TWKb$f;iQPRW!j|l@ zVB9%9_|M}I@UKuumdhB%zBz#qD4;tJG-CMaccOvT4l{FJ!M5cOgK zJ1{(rH6L4tNr$~*z29H{S-JCQlj@Fw3g*aib9TM}1Je(Qv5vi0VW#T@T=vcnyE3&{ zZ?~VEwkFMSOyl8%?@I{j`iSqv4uE>}7wD)-CMRB2f%1vjOhGiKmxLW9HNBTWX>TNK zS*ZiB%}p8q$uanL@Eg`Xq{x6I=7|D#iBt4#^D3g_5yo7>0=8AW<~N;%A}Nrw4z4nUDZ z3>xlU4XO-)-i~ZM^PeV8H!H;wYZLZbx;Qg1?19%SKjCNJ2=e-nBzokeg6_?8)ZBPB z9C)k)BHJp-lCtiSymtbu@uX+y>ZfmBS#kw5p0m`94&d7RKTtuo4Ncc4@S5wm`Ssx} zfT2((zFh%H`$P28yTg&RUtrdoB&tv@0M^lOFgZ#TTaOu$hFi-qQ)m}Een}2D<)+}u zd9V1-+(el{!4~o}Gm)C#zKf}fo;37Y7ddbY@$t1@TJFkqknYdK)r z)uDa5Fk?33444UGlAohEFM=>m(e!}7ExqL3g>ZEGxC%G!6hQw6q4;@J6K)AbQkUnq zF=^@;*qqSDOX8Yju1YfY)td4C?@Kn{^qmG@jw26JD5Qq>Kye@EX@BVjrEjL9z@Hq_ zn=Qh|HQz*$yPJ{!E`gixg<;#u7<5WZ!LcD_Hs_BvjWM%?9mlqCx$hY`8W#s1TnAEI zNj}zO8AEP@5Yws4!%eal7_Og%%w*MS0^BAF){$beJIGv0NTCpx>TT0sh zI`RrcV@a>Y5Pzk@AWmNA3Jz(0SbtWCX&YP);(NJH-tTp!;Qkwux<`tsHjO7i2{-U^ z=@k%jox~o@4&arLV4Q071HKs6^Uf3|0V#U}PDg5>z{Ht~e(Q%rx^HlU$sk{I+Y-X& zC*za#Mc8LGMuk78;;-vwW@0`8@XI$7_6auOzn*a3k%#H9;3tD;ht%<0c?Yz=&ci)d zx!$=QR`A{}igc=(V&gh#l3#rhCs<3O!8R?j&n!0U?pL(W zlw;~N+`!xD8uU+CNv40-BHF9(lM;zQlG3yR)!9d!E^`bI4OL^{_X*Hc?uy!6?`;0K zKASGX^~C#g9N80Rs5Q4I3qI`$n+%trTiR-v|MmnkJ+%h!x?LlOQk6)*bi!9)<`FfH*{18+f@1yYd zD`Z7Y80e4ALwn0H_}0pG<5`uF)!j<4`t(%Rpz8qN@Qeb~C~hZik|JPQG?5Kk5km4D zW5HUYgznJT3ED|bq$znNq^B!^xBXA@Fj$3+yLuB;{_**dr`yq*n@K!9lJFYGiEkUt z>GzfnzhvjH5BHq8}>9KHot{7JINiaP<3ZSaQb*s#L zgO#e?Fk$cwZV7Frj(s99Y!!}aS*DCl{tU)X`55%)XtLhyO|o-B1q}9Ff*#`@e#M>d z7~1q3!tY#1sej8st#&4QGG}2jH$T{fX)r4)r$Ef9ZjM>~iq3aS!hl&1XiZof?0(sZ zZ9lJ}&DuU_Ub76{ue^fX>MYWg^AZ;uO0#1ghap_-2`G)9rnmYp;`|-LIQz$W{4i6~ zym4F!^-`>uOT*d3+^Lfq-#EuB)IA9`$8VD5#}hD9V z^NxHy#S>y8@IdZFNa~!)7F6*e;M!i$vvl4%@Xsy#Jqc13(DBv z0o_PtqI9K>Ru2E=AMi*ez)5^Ddn!j4bp@saO8YV{_7-tGBAxA$qXmxGMp$IkaSU!@if zegg76=|0*oGJsF1S8+Yp4_a=oh-T4>RKxKkD=oz_6|&c%+{z5>xmt@pL?1_99mZQ+ zHiG%_nm8Aq!ULOJ*zCrCaO#>EXda)(@q)N_P~#Pz=fG6(F-(Q&Yvb^fwiwRV(8VlF z!qG{^c+fR~Ms3!@#f`gA-1aCQXE=>&$z*6f@d#~QV`0^diHw$W1ZW@mijUQO_`-Qc ztlgq^F7J~BHIe0DU!IJ6$9E9(2}@X$fGQ{}2*vNKLeTowLfks*4frL!h3Bm`yn+d( zWYUHdUeAY9VEH8vY=%$KD!w({Ei8}w(j$qN+*J0I-#7B4Yd)F3sGPKk9EHj2Jkk2! zZr0CNkQFQAcJvxkAs$cDYs-?*ZjK~hNjw-TMVFJfH^Y3P>tiT(zJk7aatn*@OkxDq ztFY}YbMWzt-84EX16<@-+(VUwZ&dhQ;B}jkV}j{)?Po^x2r| zCd}%;j_#U~)XBI9M}^19gDtL@{k8-KFC2$U-xgDc7b7V2#T)+~(E@{_Wze_v72j`l z3Pd||JwUqWNyq#MV(;1ne&|c5T$&9fo{wqQECIG)(Pz9D+XTrI5k#u9;K$qmblW}% zljdB5#QlFkHN^rwL|@_fIdkUh<7t@JHkZbXKZl*4D#5tuFP^*}3a4^M`LEnB^Y3{b z1ey67n49^U#La60<+l?tWpxB%+wJ zFa0~YkJJwo^G}tB!vgm%%0(byUwS7>Jg(x&Z)t!@s;2Ox=@VN$AD^qGh9p zJB#*G%QKxA_+AP=Tq-AvC+>li8Kd-p#RVuV>4gPb?9l1BD+XSDgdLG-^o?8yEH0Z4 zSM#`>`P@7>a!sARt2hdu{*&RI(7yp$2i~D0=PlTDL5_KFED^622f+RL2CSOMFOL6D zG3C<}D36z9r*E<*ZBt%D9oL)OQ}zkcVvX6|Bd+`s7dsrtaz{Pmlc@1xCp3GvV@Od3 zO}$wM4kO1Pu)~}=(iV#r3+~WbQ62Jb$zM74gfLz|?aAOrf(0GbDc*;IRpeFLKcPds_ zMFYJSVx#YIxVa|;2JaTq`=G}B-#ba>_{FjRpuAL!W!J@; z_v!3Lg-I>6YIZR0nA-!h~n|X1ov|x#?6x*VZ!f}-)=qs1= zF#SwDY6VWhU!{rkgXTg;bn9jO5Wk1se;`FNebqs{TahTxtEFZz1xr6+@4BCK+blo+-G`CzY*!5C&d7sURgOT5mZA6QcEfXHOgMRD| zFe&{+ClA;${t^<*FHe0o%~X=E-dIe#Ue2Ruy4}#$cOfa4O2PPN=4|EtpXBb6@9?7Q zBsjXnuvVXJP`&;N1SoNS+2brukQTxwM`>17Y!zP9l7JO%{;2!s8a#MXjCQixOzoX? zYJBG|L}osv684;CH#?FsvC?EJzw`0)>yt2WVmnim>;SvW&vHD(F4A$Y0(?4q@Xx$X z%v5ZK%tlePTz>{@H|_@d&=pr(>kxm_?^!VM5KPw zM=e%(euX$X3Y-$#r4>q6n^I+osnC0;1&>}-M8m)qI`lgOZR91`5l#>B*>1=- z{^Pp#QXLtQIg4?7w=@QuOlHoDB%tm?GqhU1Ezici8g^Ln z`97xH3gm@-&}ZwIK;k2K1&OUQl$5emLN5>-WVh2n^9NvLuf!fm_QzlTw@HNF1$c9I zGCaPM1f#7IV6%29b7R#kcGT<$HF8MC>Lt-+yR|oaS2!0h-@FHAJ1n56D~9u6%8}_q zqKsD!pSUd82LA10q^tEl8ClMC-@RSM?70|;qbK~aO6(ts-^;=DaABA&{GJ|qdCdG> z#(B^*TFp)>wgu(TRrDzTJ5OwBH@JUOWV;Nz5xxv#*~SS_xlfi2xbYbNCe4IWPa(!+ z$1>(x$!>aR>o|4$Cj-&uiYciz5mhG615*AsgJfO$}PZws2 zss?}Vzcl>Iak+}>^LgQ>Z^>Ps5lL ztx=@vm?Ovb-vdeS<4JOYAhZrVff6-$^R%^dc~#dqMYZXZ+{Id3=^lg$>CUaEF338xnpB4U+#uc8>?>&JP0PMZ(x^ zUq{!*@=a{Q0z^fZB zFAut}Zj)E?wHW<70wLZJCtRJ)6m1}keBUBgCvy;&ls7<49>Jh@FTqBMg?)B65!o{8 z`@;-Or3=Aod@6)j*pVfbuV~)AU}AdoA`~PTg`v=bONebaBj*(?o0d0xlI% zz_3H#a4MA2U=M*_k>Ed;NAOLmcLJf;~7;p>C@$U5g7-f)!$ z9_+pap{mblpzn4_3pxT7pa1b6EfHh)o)X1Kx261^cuh8Qr35uzVTFS}VdS;CC+Um5 zL1G*D#IipINJuyQ9nB#V4m#oGPAjS;qs9o8U%*76esCIeB9&u7#K`3+@0^(#o88qn9Z8Qb=WzV)Aj8Y*jkr@O`6GA zIzA1RZx50fmtrsv$^tLLCK#{|rtZ3eSTNp+BgWn(@ zZsRSImR1K#UG_lW9ZBfAWq@;xWm&^8Q?%do3hj*kg6D%oaJUx7pW3?`)^5{bwieuh zN{7o}R>ghqCtTJe(Fe9%S7R1PbDH{tWvK4S{r;ID95+`02W1};Y%Ks)Wn3m=T?n3N zk!BSuqe0kZ5i@#tE-_a*2M2}g$P)7|67GD3cRXK<>{uzur2VkRpXYTUUiBqLZdn2e z_m6RVwR5I!Q0asGlk3DnB3nC4t5 zC?f}5He#mBO(MUg1|M{7z{$4ZVE*eZp}$xdl@MlZlvm-ilrz*+Nt)D1%fdNxS2V1W zWG+}6L3y+q^c)PpgBf`wTwM&8cAh6aZVQm^*amn03+222d58`E&X6c^g19>;L!!zQ z5G@fRUyCX3&y2y|R4r!Zy{8ay_$&SOLITINPtt~+ZJ53V{=->*f+`7h%=MT zeI4H+i3s1Y|yYzlg2&I1gUS3FKJm52`NU?8C{%!2G_aHjN0a|P+++`Pbt_zzr{lH_EzYKAB?w6Po4KQ4eXZ`5GVst34L zs|-wo+G$Ch5<7MBdWftk;Bu(q9Q*wgZ7-uugdZYc4R_T zkSVOPk!S18L|NDrOf$5~iM9JjQWQ6d_{oZ~b*8~V zI1-ADQ1>AhW?e`khWcWRu=NIWOVLj9_RMC;N@LJz-7jipbO}S=gGPWNqyx3wi%o4a7AXJ|n41uAAy)Gx~BGt(HO^ zI^Nb~g^a$Veo_W8zc?E{=gNa$$pYqJ`d@mll27Mr8!&zPw{gDnS1RK?8Qb?=!OZSy z5Gmfmi*=J{GpeS8tnO4csJxQA;tc^&urw3BEzTDH@kCFKTVuXN0dFreWCUh4z=@+TkU zdt)@LesUgG^hg%x{8MKZZH@+;iwNrsi|N#B5$LU-3zIl~`$3*D7zmF+G4Cm@8x$&u z+-;1JQS-qs;1+&xHo^v<*U;Y)gYorFP#L5H^FE4#-8-&hGdcs?HmI^E{>=dCe+RJo zq#yNr)r{TVBd{WD=6<$+^q71&rg z18biPb9F0mFmpe=+R#8Ug%u$~?-(jAe*oUwAM^5)UgPM2narf4EPCAS=iM&3hxa2g zKww%lFHZXldGWCXc3lX=%>GHR$-ft5Dkd^(9_C}7qCNYLP2w^RD)^Ny#KL)cbn&V* zD)7sJeV>Q;Ym+sjEwT~EUo>z%31`qDfZMTN`$uy=_w!a=*bWX}T1@eg>r_1dColGJ z6c(J;Lecu=xL*Amtk3yDk6K+Lev33<`05)xF+l~_-g^UUMQ=hzpFTQ-&jE|JAB4`3 zAeZ^Gm}8YG=BmkNpfPzIr*mhzVBdQ5%!`-za?lGFo zoH9KH_PP$t@`4LgkkdoGzU`s0wg2dIc_XIV=_Oq`aVfmYdrYLa^XboVRWdy11~0$P zmJzqzh~5qF(59k*)QruAqdz5a@a7+~bZ;W8_FDlio?|Fix0Nc7arv=tzy$nu!9wc> z>|<`>89i5wl`MekZ!bbkC+8>aISF=$4r7XH7M&O!2x{{L%sW;sVz%v_PJg@41g+jy zY_<=_m>vlxcHK0Le>t06$(3TnLMU(D)`$3b?o=k@z)DnCxJTdZ5W@{m*MqX574Gq5 zY5G|T+ZT=S9MXSN13O1}cCrmrcbQ;YD)8NrMTC-3xhK_V0SoY4%qCS(@9Lg-gRC z;NS;f4ts8f?*}HZ0!|n4;hH#nKkG4(Tz-*ExI7B$niFBcsr~S_tdTZGgpiamQ(!VC zGIRYj;lStlc&PCbHt))ZXNNrbGvj)Z3^3FxbOXn*+JJXoe!>ZL*=XMIh?lPW3oNfa zf}yIV5WV3vF1eHshLv$-60t(72Y29*(j-VPGGYtP0PEELk?yk!z%1)3QuyCeoOIoj zs=aK7rqiDYk2@2BXA`kZY64E_ki#+|UCjO4k4F^6X>DRR5g4?<8sQgc`z!`b*F1u- z>535Y5pePDYLMuXh5iq{@LbUXrMo)l-jjKF*&>nGdHW))_YmVpe%Gb8XP)3r*O#!x za+s>UdyXeFqv4@hD99~3gxT54z&wd?yZH%>wbncOTs?)@n%2@K-if%?a269TCBM#2C@-Mbx8{>%RQF6>7LFsmI$ONH{+T6+Ul~ZMcs=PBwyJ%|0m4OCn))wN&SD zBndDVVwA6bq`5nMFy!c4urQm*hFW!z)R$Up`IfU()lZHTqG88DEmjL-#u|8HJu`_+L_dx+PXv+0RXA`+5EOoYMdRX2 z7*it$cNLeRW|BH?>eXNqV`sp&?hv;0c_z+2z5{pod*RGjPyBT}4#^uOw)?sab912# zRoZe3&1|QT!-^Mh;Nv&k6UD=kpbokrV+z^-MV3k1rOP+;cP1;kdSTk8GZ34x2M>?9 z16Z}ejq8`OW$PuV)u=#EZs%yI6>GB#t*^N!r;05yN#EZGbLL;gZL-4HSX09HnAi?RXFrpQ?ZV6jNl84>mxCcDnOON$j&*oC6-AXb zFz(AXI!Ssxgp{cb{d`5{opzwk?Y9xH7e`~s_ zG^fM#jI^Lsp%B|6eG+F(5`*>iGOYae96Yn%nfCjx0-GK={C+4DDqlP!I_qu_#`iq+ z=$*mXiA>|=Tl@owFj3qt)5l+xCks>EJW0n{Z3yp?WxJCKaW9$3fWZ#F11rPo&8emK zrV7ljs=3fTxezBgn_$(wQy@CN3C3Rs@w2s8le5R3K`Gbi96qBB!XDhjuLqXnlvR?< z=&bW(!ZA%oYKJNMKa}NLA1uMW+m)o~3E&e$j)5CqOM545gc(i)@5L4LrE9PJmGp6oMP<|Fwq)RCQY{sfUMTt~t_GO08J1Oq{QGZH&vvCvhEp zQtV=Xg5bzj>JPX$!jfkMg&Sa^AXQO?+J`7M4oN zV?f4l;_RZuX%0;&E+olJmidh8*FsU#Q4$)YGJ4x8)q(~I4Za|`rEA6)xBg)$n(BR%f?E94lhq_+S z5!s74a>SO`w@`)6Dbiv!j@_XKB`R3y`yO{)76A3_PE3vB5Xf*lz~-_+^UvFkK!DW< zFWjULJ2nb|`TWz6DV7cSnX54B3d2@bE~V$<70@oz3>m|4kpM)bXD$S7El=wc(Xx@gVOm0TNUD z_=(37sI;jKcwUTz9+yCRZ~G@|ZJ&rkoOaXI?Z*=~N`nS73mSe+i%F4-#a(LGA=G6A z`}TLE_5)As#UuP!CYN`0OqFTx)CZT)MbN$`n?_9&W$pLB#JEek=#kOE4}C5Np=w+f z$ufd7LX|<_{FnI0CzhK2G-Bg-jiHx`5v#NpIX~aIs&*aPDLtqhd12;ry!-JLzAm#0FOMW$(r1TlG zf;op!^Ff$-jUL~pu>TGVp+xpavcAKcESM8ASu2nMzRbt}~RTD6<2*SFwf?v)S=k zx$whpKL6HLF*ZiB-TZ{a2I4)-7bmU~WZi6XU}vcsv%T!P*@~^RaL{8LJsv5A%Qpy_ zy;@(2URj&bJcLrMqt9@{#GDedC3XC3@y_7Fb;hWR{|7}$BXswFNm%9^ji2I^!A<%q zhN!RLbVYOcA~p@RL5@> zq@avI1XDlf*r9uqxmm-*PV3vm>v(>- z#ehajzaf_{wbL!rxtt0Vas9%;tSFLU)!w^O|E?)`D{v6ABapvR;}rLdmy>b+9O`Kw zhl@wzAW`WyR+@6%(KF?keF2d;S#SvVz9dZW`7I>X@ila5UPL*K7mym;3ie*pP(>|> zFP7Z}ZhN=!7DQEYOyOX7(Xj~|%(lWg`K_>G+X(qu5{>qy8>!bZJ?7peX^0<{V6JXi z36tL(V82)7(1O)-;L?n8*m9rqWZr(v)0r|vE2poAMu~iwv*;a;+&POTMG3Gq#T_n+ zhGSEX85Xy3n%yQ{_JdRzbpMiOYYalL{9Fe%nkQoa+vQN*b_(3?x8pYD6!d6%jB8WH zSiKCc2iZ0XjQhGipZ7a1uLrdMdjP4Caqz-;8=d;7 zioUCx2FsrAfosQP*@y0xXpuXW36)gB`0amiRQwzSe{2&(-t&S66*m&d7BAt+Olo#8TyQrij;ZW>40R%-sQ5#f_0K#D9;@r9+9nYUE8hXumY2~+O_bE! zdPcs*Y-M-(FM_%k)0v3oLOgWW1mDMU%-s=TwAM{S+3I=ld2|9!onp^r=ab>IVl*~h z3Bi-+xf$zZb67L508QoRq4VK8*eq&@^`l+5AdkTX$09(sHwAO&IC8AcrfQoFC*gbH z2B?2%jA?4zyIa43E{|=3MR5vvvN)c03+nNNX%o)idIq;`j$x4eRJM52Ys`Jm?V&ev zx%!@Z)Udf%t$ECcus^cleU=!cy}AXlGq)qJ`V{GXc?IHrCXk~ijTqOm&88RCOjxfg1NE~n?Y zPNCV2P5i|oqUdY2jHx)R$mqYqyv zc5(ilVRE;y8AUTD;k*Y2Axz=X^?Nr1u&-SOD%-h!>GdKqr)&crSM$cfN)u3Xrvm5b zx3Fk(I@d`%D-hnYhpx%_hM8L)bAEs%ayCaF+Ge?e@y8U1v5&;>b+>3*?!hlY``*Y+;Kw9}!G^H+R9u6{L}*G-*NVINH_=uguMq;qi7N<4szU;+cEDH8hcEFAoVOVn6C-8XXcWQDc->UCk++zzmO$SW{izTCGU@$ z4qmq3O5Z0DN~@Dc$Sz~^rJ)TUbsxt zp!9JWxZJo!Ez(9nXn_TLLiPmtrZyiQ@6M$Cv$NsYfp}v7>>`wGsX;4NgY{4i#*1Ms zMDK3}){j-;UkIZ|j{ZfDO+u)IDdl21~@lgnmp5V<*xfY@SBs1hFcm)k4^z9moJA*%_M9) z{fWk^S&@3#Y&f}XBIEk%pTO{j6qmnH=Ulv}$@Xy;z;bhuUELMD#?`^(w%s@WIs8o= z|1%}BS2j_1XGivObvRb1c|!3@8T@o#5~^ldx&>GxZY< z3x9&9llS0Qt3PpDb{lnPHA2!rJ06NqsqQ=Vi+mW#1%C;KRo$?M>@yn2CjZrh%N7o3 zr*;FkZ;zx$7M0TlRv*FmFQo+u+_fv?Vd9@sTDjB$GwQ;y>+Az)&=Ej>XgYuR%Lr9D zbdV-leid~6-VC}H(yZRYVS#6AEBIy^plE+ABzju&HyU(8tavr3XMd;Z^PbUfl6_?C zO%04l%!aYk=iu&tlVQe;8xXg37Ir<*fY#+E{52YduqRO&?B<<;dk$4d_2Vd=ag7WL zeBt1_RiL0Yk%ZFMz0y4B8^SO7CU*SwlPq;|huT26cy>aC0Ds@;9`47A`^`L=K zfgR5dbMpjCOx)N>HP`nLuUJ#+_)tV3dN>OXP4^__lO{0%L5(<)!AE=hGpKYuxg|^yXN8dsh8m3`Z(~Py9`W3K9Pj@3vi@6pK7+9 zL#L^+xNqS(xZQ7n`9j^O?_|jFrJ`Y#gcf5*WDmQWBL)~u7qJJNw3#Qd5nyn)2v=7tar@vqXK)S)y=P=qo5o!LlC>72zv9QG$(e8kF}>0dn>JV;9(Y;E}QG@cjGh~}#IlUAXbYA9W3)UF? z#~**{oP^im7qHam7nS@O2|SLQS3Dd`J+`!<(O?JFShNyVPnyyGB@#^X)&rP%>IaS; zpUu1w|H})KX7MODa}Et(!Yl0+5D|e8Q)sQk589zgy@t zJ!j@z`&}T~vDDV20A@XXQ7t9fOu}4kAT8(#aU321pUpgWu=E~o>%0PvJ2SX_<0cwf zlS%>&M)^BbjRfA7BY1KC2KcV9l5;(HL9p9#xTn1oQiON{$BCu88k1-gv{~#q#!?F3Z8Rp+VPtVNZoO_C#gDOpu*>=7L*){!m_N4)9a-|qYnUm1G z6mgeGEZpXHb}s+K1kdCb;lb^-bj8`>>c;jZ=(cwshWB&3yI5@^vP_K1uh-)^ktOKF z`CC_N*fQ-mPI7<$I!w*3!gIwz#8Hjo*R>==Zlw zixQZ$8^Nb+3uduhsi3;n5u?VQ;mHeJhRNkDw3MHRTGfq^`{k$L&va$fJb8>1Em%t0 z-)|$+e4Rl3xHX(uRRFw!GE5TR3M(g4>K;2A&ld#4xs#$y-tq!`{J9bS`ytH)me-I! z)kawRFAnuvs|6G1OR&Ov-U7+7w`gX=&GG&Gsku!SNeqx9J*TVa(?%^ewYr47?Jy;8 z^9NBUkaLps%d^6gsyqfLt-IFa1UdM2xS))#h;!6mW?szEQ@zp%>sPy^W~n zfhD|2{AuU0sGY70H-yaScNHtie&x36ZmXAu?|j#C&L= z*+0b@>+PxF))R=!E=fXaZ7?}=_8UY6?m;e-iN?j6B&)>+pZ$?yuM~gAGKKTx8MlU( zg&EvD<2O#J5ylO5{c;;9u7)ki4=1dbfC!sqOh-GIJ(I-up+Ybu(ex zxpW*_Q;G+q6~Jg@2%AT;F?hm$D7+yH$LTawOsPSq7)fXg+eI#(FNVn1+GrYn46`Rq z#kb;Vm_YW@_gQP;$I0jDr@EM|@NdAUj3>;}|4u6$A48&4$@$Vb(MV3lzEF?>!Oi#$bxR8g~J1{-Xlc7Xz`FS?i-G41&Iiz;juD+475BmB1R6Nz|x3LS;a z=|{1fC?Z#YMBXZvvv#Hf29yd zZctj|9RgKeIXJDhh6t^RhixsUy!ST_Q_**4u&a^t7*1FK%MN{|`VG$nsh;;J>upH; z4016h+?0-1Bx3kTH#*t!Fegi#k+i;x0jU}MXNJ5_9aV+aTGCjuQiAp_c!nL+oSiV^FeElcf^4Y-^UU`T?9Ltms>6rBcb>v8 zVI@SFiLgU|3CwV6hpD1=XjH)08^&pyLHVmvm#Yys<~e+1so{|jo1#$gu6 zSQP2ILsWexW3p#8JvbP`2NV!F&t|SuI*Hl8?*nMg@<8i7p9DU)wAi(I zTd32NyHs)jLElY`4bA3evsr{JY(I|fgEBzJsWZ|uL|Is}9yZd6tVF2>w-^3L^uOM~ zto9VR9aIggg`KeM?GB90mSU}E-a)xd2SHv%KvL>D@kF^HJ8=0UIrmNnTfa?V+NLf* z$vdtnmY0Hi1~xJQ?iU5M_f)a1aR`NLIG$?SCN#?Cm=jC6{7#w}ey~)*WsnYf;ttsI z;1fn^9Y%wD0(k4T3!6O%ocI&X`E-8rw{J?tB@x#6G2;WQ?VE#oOI`Q@zG5^_&I-ow zVIlRC3mXv@jWuc+7;rR>zwGx)y323`&6dt3e}yP}j86+rox$dg3jXl}2hgGRIz1F2MBNT;K-mC} zCu?0qj~YLvg#n6S`)~*&%6+l^$x}EtJ_gdv>*>q$ifm8&8T$J10qPK$gb!k;u!*n5 zShvU;8gh&4KD_5TEYI|Cs4@e$Uf}$$4_j$zhxr14Y(3!}*01P&3REf9c%^ z87*_R?YbmhHPj{HZN>OL!v{qsPNVMIwxZA6ljufF1P!lRNXn=)dT{((%?e4>RZfBq z<`K3%)WeW8duHTXI|;6;CQFt~AWb*&Ns0Ig&`~#G>Wgk*!N40bZ^=&B@!&3A(@Dg> zzon${<064}_dSm1O~Gl43EO#L9c!8)$rO5C6fE9p0xH>-ko=%RFxJ6w8uBgR@qTB} z<+wI}T;^}37{~Q4&jqstRS54q2bpvj~j4DY_fsqx0BI^G?88)ERN#VCB=(1x;g zH$con3$?WvFkE2?_T%<2UwMHby*?Dhqa4tC#0cVcDC77|i|E+L2((n#jQ0|TX`^&2 z1}xb|=9oSsw{`xK4Y39Mp!3VYbkSTcpK~4THcdlb(h|1FXbR~wRpZ=E+wfb?d+?Bt z1NqFmI3qa^Te%&>nW2R!e#H}4(%JkG^YvVRB81}>DB{qJ82n}Vh5S{R3%&-j7<8{1 zQ{U+_3PIvn$9qSM21VHyN8Qk_qnExaXve@lQ{2w^mkmvpz?fSNQt>089mVa6CRW1N zk1^HR9>>9GvL`wyy1^-t3wXI_Gr*JrtBYM{Deo1>R5RqkNjEtZ>g^yMtG=PybsqMp zw32(n(P*EU3qiV*==b|8v06nQrEh-W7aSO-+p}gdLx*eyW^3E2%F)&MGAjhzJVc@G z`&CHYk^r|dK9I3B+mN?sBh$O(CRDh1b7#;-a)bvA!Le6L+PEy;J_({`?@B-Z zJ%p8ePT);xLllej2E79X9IGfAGrxsF>sw34XpbwNw@88}{hyS4ohT@Y;r6|m(X{7h zGQPPqOYmpPI#BiJSoGY!{(hhn^!TpC^>4Xv|0)JEG9+2AUNxqKnZ|X2#lhZX zGKshs3TwHYef;EEU}3xrUo07<8!r?=Y@8~qWKal(dm3T;dRsyJ+<9oPs{?ZB96R{q z3kcdYf`_@;V>w;N43AoaLC$9!ELepa_tVkF&yLn7o8rOnTi{l`7`A)>$aL6%M^q)* z;B#-mwono!9mCMP*@Z-I`b9lIUm`)fmh$6#ty!@tJ>Z&hu3GJ5Dnysb!Cnb_mMslN zLwRLphR;S=`S6<{{HHU@Tp9eLq$8b7Gc;qt!T*Zi?-a~{>-v>EMXWZ3+l(;?_eil9Y7i#Kq>6_ve$@l$RV z^j%KC?%@k?fSXy~V!c7!Oa-2brNDX52x6Kn4SS=evtJ`a1W&xL&>XkLxJCX0ejJJd zkNo4{UXwx^xqV@$KY-X?j;mZzN1wK?1^IR(?jD(emY_vG8Y-{?qJ*O{T9gd_AwKIk zj-9zXb}8nA+pZ5JKK2C6N_ogJ=S4w9@CZMZsDdi@403#4gJlclvG>ItI3u+R^vAT= zdjrmbn$$hGeRvR~^f+&T%2g6uti~kt6{AaxIBVZ*jV=4)Xn@6C^t;@E!Uty&&2h4@ zpsI`BIw4Q%Rky=5yLdYLggR->wj@&{ZbSX?1n5fm4St8-P%4403Fq||29J0B&OiwpD|Coyw zw&Ph_jp^jUepy`dNg3ws-;ShO1^P=)VfxKa#HLc5t@_5V(h7Z!a@LmEG=GL*_)0M- z^*p5?*EG>y)dXzXpvbB(T*g+7yv5CPrxRm+JxI>XM3vQY%o*o$crzMKgl@~SvW6~Oz}J1M4DY|m zg74@os&SEfxAw=QE&mt%IsBgF{K>$7lf{@@L*sCN*=tPIjl-K5!xv3Y0p89Z0i^r)4fi?Ab^y#{W>Qpfywh zW;`*(MT>M9jFp1CF*nkhy8sT1pUz|)Q)Y)Zl#s-g2{2*B8rF|#qH&{ZnAbv?_@*x% zqwe*PyT`|~1*MWqYM(h?I>_-eD=E5X?j)jLlBt!VA9e2f1+rK8=y*aCr6=VHWcS_0 z6A$C@-lc6g*)p3(H$4YG#spIK#lw-w5}>v-l&L>34c}O8p`Oh{;B?rv`t06wAadLj zR-ikhZSq1OeSZ)Qm4Bd(H}`!gd>v>&4~d_6marRIz|dEkofvQv?ryn`5|0wFraKF| zeCI=ORtYsPT1|cD+ya+t1MtRX20U7(M9LM{(h$RYxOcB2il1a4#AqGJ89%3EC#n!m zOG1BtE^b@TArO4Z(EW)RD=A2V_6gtMMalpjULwkD?VSu^=9l1HjUF3t-UDFAC0KH= zllzXiLFj6+|8CaQK<1a%)YqOkRHyc(KEY}|~&QZbLJ zKVO8~6?kmKwpJ2uc@YhlIFk|YM*{y~fH!+N&d^O0bc~^V@4_!w(C!85dg@>j+m8dE z2RJTq4rt+tSy5GV!KgG5D)d-{?|0mn>x1uv?o@G{bwq=y zR|td6PvnWEayux>e-otHn3D~U5Tdl_Lg_kV@Z!!>U8bHWYJG~k|25gBB*2;@|51+( zN;tTwn|~z#Cj@Yv$!|E1zVr))8QmMPetHzf_swNnLw2IGE)GHJGCnkhu2sS^8UAELdk2A*He zhTF|m0`c>Ke4E3aL{(A^v~LZ2)aztr2InIcAe~hBJ6uA9`HJiko9|FJuLO;ql^M77YGiO8;dTh6*wQbD zvE6?x4e!sTQa$7F;j>}vvDJY?%WmMO_rgRi+69%o5^>%gdx1-fE6wz1Apy%A@nX4k zH9KP$Mo-Y>ome-Qc{_>ghXl*3g>`ca{rVkIM z^KntjYCJYAo1|Wwhc9-P;>zozsGH#kS8L5!2ZtOw@ftU~eRq?*$}@ouzi2f5RR;-X z-PCqe9V(3_IsSe!eU)CuzyEmxQ{WH=suDJ2SnnF>YGlJ3=R(Sx-7I*2GYc0-H$$)W zAqWp~A_d+7{J&Gh;TXqa^llO(ks{sn!$>PiN+n~{z+0&76(`=~3UNi_C-N*;l@a-_ zPKRt7F+r_OU@7JRuC5Ja)bAv0QJut8hzQ8%OOuJJ%75INI*r`AcZ9Au+)A9kj8VNk z&uD%BY-*=<96n8)%f{KvMVDXKAkk|HF?n>0{CG8$w}!l?_cBj|m&``&&3po{wUk)R zdoQ@WUOa!3@d^6lY7(vHa)_IvIR@wS7Zi%}F^JD&f3@YH%47*vS(D2)R$joNpVQF3 zP@nUWpQi@GqBvZ<9d^4MflusgY%(f>d$(F?Xm%uHxFK-H#ejrnfVe^ z*9tRDTdz`6yR}%B459)Snf4Vl@(ocCZxJea75p`NZHvmB*3;jlboS5twB%Pdg+ zj6LeNU88L}%Q#ljVVtV%iYlqx{HzCHUniDc zad=r<10xE2;NY)p?AF>s%eeFU{KLU08W#lt3cHErhtps>#Q~JuxL$Ne8m@3a{5h-$ z4H^!x|M4%J8mfY>PU#SOSPX9)_Ta{o!8mX6O4JwpBZF?SbgN<#PW*A71e!g8?gM3L z_gaGM#b2Pc#or6*;VddJ|+EEWF4 zBt3j}a1_#Kf8g6b{zA6wUQaB3K7qGl)v#p$ekOfaCHk!>;@QonY7!juId5<9FjV;*K-RAsY4vfY6ci^Td z%T)0t*`CQ?V0ii<4HCI6*v-EQJ2qU#1?nHD!JLP@=lA1R-SJH3-#iE`I}fg{ z+7M$AjNA3M3Oa?ZQ%yHHa$B?%mrpTZ^sdZdQ)*)bH?5^0TPzTUepliKeHZMDFhsvz zb67P0CfO749)=?Pn<-p}g&gX0rwx^#ajJHFy-d4RfWI5jHJWkbaaK0bw zGVr&p#+-i(QNz&(N6{)SE5DR%2%Cu&Z{MTv%095;vM|{?63k|i zP$Xoi@{jTb{JUcu4%M4Mgzj`unpK7ev;RZS3(eL0=K}tU+6ylVJM;FM|Bs>ojku4-;i8U?f`wt*(3H`MnMF zqu~h%TezJZ-S!N2G)kfNA~_5cH^IyM6R3y!VUqd5n*ZtDH9W8=l1e?0Vyp%tV0DxP zeyg^mk-kNwMkj@D%I!OyKJ`(JU8Au1M;nYq{ltDx0pFo662G1dgNNU{KuJsz`=9S8 zb_bN9Wx5U1ArNKN-_Iovr+vUg_Au0oUgvgjt5_9bS8`}(1K(*w6;!PJNj4_!0-MZO zw3cy$?#asVw|hN3FliF&6g7@XT3E^-(B6tu$|M;lk#}${E)0fa%~{qZ$m6UWlTnlR)@aISs!yh8-CRU^6w5#N5unjURmlC+7DP(RVi~V><`6 zHZ5Xq&!{BcJ}aRqW+MhYlmji%&tS605#&XZz@gU{b|ub1`Gp}+qq>A&bA$(l$wG|Q z+*y3f8;x8B{U3CjekP4UVkl!7Wfg0$2p4`y!of@-c*8ftp4q2Bf4~6e?v0_pYsX=w zQU^FxXhZ3Q2r@+eLEfo#sGH>j+EQ-pqO4HVu~LGB!G5Y$R76EQ_HtbMAn2cwPw77i zw%0lW&+Iq@GxB=yhS(W_@Rh%i#W9wXlYQYs;tuTVEJVxQF?c_IH&zID1KaBbfpb6b ztEcOtff>i$w=)DQXYTxVw47LXov$8~oC7)s=3;Ef4IK7ygZZJskb3PiI?zE(T+>fV zrUmoWZwL|R_4|3nt2!WZ*>OBxHia!-z7DqZaNbXYI>>u)jmYNoqvK#3Hz&D5&-F>5 z@%*Vct?vum-?W|B>ltGW$)&ZaMO3W#E3NhVUhVm~mKw_67A$W5Me=8hGHESyK_%H9 zQqtz(;gJ($b>~xDZ?Xf!G#HvLGE97uQ_0r)-5BK%K&LRl^w_fL?De5@IB>@qFB|i$S|D5R2i*^y}g|5N>I}Y*y_c zZ*G>O-mnnSY_?^d?~JD3ZWVxYj33CoI*qel?xMx#25G4Nb==!?jyNQ(r?rKb@qBFt zo&NV64)-obx#bk=t)sYS31Mt*&cXAS#Bs;@uP|wJMPcCkNh%4xYgDs66M-&H|;Y!tASz zRTv_%1)LsnS-JqgC)d(>kNsA_KIafv-o$kxS|>0ADqW;=?lo+E--JJ6o>3ozLgIS; z66x2CgipN!>Ku55-mb4At@{*cv8*}FS^o<2(%ur4gYU55)i=Pr0`#7{f(G_yQi~#A zQkIlVx~|D$()2h8UU7$fHTi&z*ZxAn&ezC$^$2fvU!YkEPw4PUSBTP|4Z)Gi!F*yK zDU`8-HS;Z@aBC6Qt!Sp3)qLQ4Za(CmF27nm75P|Z z#Al){E>hi(9K-pN75wH$L(rid+7!A5s~$MBRzJkR>TL=7uW*6*mlo*kxfB)O89~EO zdnyt*4)S~yxVe-vGu>wp>X%<5KCM4Vg^B<@x6Q|r1~n#NK|0?5b01b8UkBS9HpAt| zQtS~Y9@ADTkCF8{%=l;i(E79iG$$Q^%dUOgGs=bfD<$HjxE%a^#Q|@6_3#xnwo}nN zp|HJe8RnNhhQ^rJ#5R$;=R&j5U}gts1ni~h%~P4m@;T^H{+t%QI)j^3ConvNS^TJ(}H%{h$J*bVskMS?8W^MXqIR@9ig z0v(6jVN!4|eIhJObRJ)|dgObSe|6FoO_upTFQpqE<<@s3z`E%rrv??CmF-A@6DCfoCqxqJL zbmPH$-247H*sbEc*n>UPQ>F>?g7r~;PdeU_4uy$XO6*_Ta105JAoH{>$$szen8z1~ ziZkvgHvST<;<|{-m1oiv0|RiHIge<+ngUm@^uXto0A#NDZS?U2${SXp z^uZW9GFu(r<%ROqcgIoht~zosR)=voBnzu{O@*#;%W#p{WadwN6cu09NDJ>~U~&B+ z^sDx!liR}>$JRe|?~X!nHPB;UiXo|mXSs44jHDB``uGp6+tTS2X)R@X0=Z3#DiY>^x&cn{O^(klfOs;G-H;b zQ`~9v+MG(IW2$hO{}dd5xsZB~TZ7@pRM-c5PBMR6H?d`(B2m*ORp8sJg`!O?v0dwd zb7t>^iaSwoa?KTJO3M-y?+$}-Z8J2!9|kQ4EFgb!A-%fj1S(A`;Co9gz|H(|e7l`t zI5z1UDe9icrgCTh8G57i{?IAZ<8mub4V&qkWkux1wov@@$dY_4m1dvGM#8d5?x1Qi z2ZQaoefTvCXppRcs|R<2!=R%bWS1_eR~N=`WBeytIsZOn1_oFRKbc# zn_zUC1~}+*UBhd~Ks-KIP%q0llke^(3NA9tru+nY#oQNuEjMKUJw8rU&mG5rS}t#6 zlY@7w7C@EwN7OYNg5feZv{e^ip2HJ-^6?yT3XDPdrJ2y{xrCe66`^RSEnEoE>{eTZiPiu^su7@JxSdxS5|6{XE>Z10#SmPnLR7ZTCjG~>8Eg4g z&a2=8&hMV$#OG^~w@Lyv%9NN#@{*8$wiEZH9i`j+dpYLyLaNB}*>Pzf@S>~*jQgm{ zbUuk-3`Wh_L*bKg?f40d@ts;C|1ywVS|Nsa0yWqJS4!aapbgyKzZNZG-q5vi%iwA& z=c9eF4?-sH!9STbw723bU-`gRX1HMnR(@`!@2ysm-KxpFEkUJF(2`6ajLkveIt38! z7KbTq@u2K69&hdag1eS|BX2o>uJVTjJo@h}TE{;IC9hksN0R&2TgznyINnrq!EG8j zrJ1NQB)hHJPHnda!N7Vj>#Io!_^3 zqSPfB)<3!mM*m!(Nuy2hk#7YXt((b4+78C2CQ+?><|MOL1>QezCwc|1iMxC}>CzmA z7&~hMqt;AhR}&bGjR+XSI--%g9ZOEwVXk5%=V+IKGwxr>`9L+O+8@mBDxLs?Z*6ho zoIu>ZAs@b3?!uqZQf$vUarV!z4lXBj0vbKJ`J~rt`YFepOh3ZyO0Bj*{p#an_2xzn z8$M02Yw0+$W0et8YhaFrE*7A+9dP-aD7?Ero%BmpLAc!!G-}E~Z~qMHu6l`&| zj9Us9x0aHVzq8@SU18Qv(GfBgU!&ryPTV_Fn%qCiO-F0DfKtv}cB7ya_OCk(OStQI z-{%RxhaXDKHs+)4-0#p?6_3pgt-J&SIfj0m%*wsKkIy1w$-TT?)ay?wNKF?)vNf7I ztlPr6s@IUTQX__4wjO))OqtECF5;lPOwXKnC2BeLD9M8 zVA*{N({8mx!>eZWudWqnpDm?i&3oeYWC>{;8{zmJ?j*uxGPFsikec=;Xc*rHnH{Fo zdT=Ih`>#ECIqV@}cD0ed4wgn8xWGM=p5craA`lXP51bFk!{4z9pfFj4mHTFn>WXja zR%;Q)-Yo=TK1I=(V>fBRlmp=WXfw?78h|C+PU5tG+k715zF~&lC zXn&&(J`3+-XJ0#>#7ic&9qx0Tx zphy)n{&0 zdEc+ZGRFc;{d5x4940V}VuToRzbNQ__XGN_Nua^$IGpM(kL<+tQ2k~DZp?c`Yqwp% zX&p(}GZjc~TRp7U8HyDlaxk(+gBg7NmWU58fJly+7$j4Nd#&UdM~Ry-Ch`rJ{nTV? z%RZA@bup$hO9mUI!vtLT1BWf$7@3|MI5H(0IuE`l95f32l$^-%=rqCk{US`lnyC!A zF2k|MO3`7F0(Kg@z{xpl*{@|CWP_?E8{XI`X#1LrDJr&nZ*z`CBjJjjF4N(N%wCe0 zAI6UzISmpIQV}PJFm6_#1yL${$$@JVAo%nZs9HY{lxjJLXL~!zxpbO8@lPli8{ee1 z>0Bq1HA`=s9h`E0`_Eb>;S<)SYx&u$i>$l46<~`>i zAhi@4_U}Uxvm?amXB+r%xk1Mlo~V*w1%ZA8G}m}3bN^*8|1O_%;q2pKji8J##^tm- z8lvgiCj=Ah&R|o>Dm-02o=NUY!6Lt4IPaU&;ADcu&4Zyv?tU!NK7ER zz=m0}Cl1eSQzEsw`nc`cd7?F2os|)lVx%@d1l7;&s3r7a;Ng9#Jd3np5NbG`9mB{}F$KnpNI+frKd5u(*wx1uvZk&o9B((4b0)-+BI9eg<`qLUx%2neOWb@o z^*qXOv*q}Kb!dFq0mhDYlD^ZovGSq^G)^*RCEp3Ldu7Hm33dtC7xe}Yo#lAY;U;AN zjRtCPipwP$hd}GNTx|a+4-b!x@@5ffjEpnGk`POz;X+Jj-x)YMvzq3wC?XZ&Is#C7MhsXL zlu#SVGgSP)TKqQSAZ*p$P5#i8C}n5O^s7x~hSX%iTRa(L{@vlv(5pm^>KEKM(RgON zmI34jx^gbAF1p0992RqZOqrr07@m9@MlNc@nS!q<$L%!=jJU1#}DZd7b97NdI>=vr}yOgGUngNMc zt)zPq4}ZA~U~GCHm=x*Lz(pJ*xD|2#ROC2ASI}jq34LJA2j>P~{_fq=nU=O4;H+?& z*zP}Hy-mYi5YKg!BZG`k#qTgvJh6qI-_3bdyiZ~NeslPNk03PmD(xK%rhdo5&?|U8 z9# z@tk8+lFj$K0Sz;b&}y6Wg03SqaQ3hqxJr8qGbXWqQ_o=T z=xh`#KM4_HZm?D|9fD;_iK?s#R26wp`N~UV=go_BsO*No;9eOra;8|@F$%w)4x#sP zN!BbN8W&d{LCqIC;mB&vMdI_G?)<7nmQPEeQ!ni0zuY0iCUW29+XPdHon8W{H@l;A zf)n;^G=!V)tKdV%I_g+$MwfrA;JO)7jI@Ot6X79&=QKOPf0{VU&7-lnK?~qlEW7zD~XU>hve=)~$Nb8&snda_9NECzom zq3@4IayjWq=%2O``>qKiz5Nzi`_=d}lz&5|a{$)-4Wi%EFMw^A9LFlGgTg&e1zlG8 z*fdL@*&X15>zpzKW2fqHy@elMeRC0X|2c8Yo)7%50zLj5xenTElaF$ZsUW^phmjuG z2`<)`Fo?Mfdl%%x%f&(vo|gxGN*6#|RSL7Z6j-6eFZAHZJlcGGGnkzZgSH1%klVQxF&j30;})IV^gd3%Dn-PE#|lB6L5;xupC>fJZu^;P4z6 z|MMi~|CMAi&HO=RHIKi=Lmzf&-^Gzk1d;o)VCAU?Vq8yGT-U^9p43(i@7Z5Ws9F_*ia`|8%GghmEb3#VU zV)FJG;L^Ga0_R6%H1XvWrnW8tKkeYtu`ds4>hCb}XYNPtY|cZ2@0NmDx#i%bH5E?g z26KGeWw6=c5hge<#y!$!;CJZ3z$mNJy3kXZ_}zf1Sbrb8p6;M47w>|D-+XY@ zwi0Z&%|^AQ@yM$<4}aA=K9CH-Dl_b+)hZ(Tw?iwrz z5~+^bmxV*!77#T=FwAcWd|4TXjPq4odnJV3I(iWbY(=1d$Q&#byHT>-l=a<~2ZtoG zFf;Q>^~d@vFyfj(BJ{#(@z!aq_TxYF;+YF{cl%;+&z%7sd-{1nmYO^hY6KlQUopUA zJToEXh@j?lIhm^M0qePp(tKSm)3RwcC|+x>j+w}(l845z{(1+|Pj@0d%dDn3xvJo} zDi*XP!!Y&HYT}T0lom#k^X<>jO>xQBmK>J_OP=mw8Nli{lP08AG@B?y?Q0dwvQ6E`A-TfRQwMHo5X-{A@?;hwpL)Iqzn;8xw!S+DK5jO3HQoJVch4_IQ%;g zen?glZ4o=1dddjuUrBK}F(J0^=~S@Y`H*8KekN?vD-5hS2C9N3q&G>MxqPrwaFSzP zBy06RY130`+Bq4OoOReOopa&nG!53E{WpkB>%#RXj?t|rCNb6lC-80>*B8#2MruCU zkk9oWxY@QEF+CduG{c67$F{+_FfK3bVh$C;3>%#Bo*KEI#a(aD;Ocqet?f>b>jJ|leBV<9rg+z#;%QrG2oVpAXKUh;x2GL zn&kD>$-R%L<(C2UQ5vmko&AQC4DW}mfam0vygGE9okQBpOnBGhr;_$}^6Xt!gbprm zq;oB`XlKGKLF&p9kaQX*{>7F+zF);rpXKb(^Z6ioMU`p2OTecmmHK5*$I`x1n$$5L z4~BAmf$@&SO+}gX4oqiT1eC96XN$bi0hHN15toXqz~-`v?5uZfxbIR7sq8f%cOGZ+ z<$rYGz*WwdHv2JjnJxzpVPV>FZVRIroP^syloD;*uf(_MHl(GOQb(z9QkFIWvx3gy zEEvz!ojyo5t?B>iauUNbi@Ax?L1Rt5?(@c z%^-h`7T5FjbpzGL4Y>J*F1zvfV$65_fzB00VAaa;Rqrapl^3p1Z6nQ&<;XKjhPk=O z!wVQ+G=t=s1g;Gy;mPbG5;;i#dbh?g0gO6E zTHc`R2Tnok;B*@K&zDGEGl07u-gHI9KkB>Fj!m`W-o>Bm@XZW2Vz{r8V>@_3>=hkM zn|qV~kD@bi$Lj0Cuw=+A6e1x*Qb>e%uhS?cB}pMesZ<(hP%1;lRAd%1q%wp^D7)0;k~Z&p0oF}o^{`UXU-&E11BJp9j7|Q^V!LDvg~FVN%q6z zJD4$Whf1rIb6zefc6n$TlpS9L#;>-~fcrOS*d`I0tQk-A3^c$hO_1vEzYU)Mb(1%1 zk3dP)WOh#2Ij~!~gSA=4akpNc1M_$u`!PKb@5(EKXsjIfeVB~>lS=5DgLYWuR|m;6 z>##A+7WQ*9k}mEXt`fYFRP47Rj+q)v?xFk!4>;j_`ek*p(OkRtQzX1>4{IO7am%QGcJX2UD8ZgW}JZan{7dT;~xC!7>jk% z`izy1Gl-4{qxZlb#OzmC^q>ZvF0I2uzVESR{}uWqzywTK8_y04yB%k>~ga$*r<4&#&+w1 z{J}!FsFr6cwOblwkI!ZW{2znDgB&okF2IBDYw4>p74R2A073b6%0ZG;&WAG_`)fdaLgcnrBVUzPU)4 zhK)FU;UjeT0orbyN6qVwK~Clb=GJtM$36I(`27}S_lWYCm$6FBS&e9-vDS?H4}T`2 z9B-#Vhnp`vyns`63h0WiEEJra0B5g6@_t9mWCJrpnH5id@jX@4sF*`8(P$b6enUEa zoIaCD-1C5RxH>?N;2hGSw-E-1uA<4gxvcgfO|~+ypIQfW(O+sI@MxYGEbntPe<5tc z9+R%eW1^|3=3D@wY(CAD38%TzE!el+ir;x4nzSlUU^J^s`K?deiI0*b+9WoC`kuh* zoiiUG(|eP&yt#oHXWrn7egOIJ?~}^ zs$$W&_!q5r`2%K6Ua+rx8iS+a%{#odgl*ehxS~-;W-VoA^8$|Dkqbk zqs1`fnn6ot{=jTyS@5cTO0O0?<@Q#BsnVe>Xq+sM&kB0!4(?s_1>~72++Fa+p>LSd zH;euHD~?K>It8OK?O?dE4t+*9U}2~plfK>#UmX=kr)51z~06ntsy0&`Zl$A=cU zG@<2S8NWN!gb^J-!qlk#CVC2Lu;bl5vQQxk!$r5TZ^_FlQ+<1El_?_XV`rE=i*G1; zK9ZM}G>hq#9O2iexYGVWQ?^Ik6r2af>1q0oZ@+YsWj27>GO?I_RUb6Ztl+w@ z;;hXK6HFR%BQWm))t@r~{~pz0V|G-K3HTEtI=>UKS5k}}zY({;KZ&Ech~L_G@ET+V zQMqa*ygjgx&*;dZ+fFss;GHz_Ybb_-qf?j#wqZn|_!X6?T?x%AyQ+Qf88e?Pe#8BF z&+*~md(<|r7j_8j$E{Zu#{GTLC=GUd|e>i~~k zl1%%)X`>L=39Az}WkdA8gIl31=(?tI`NK8rO3@ddd|ijO}VehnjD+u4yr;?rTA@ZF6D%p3UTzy%O$M_oQcDvpkLM zui(##UIf<`Ja|N$31;Qk%JuUZ_o{OcuRaqiAILB+i7Gfdy^1b=+XU%*e}cr}2Qbx_ zW3q{?#z(jALzSm(m28o4j|I z=Af=Y2DUXlq5*d~hUcn6TCrdaD1QmYZ7aPvzuF}jIA2cpDt1t_wiq4yQ?NkWMoP<((}x$e5I z2hfn;J+Ogn1=~O8xX*bUW^FEp&jU6zcfBKBb<+*k^l(hZr6Ob`>miAZn!+9(HHAr$ zVmP;A1-?6SgBO<+2`>ZQ5s%gOxTa7LDzsm4yp4CfHNh;G2U;=)Q%_*`=m=f>QxTS4 zPvfnf5`)%~AtcFa0+;81f}|R07(Buftv8pc{ylM!KmQ){ztv!u2ItcH8Vq!^C?g%r zu_|M%@T8g!V|vRT6hrf|D%+Q)M~qUPlfukdo+9t4R3$|EN;3D>IZ;QC63%C~9!Y@# z{#Y`fRKO(0&w3F%wWkQh0vTLdmPrj>&H>Tr1qA(%g0KEre6&1*Jo~%?Qj|k*>%#={ zbMbVh=+s-tRsFy#>ntQ8^&+rhKnfyv)sTR{vnyHmmt?(Q4wr`#XlYXCPti!``UlO> zV68{|E{am4D;4zb^L5Z%as@@ApOLjU0G^uNP)7*1Y1`e4Rx6ZwylJ+RM=SU;zZ!ayxB1K4M(NLFj($ z4zn(5(*D)&$&~sqn7U#%K3y&c0+U2p@#;lreYz1`_|uSlY{78l3M#pOkpHox9qZo@ z5Qp=+cxAap^-FF~9@Y1umDh4cf88ubsD}^bO(}GApb|V>F5ovUK0P#Y0o#_yGKUYC zL*7r0jcz;#pDl`j!kL8_7q5w8U*7}WSc8xDTU&IVPNFBBol$XZl|{SuQ+Skmp7e_q zyNm%+Fl$KcJcrH-@Q@w;p^P43=} zk*Byh|7G8ife0f#0%wq=F|{Oi<%R82K{c(Miol*X;0QNcF#ohlN;hgq$w9byfs&H-xX4Yl$ zSLPT@l8fRzkAY;9oD>L52qDe6(bTrq6_kZdup{pQ)@=}hM?r#&@Zdi(_VqAm7$=ZB zrccqyc`>ZmEyAj<%)pe!G`v4OjwsCL<~v~;tdCU#Ji4|OXXYhve4CjN;4}!$>vh;O zL$5#~IT?>C51`4PQ1X4jWUSCjg_0vWaUUnrtF(^uvzW5T2o?oa$t+08yD2UcH zK+I-ye)_6ebn%P<^56c6q~JuS7m}j)1vYO#2d4h-=_9d? zU?QW!7@`u}pDhkqe^eRu)9>lQtizZxyB{}cUjWA-OY}1dK>yk9B{}L4(rZ+28d53mCA`d<0FvqIZC?i{sWooauXEzn+JPL&51XXU| zX&IzncH_FFb3o(5f9T2WHl05Hov)hOM+y_Bv+6sga7j`;k=5ax%SMS@Hm-)zx*h1{ zsEPWvmB3l=dG8KA0M)%nQ_lazqeTS8&wK&N5g&S=R{~=54$#=naQajw7*|^!0Lx$X z^qy8K|C?kP-}q_>xi2xDmcAK0qz=8Pjk2ftSO6 z%y$%J^P4%|$%YjEJ-aay)Ru$ku`b~FtAH9ljHdZ-O~G-MIkP9Nk6IQs(3svi)R^1% z`QuxSs*SlY-|ZsD8=pqvB40vA%P*oestL~%WidWS6vvO>^ z@lz;6Yrzp1lTu^fPHIDm*OAb8q!BW*KT>};LM!w$hyZ+p`k{LCG?rwi3v#n(d(Q3R zW(}4_ab}edD&bvEIbCgXk^J~NkEy)-5GHaivyG>XDoZDyp^3e_@S$oF4riT&A2q(1 z=pzc=8{1%inJB}0Q}Sm=HyJ5l`OhLZQqd*B@bkA3*p=Re-Hx8bx=|gs&sW9oiIY&E z@gyoonZsb~dc@hE`E0xy@kecVbKHy+tc!t6ofepGq)LOc=R$jJ4(?gAm-#bS5jI;T zppxz~TqG4wZV1^zZ0U1cVtIrlTs=ebrYmxtmd_vjrDndH5a>m?^@3xId8%l@Fzd#)qMVY{Tw__Fy*12gUAIqBu1V{?Z#X zr1vo_h{?gY=5ee_-bLHrTWG)dy?XUZSL}K@l{fYIWVY101x0sx;jQLQ zqMhRikCM52x%Dls$JGw$X|8l~cPidnQ~>>7rQr6WXb{!C1W_qxNo3^(SRQM_YV6j6 z39qfm;%gyP@!2bSf9o7}Rm^`-vv4tVS|#yHd%K9^^MR^FsY4L+{XhO}JBAV8r^y>=4l8piqU{&AhB?=<#$G+(iO2H4KKVxF|GfQZi9NE&s{;he2s z_=bPGz+mJ%Pd7+`QP;}hT-mA^TlxnlaQ&~x&&8l*)){K^<*UWrBvr<3UMbk-$gv); zqo8EtQ8ZZajj#ra;8FgGuO8^g`8$Q#%IBNGN#GG_eKmsA{p36tzeg*MF*-}VE<=XNS@FD{%?-KZ0iz8wN9Y{tCmBE zz%UMUMdAM4(-?i1>1^xo5~{DsLHGFcE?iTD!7UM(7nh7};>zq3 zol#)QR)XrOLF{N+&0M`K2xtCGU|%oGq93--;QBOvB%`(ueQ#KTz)&1wgHBUnLup9WPO{OVQRt$UVBaF|#j0tyck&_j1 z;AWx;oEEk>HH+N44s-<5=7qYT=VV!*tZ`p54ImK|4UUWi9(`X* z1gAuSgm@TSs$IM+f{rPLVCl66*B3RCn8IRuctZ>nY;YyRkEWorqc!M06~L5;qog8m8O)g}0=qX% zV)PW^KzRNI*cTB7e(P3Jjnl7jzuGb~PYKSR;q7&_ zf0#x*Gggxs3CZ9!VHdOGdm7CRz^VW zusGcQ>5opwHxO01)!e&~3=@{nPwot=U zL$u!ChFVw(vX8_!lh1*!^w9(2nccf>4=vhs4b$%4C3mN#ad&_Vus!rX#(897@?JBHa}J??*%MfI-#K{8@Mm?; zS#A$kcrE9*KTl12pC`i2Oy!?}Kqsn@SO%vr0AIeQbJI;<8yej6rJ_vb?@JwqKsLgDAH zFw|Qq0#O_%qVVF~>T4T&=~0s!FAq}8#|$qcal51a*ntxX-4JE z2y|3$2J3mQ5O0)Dk8v4WzxV|rwMmNc`1lfS1f!^L)&MD2Ndo)a4zMm(CSEUX(W$+J zXSCP~`py|K?g^2wMb?l!&*j+EqL;xpLz%UHRRB)@;so0zbTFVrFg+{WD~N zC&l`yzHyFP?6&vg(vBmpWGrecu;qaj|C~-X!6Whl@ z;&>F?kN!?Kc%?#l#&X`B_$VAv-$&B~`qAKE5uR&`quN4?@lq(~auD0Y9J(9A^VTn= z*W_Yo)}CPuv;Ik^$V8(}P%?^-l#n8ZZlKMfUih1Fi3BIc;8%|C(BKvW@`CNOJ83I+J`d++_G^jNTuHEs%e4qR z{e`!9>MO9RIK}Nb%z*DfSNPQ{eE3}!Tu-CmUiG1yMPQL623o#a)%*2jna>#ttg1mZ zFV1@}J6pmX9*$i=f2+$tet!qC;af0PbOe6<`2^cyqfvd}8Wxm~V2I@>b0yZYdYgL) zWQnE|7uT(*CGY^GpXoA^FW=*&*HO5A5r?%3she;jrn;ej)*?i;=F5}p!2IA zM`f?Vj?sra|0$bb8yUpEzJD-pZUgS2-xX?__6OX)aQ`8l#rzvu7l?b(0VoK%Oe$?pQ78F4@U=XP#WKa!X0vt@ zXCppNKYAMthTf5mi~CT0OB<3UQsh?GXVMiQ#NKzGhGsP~tWlRCBY$KbYuG=Nhl*kt zbx?&7s=fptFTR1s8QpNd-Iix@b~)T1T!cvv--B>(GVYk(Mn8TUpq7tj!@-QVWd6l2 zh>bXlYQ9gKjdD7LR~|qCH%=8cUUDZ-!7~dFGnr3Rc!6pH`<&gTq-X zaChci@+-fS7A`elgbqKZ&7Y?;a`rQrC-HkAZ+JSJaw&#Ht?VV)6$(%tql0o++p$~7 zmv>Ana)whh5zNfw{1_2*mNuE+6_1~epi>OMLnR-_Y`TG&J&(FpG(wK626M4bgY9jYL1i>ushIyJ z_`IG6)!eyGe_t`aeKvtH{4-Ac<^{plkFTM!Q4aLhE`WzGooLP8dThKg$WIGQtoWpg%PcH>oy?FH5 zE=)G$R?|#hXUv&ph6VAdu=mRWeDE&}L+?E1eR-nDv7oX@x28LlSrSH$&71&J$b4q| z39heqau)XHm@$rbH&eCtZ}1^CkZe7^0Sp5<=ijyu5V%Z)wf!VQJ$3DIx}pb8QAZNPfVWE+3k+F)(^nB-40JfQnmfg*C;3w zk1v)UKtF*|eB%5A(rYXE;(BX1pIa;5`QA+<#?@$n})Z72=TGRMdq;WuFO^*rSB#9>`k z3Rr4uFsJ#C`Dw+GDEaXhZdiK{Qlf9rmo1*?(kX)x)|EKCT$~*}DZ^&Vh=c5_>3A+n z8cPSy;_C3-n3WTTfm`GlX5k!A3fx6E4`!finJ1zcYS;b$jZi2Qbe zDl}G-U*A{2rRJH?o0@{Q9M92f`mgGaX}x^^b^17WZ7lfxmLcG40Y@UlAkV=UmUpc| z{d=#_wX}e0u#=c}*=W2q(Vf&@m0>(1RUqA29o{H>qMI`#@zB(NP#(AhpWaeIoxhWz zMw{aDf0kIi)sY#RuZW_Q^WypL;k^)A1Id2E3np=lTI-d@D1JbUZ7O*Wo#cqc@V7M_ z=l?ROEH^-lS!cmyhCQ%;W~{xwILAipf$T?|LwIZo`*L5Og=xuZG&}N#zlihrIbzZNVww*Xuuq_LgW5S^K!`11WP zSk-V4R(&+0?Xz1T;Nt_Hq2nj;=dUN_4Rz2@FVZPo*CHj|5^U~|;Jyncq_y4yY>)k; zJ2ee)uTDFa*pml2Ya2jCpW_96=kkoE+xTZdp2N(EVA8ZvNUC~o;c@Re*tITT+vER2 zmgZ5kTyBReE@y)A?JRgR_L`_&&BCF9BQ*JK9cmh_L*?EYLb}wkE^Y@nZC8P4kxtmj zP9$=A8pPdnCI(1PMjy3;a6S?R8@47-Ou+B z%N`f#m3{~F2h^b{nd2)gKLSlTQmlo_7Zlv8iNS^G+xa6XdD z%C|$;wW**}$T1pJg)m>G8)9$Hg6Z8V>?PGOa9#3$M~o#ErpHia?h4xZcnD0+XJDy{ z3xs4}!Taw5dDBDlV9EG3?7b*}bxZ#Pon^sbImZl2SNPLKMHk??dm8Z%SP35j44C1) z&#-14=OA~#h2*g;((VXG_lg-i?6Djjd^W=RV>wvsoP{w>!MLZOXu++3^Ca$mH2$qL zd>)pALx(u?sO}WbNKD7f_6kB>#E9~iU$}p^BAgWD<{_qXZ1Lk8 z9Ovc$y&WsXj1?hBi4_1_QIG#_m_w%9)anTa`FyveEb0(+l~nxorrs5k;igA1N*i7# z{RV>Qw%CfST$zX}e~+T!d|e!B(`Hn~40%yu%8W1Z3d$sp4y1Cd7);3vqz{v&+YvgIq!rhJstbGyhd@9lx6@$0-y?)l!% z=a_LD{aAn1N^nhsuCK@Xp!Y<4DO`WAv@_Y{EW94!B9 z!kloQgjF0JRnw~T)I+tRRKRFEo15QEzTyZqxE;{0huIPF4 zJdF$$APvl1a@uz))^U8@=Zn+PFx?cdanIe`k19wzl2PeqA0!!wz%#2pv|r#4(~|_5 zPrriMxJxPE{`3Z1mIz`UDtU0mqy=_%ND^d_@^RpCNJHfFO>na6jA(G z6aV6D186q;$8)vi=Uia&V_LDxLVQJFV1WWz-Ks&E!%It^*rG-0&( zA_n0juj$Kka+KqJl9M_=(dS$>|LxyfsP}IN{CSXw;RZ6iWIf7{X2L}I2jgxKZ8q_wZqP> zZDix!3E*d}j|UqHv0L{l8VvN%Ng1b6uP6xvER*@IZ`!f^)gw@SzZv50`I4K{rn7T) zUgptR-9&gPH~R`G#(Od?xYgDLo-n6KsZtbPZ=Hsti$36;fM(iPcnU;5oATf7F~;ex z4Y<(jHKt43g8nA~mgVb%D#scf-1iGx$LdMFlsMCK!w2evuh6DUPni3Hv2e&~Bok&O zL3Y?JEPv5xVYxzvIrw4;^mR;SDy1(Hl?DnLr(7W@N1xmekDwziCRoThV6BSJ;aAH} z;@x78G}nnXEuI2BQiWJ7eFLyyC+rJI1niMzyC)7n)cZSVZ68dZZxvyZ_f>G5vwM8U zPbTbMZXeHLo-kvdC&W0^$Yc8ETs+pq(t~B$_+#P*JUZhI?HK3=vzuJb`Fa@bU&fOP zX$I8Q=Ro$8mqbKCno03*s}>U0#GdhV7_}5Y^&>(czqS!NTRiB_`;u(nr%F6?G>+e6 zYfGx8zlA{F4G42ogY6tMB;nQ-kmB+om!efzATW_>@DK(y?p(X0>K$CXlTE*=8PJMt zQUtg8<7WO8B*$bx^>-p|`5O&puV>*4{VLdMlZ>iIe8^B%CGlG-NUZj9yv%QVvG{ca zZX{ecbKfL}>0Hg1>)l9ah8&=lW#Is(Wq8g`pU2({$3LDmXx97!`!XBJxfX4#XwYP= zKF!2m%Wje!p(~i#C53FCIyw00KS-|d!XUec@LT3TqJ2w)HQN(YB5QF`2mne;^nl5){b;shx;_htrBnCy=G zdZTbfj|lr|Ul!4}m4G()ExhDxFOZQ`U>&ptm?!CCs6TfHs>gC3@$g~#x$F!E3eRR; zgcKQ}Wx>>8e;{@#KIa*?HlgO?T{!e@IY}DXMpZ%&5j)j=Ttn0iVsKTvq6dia$-zYKtD6YE9r3 zub;`2E*I$YC*)j&%>5aTDTnOpj!t5Sc#BWS}1H_r@u5Zi1y~cJ>IlBymT3m?vG$m%S zTt22d-y!xJ62V(?oIf__EPb|8jvYQQn^oqxlSM*8YzprK99uOP2IQ0&%LrAvW*3)J z8Y{6oe6@&bjTZak-YRyzYzWbpd(IQ76bBUrdzc>ijrXuzpK-HM;`Vv`h)mT%dMmgG z)YA;O|B5QQJ$MK~GcM3eOZ}mw#)=oInt?milkl^JEBt#e!HkINqIdi{{MBiMq8e3H z)V>%!=Hx@fQV;msFHiqiO0i2;9*4ha|3Ux1Gt~1)1m|wQ2}f<+(KEjZ5AWTEiram_ z@>C<1sC^~cyNmeO!nMF)?HkB^w}XmWdGcR`o#C%>p1?S4j77P9H!;(6DtmB(I&(7r zH;UFUtn&+V`rktdref1g{E+BF0(KZ;pUGSlohrcUuWzKEb*spw7q<8@`ZsR>J^%-p zWZolzb~3hQBCAwrgf=G^Gmrnxpm~FO(61|v>fGGGY}}Be0O#ADl?arYt9KuawYFJN3!D`ctIip*-jcwZd4SD~6`25Wx*4 z(C^v;DaX`7u4gq0$;QEW0`XpUIQxEgxSS88=>(x9|Yr0VNPy0%7$<~-?uA4 zL&THx_bG$m?o)KbY95V!X#$tseq-^~<@hyY9;vm{puI6C&2ug4`OB)tP%0xH%5x|< z8>P_|;yW$mybs{whY{Qkxhv^Y7=eA8&!YSwOa2`igYr4AL2>v@^(l@aAh3Ti+tGOs zGycq@O7VRDg2%Jas%Z}qJvSG(UU&*C^3S5pmpJ&b-4X><(@<6;m@W;^g%Ot=Si5=^ zT@^c@#y)7JgSWYNnqCHIZ`^?SNBY1yC<%(wF92@|_y3lyqz!-CpgQ+4&(>xuru*5# z^nw&BxJ%po>8EsD&-|`Fc6K7{yuBR_2M$5xfko^!t2$oRwi14pumH0mTZGLC)4@2^t6Zulm4uZzx>}~vR_M_)o0RqfwMzVPTCr>&BWM?R$uw^pS!_w*LYs}rZbqP z5RI+qpwUg?fe!3VKRQu#@~;I$_C7_Z7y_*_cIJqeF|BB2l2%B(_j~- zON(Y0;xjQDY~wW%m33n1wCD~-+i%7*+pO3+?)%Oq5xibSRkYJu&d#b00iNho`t8JQ z>=WVqiyGCmBX=1(B+Nk@^HxBeulP+N4*%TJV*1a@v8sOqc>$X$d39+YG2!Azj8c+h z6U4kRLEj7YUan;B^ihm+&jj}O70}B_h3)xe#9PvaEY6z`S5K%TK4HQA_HI}+#N_ZBV$G^FABeGreuoFuZ0nmCHDRTi);I(}k zKuNZ%M3Av*X-32E^6Z-2x!CW+F}wUCc}_E%vA0Qvaewfg$}PJI!gjwQC|Q@uG!SR5 zMb(hAM1Zx~W{%yKCXBXVBUyFfJtl9uK|pLLwC}8jYhh&|@^voT^hXiI7nY;(q)TA- z-T*gj4d;=`b>MY41O-aD|9{v$`qlX>eX=Ws`c}+@2cI0k=jvK0=vs+eb7wI{J!@%5 zpeda{9E;lHTGV$+A+{%P<<(oxW`g2PQS8Slp69cxpy-threW`}YwI=QwndILdQm{X zX>dCtqfhvz6#-CaKTI1wMJ(7li)=`qh$fpWQN|~myGtqJzZI(V*zdV`*zztN zdEG{AIKHiX@H|G*E{VU9%Q}~q-lmyt_aS<#E%R=^6r}7v53_cirJme5WKT&V^k4Z? z^-d? z;OKM)lb*C-#?~LSEaX4DFO~#zmIh&UV+=l45NE$1JO>s^C;5)iJ~-QFH(EUoMV-Av zG_w2#*Mm=|cOnNtBl;)N9*yKWQWvO;up8X`JDHUWHHW$yV@%cEK?CCMlI5W;G^%tV z`X1U1I%et^xi=ib7bS8WjQd>Iy9`z|?ZVE3BCN%vI5ZVbL^eJiR^1JuyWF^Ux59Hc zs=^Yr`y6X5q>IKqybe;A7`DN*gq!y$;Kas6sMEWJa?4}Uy_{naTt3hJUO7DUss|HZ z&0}s=AH$U1qvVxcINm;-ihdUVL1q-^-|)64OviDUZ>oiHQ@z-e+j7zH`8dZF5d`!0 zU$k2O7N*?LqIRq5dH1>beOGE0DlV)-i;GjRN|?<&NfFG6wGxC#cBj>DO< z5q?*|G$uVw4j#DwfF@xtT)+MjI`l~}o)ST@A0(;pxGFxej(}U-bLhKa0X66mW>ehf zL4d;tTK_s2!jS=syIyz^bLhmpK}`6l3~Qn<ZH15V=b$=_ewO1eP3+_; zzq=2pr_VYb9i%$Zg1m%hCcJ;EDb`Q^1BXhDP;N>nitl;?gOmGceP$U?2Su2rrg4C~ zJW0WyI^tD29ktu9-)t+KhaL}>v+q>KQEMy(_ZM`NOyxPaB-9-m;QQM48%-=7M34MbYx_dIXJkSN7JPD?yuLxQ*+Gs1e4~nHpP+@)sb*pR`@ewm( zZ>Iuw13dKBG7u=C}4gSmyg5ih0uqAIZNZFO4#LJ1~^wT)n_QH|=8u!MB zzfGx(QyRKX=&xSd{Pe z3tkI4)@QM0D3$;0TQ#MM`LMcc94)h6W2~7T(~^CW+%Yg^`66LdU-S+%z8a&--=Ba` zUK8E=^b~EoE6Au@oQGqjf~>RccL;g*hZ<(o@?D4Yn7_d&@I5%~7)msc zb9yqkibi%@VDXha9I`)+qMg^Vbnh>2UqBLknsu;E-wRr+8+dVI)-cza2laOzp!(cI z{9{%}vrb%rAF~z{`_gb?Tw}=A-&5n4Hcw$i%_Om}C5WH5ekBQc@RIYJ&1M6K7{)H~ zHl7w*z>e?9;n83X?wjxk&TR9=ydy3ovB->b1yu3xmQO{EFS+2$_V9`y#*oqp8tjMZ zRn)6y0uw50N+U;P7{!grbf^C({JsIfguAo#`EZ?AryD%mqPwK(!%Aj)_*uv{Q6=9s z)YxkW7C>)`2kA|Zpyq2T=#L#s@WaEGq&?0XMN>7I57(2y!1e(?Yn#QqyikfV9B<;! zmu*aREH-{Lgp0p?VYp}=`u)wUjy$1*UIzP+8l9mI$9KVaz8VLWwQ>;DPCf8%Ul{IyvdV zqG-1b+3TYRt6hlxs;?Xyc?beCD#^~44A9pzV@z&4Gf%Yz$iety_`+!-v*BbjmU4_{ zl+?vL$LqmSbh%qW^zzP974JQGdQ5>y(6*qOK0=IQYcA&m+KK{v&T+EKn)%hb z69re!LHFUEDD>3>Px9)`z0IevzB-xoUEd(xk#Ll!l$%HeAK&5fnCEoyq92f(TmTFG zwKo<`NR&T8!fh!%%GGC3Q~e$9>sGq%Cih7KCJh zO*gmiSLp#pof?>bF#tC`m`P(DCxe1S5&r;p{`?B!JO|n7jDF+^lpS8kyf|KtAq8)6 z$xc2AU$dId>J$OdiG4(6h61R4d(3Yay=KwH_2Sga@}b`ACcW+Uonu@%LTD40(MyG( z{Hv{a{%r#1EF$FhSTvek(q$-KWCMI<$3u)Len3%#K@zAOSSS%1c!k*#o)>zPhB8Y1`K9Y?+T8MOM- zcKl4{;<3(d9Gsd%`jT^KoIxQG5KiSfgU@O6m?-?!62xA;$z;``zoc>!C39A-<9TtO zS-lW%G%e9&{9jMKEX zV|?#e1>!B0gD)CmF=|wcs%RWWPhTO@dZhydM5dF|wPQ3ZrUbL^y~dg@g15MRwzHZc zC>HsauGoAI{97Y1M01e5`^UgO`+T%NH67R~4`}nub-dk<b85ZE$g#`&{PVZ)Pl*p3nK zH+B>A(jx-4%Ay*Et$2E`o0*%$ZZwlZb@&04mPSgZu7T zIQ_CVCGTa>^HKn;)mn(rf7a6CNBMAX<~CRq7J{3vXpqICx6o`^1%Jgc7YN8Y0h@~w z=zj7V>Q7BzhuHZLYdZ;;27eqC+Jb4V9el@NQC4i-7D=il z_$f`ASrVXyp~qIjjkd`ot!M{MtcfFX4a$rNqsm)*fjdu1cHo56`=F<#Pw&)lyt^Ca z=$u@DG1bMeA@dV8;FIb>y zlmhdwOoEA%jN=C?HPgQfB+zQED;Q7sKxOQcackBb(t6m2^v&g0*SkmJ*B75j;O#2< zR7Zx5-0%rL+)6}^SNF-902OX$G7YwjIS^%)9<(jl#&qi$Gf8d>P92QMLRjPOuHdkLM&<7qRp3*XS4S zTOJO51)r(@o{L08c|MvR^MoX!cjW%uNn}U20(yPnW@3`zG`3C*)vSMUY@#fjKy&Fk zp(x(y!&~^RpbB$$IKh(EzaXev3rEkeWSiy>SdnMLlng9`0FMsTx7vqsUFkHgT#1O7 z?j|pUXK-gMRpwuB9et`O$G%Z(vq2WFSHE{>6+Cir=?q~QtG z8++);=fjZnX9J|^UB@)_A=Hho21|`F%$X9;E3&d7;Q=;a|IiwRY$aK#79qatR1v0E zLX!2XIY@NfLy7yU99Zgd7(>;*!GYQ|l6*0O%WI-w^X&8Je|nrsEe*#c$%P=VzK$F_ zcMav_-;y_?^{onu5kKDP;^VE6Xj}~_jzp{|+D6Zn|nQDU% zlRL>4?*l}tFBz@6T8WkMYkJZtm99~!!{O>%a4c>X>2OnFPp68pVtp@3x05jw_;eLd zzU(g6)#hQ*j&R7xtiT&*t6*vTLvWtNariqYVyC-1_z9i?;docf>8?U~6@T9Rm)0z+ zm`_%CoWX*eI(YEN6VgoEP+u%y=h$`nN*k;;%1MZ(?dG^i9Zq*RK2 zjVcW)3XMpF3>iX(BoQ)%!rkjg#xx*N3WZWhN+^W-&i5}|*WG)s^}f#&nuvSma}1&D z1dJ|*(8qhjiaSDk^Dd88%@c!4RU%rYt{NG26e)pGO(NTyNUi%rHF-y$9n`;z55^tQ~ zJIM2TSq6ET4fNF3sc5LM1qFRG$@vwrbo*Uv^6=jza7^5XxkVE$`ZCekW9Y;@! z@KpOYeB8Pc>T}28)0GDP@V8<92=5XEEsj5k@ACC~h1iDexu~&Ph20b8 zL9aa(W$s5vL2q6ZOuP4kDlW2z29{FcT5r%el0X_(Z-O~BR&?C&J@>8XH zs>1C)RxHO$KL5xsvk){Nw*s}uEYfBE9ftTijD_nJI{DQXbi8|O=9(dc8D%Iz6(mj zbePAxr?BD!A0fT#0qkoOWV>IBv9muIkz;GR(eRNdyXU|`+@<`eSk*rV2mXmNuL>_> z{oYAzH^(UQ`s$09Ui?A#YGKxH+fVZ3Pa2i+nZUMd0tybPP)E^2*g4RG>3Vmlp1^s~ z`IAN5*IJP2D&p|Te4HL@Ig6#?2Bd#L4+S=V#1U0nXtr3$3UdBYZM!Uf!^X*M%7b0_ z^q?FgR$0nBT>Hz2eG&uFs#m~KY7#sC+zoE0t>-)>+u+u@f28TbJvj5x8mf}5N$OZ( z@w?2(;w@%7JcZU)znNscDOt>+Rtsr=v=)mEUfX&Dpx+Zh{n2r!}2 zs?>P*Y0Oz60TGSU`4bJ~*iWq`5OVwz`nmq%e{E|;r^4ODewrjJd?kb|xcdS!+&^M* z=v+82@(*7|$a2aS1FmD?OmeRKV(R@HC=|Z{l%xcR+>XZ>>i7ZY`w0=Vo*(p>U$^n0 z?gFq_!?6|Icj2S5VvJzS?pB%sF?=XMuy_Yoes5m=8 zaV`m2ypLgqm%$$<2s>P>Nnx8e*y-_#8`d}A*RT{_jwZ+KSN@aGA8s~^f=a}Z# zqq+I*PKcf{nXP(%5QbX4X_?7hv`KS7Xun4{oSMn@-*loiJ;IEQ^dX@A7VP|YO+@QKoDhGM# zNw{TIK3(?xJ^1Nc;-c>vg@31+60@8rSUYeX&dn=_C7Y^=SY-!TiDi)VW)?F;=QH2) z&+!FU*+On8H*0_Hgtyn$z@}60(BI$(+=-LI!?(Ep70sa1VwE&>?qm9-ZX2vnw8Yrm z56Bvqc(jRnh!dyTQTs<67vRe*#_)U>`UF{!6t!~D{k)1M*gqtxv==?hnZ-&SQ5LI$*^Ve3vs8% z0bFZB(Kz@7_vTxK5g7+?+-(ADR@sOSF6E?*^KF02{0|PbO(L4F#o0?zc2w)sVd8+t z$jfyuU}kZP=pE$l)LgeN&OjVrOQ`eq>R@qfj4j6@eo8(5euK942GoAWWehtkN+q)! zQBj!_!`I&SDOFH7BajIKrB#`q}{oI zE?agMk0nh94}ArgXf}~?n;3zO{|4b)Z#`x0yZLSFFTt*LeK3zEkp8AZ6wQ~Rwed~( zz0QgA&`)Cq;&SOu84B(5>hQZx0d7<~3;k;bu%xb?+#U%BtJRhqCyBwZ@K^N4>_Gmv zm0VxU!IPYMegmCPy&*xX6EH$jhLl>w6@`{gh1u&Lg4f{)x{2!nyjxxao1;=7HcO2a zpPGfM`oie$n|V-lu9==68{|cYYC*{MQi$355pq;Y>C}ypd=nLQ=HnOcj^=K~`FY+@ z>lxD-v4wxB!IU}VNv;C~T%H1K=6*Dsluu_)`Am|y&PPH-AxwXs$;D_m+#8Z@Qi5A3N!3$u1?^Ep0Z-TW!n(U^GVqDP{ zL7Q!rF>&dCaH;1w8-H1pvDiP0jdQy8o5u97vrB#RE4-~{gYm@dF%%Ca{Q(8oXIfZ zD#sPIQ9*mPIC`Wm9xartAf4UM+*h2=$S!@#`)ag|*bSCoE5|zUd*w_LYOERC?pyrL zse+8h!eL(Q2|KXrxlf3B2I?KNV;7z*Rs5SXmUX9J$YxStfSR5lK31ph)ne=98vx`K)` zZ{QK$3U=hI6rSq$L7Sdp@V%|c6vjD{Ozv&}?%H`&TylhG;pIv~-v*O^ZJgT{Be|^l z5=@l6NA&e7aYwKO5uX%+&RVx{G^Pw!POBx>vBIoI{zsm*?>mgyWPlLx8RE}3f%BWq z7`VLz#|2~=$&qiUkU9&8Hm<;%^#Q!Fm zQk4p0%pLv!24CxN>2(Qq>xePFUeSgxende1#QCgrza37vtxlZ!qVdG8=@=Vv6&A)_ zhZ^lu{4Hw)F>-M;i0D*stO8HS*lWkwTCTz(VD(m|`0ACRe%w@I>eKb=2NPWPI=!Wx?e%#nyvj@>{_~I#lNk{%qlJ`;< z<>v)psR~IrtJ)DDfWi`(*Z93$rPhy42vv_D818S!EwBbnw z;QDkVixauO=MSo`x{G%Nm5e3Q{{!c(?Qmt~OZdj^W<+&@$jF^sI$P}&?9dBDtqoQv zdT1w&`mYRHrp|y3J<2dGDxd1mVCdO5gPoKVgf1UXfoym#Xd6#}*j{gxblt=hE-;43 zj!0b6KM$?$`@{DS*0{p%4ld2sW(|+IqqfLr2s-b9J6k$n>*z*IJ>Cws3BfQ@=#9(n zj`3c;SOjm62hv(;9T>T0!QMG0ggH^C;CufKdT2*B(3z{~?!L9yBYl|h;JRrqB9l?g z-2$7p{o?JB?19$p0$|!B%a}BXF`g#e9$m$TiPWhgsVjR?Rnnbg4-`P8L>9@Lr@_{! zin1&It;X5?8PuUWocu8O3347XY_rZQ{xqG9r1$7jeCVfwf%=y?_mLlM+Om-~pZA1D zzBOdn&{7({x&RAa?d0w;?qIK^Mx}}-fvd?r=7nq^=YiM&^XH|3PQrD3s;kLv5BiBp zrz^lh^#a%X^98Ly?woH+!Nz^lpmL@z(=+=tnB9HK&CBM~y9I8ju~`>p-3laqde_M8 zOaq>a(MzmeUx`OoZpRFz4iwpHN@X72N-t zUs&=g70&#vLb<3$<4iRb+%>ce6`Fp5*W{J3i2FWejAubrxjX$-;)pJ}e&G76f^HJb zfPK31G|vAX((!Ws{puLnr(}jz>kt($zcZFI{Q^q7HPB=D2%@KmgF;OqEVB9!8}0}( z!w*iAog(I}!=wj1<+*lX*y)VHT?<&F)$^fr*=C%-Zzp(|aQlc?4?$4a4xX*Pf`dO9 z+PcghMV}fWZ>~S8=*W|_zyINXrE~bPnign3c{#pvxI$iUu}3MLi@dLr7NB|T6&^E? zK`{qunEK3+-J8pu%|kBewlxNv4cs7NWgC9Z)rD5$L7Z(O$UL}^fVO^d{EtnskQtZ` zZufOKpTasA2_7V(C+gAr>jAPR<}H<3z5vy({(j&C zH^*{mqVqTS`5_F>2mA3yn?KRM11s3nI~lOqT?HDeu7JD08k@Iu9xLI0f_op-g7v-s zuxouPg#OLPNAqq0_iKag;!^Bs*{eAJhA3OWxnjxJE}A*^r(y@yXnC(6pB(Xq zW4gX{?M^v%bSwuB7i#Hutq~YJs9|~8Kvj0#v0)i^ajo|dNdB%2ZX;S^E@UT zTY{>=%fa*hEY1V=3@p#jfsA4gdNbY?bH|j>_~ln9z4{a8*J)yn=Rb1(kO%g-jYAK| zcR%bJgp}Jiz4g9=L%(*Q(VjpIZj9&Yzk5xq4qXNvnON}jQzmqGG8Dd6XRAd9F#S<4 z$rV`xs>fduIlt%p=e>Riziq&BqAJ7=-9hJ-rnt}2i$<&7!ZoP?0aH`?+vmkXiqu9X zKcWf$q$Gn)gQD@8S8H*Cqdt2|RE52kkPOp{{BYL~3)m?v0_G!Up>eSb1d>w_He5hF zyCZN<+z8B^98SjCMnPo#bAIfHTfDDr4>|TfH=~@o2RkC$scCQt&c0qoEXNEWcTNL- z+O!kqJmeVAjR`!1pRRl%{{uL+&J`M0^NU}J2BU()N~D|OQT{Z?N!mGrHI6)f<_0eR zxv>T>4-2E=e-6a(k`mJ}Nfve`o*#!~(*qG5NL_T0&ZSY9HHnf(TA4@_X| zB=6vfwbZmz)?47q2qzAS@OD>S_Pg01!od)ChayENTMHCaK zJ%pBFZnyDm8E@4FWwvZ@CW&|6MBZFe=FWlyns-D5@yIx?a+icBr6Cw&y$emexekn1 zD~U>uKuzaVNbXd_pp6BXI=mPZawLeG#vQn1`iA54JR#W@f2bbsJZ{mR%d@p1Wbc|J z7@@6Xdaf^c%UAMGemcQ7QJ|>V@{(4iyu+wBvmxGU0Bc87LF`%{(b9a2De7A6OZ9Td z{Uy!Z(-dZYBqLggltO4=6a;w%!-FP4DzmSi-qiCUs=v5h$#`h-K^wrH2z68}6lOLa zp*+V4GTa<{FAXcMBy5o*icPczp%i(vxGl#@L?0!euYcfYZks`=G1uuI)FozS z)kOI27rgt^lMJ?q;mVSq)IjPKsgAx3kECT;VZXze`$U<&>g|K;E03au$pEhQ5@vSY zc|{6d%!X%?)@U<1iglnhSnST->CTs7uE7-yRe6M>U5W5Ar3yFMdZApE3X0ybM5U4) zcyVDM+4$rvI{0Yw1aCa%7tHrGirKyzC0~xhgmqQOba`T%sSzj^{=w{#&-}HIxUOFM zM=GOu)#$L`7@4o)NbRgv z;kO1(D2#-L-W(cob2jTH`5yAzc3`&s7i#Ix$IzwSAoFN|^jKMgtfCk*XMkhdxQCEm z3f%10-30DW>cqIt$E5Y#A98qIGIYz#hez)N$c{UXILkf(qz31(v+BYjyIGJ`_OvNJ z5;qaL%3h-KtYK6S*1-wpnJ6bOgnk8a+~4{d=J(igy^e>tT~?8P{rnLWw+O@did<48 z+(1TJvSH_se|Y;XkIb%YhK>^ngrzq0xyfDZS9YLF&HN})IL`_QF5n%B%mvlIKZ(jn zI;~UTfsa`i9C_ypy4-wLY zE6K4bHbuOx7cyaRl{nj~dkY0`&jh>ldKCCz1mo?;FyGh%tp6&a;=*IJV{S8#=g)Oo zRExppG)p~BC8FcM1Tdb|i$6X|K-8&e7#;Sz$Y9M^{-Ki*7=F*1wH5L&E^$R#Go_i3 zc|z>g=g;^N4?GDgu@%=JngrHY6Tm6fn|sIaAga3w=NtPE#2&0?CNI%7@;?exPe+!y zHRU8R4oTskUn+=q)vi*ry>|FrWGk3*`LP`7b$EZU42}LwfZMqWY-5-Ke1F>t%Zh?Y z!{jN0 z@iIRZlLF?#2cI~Sv0xutvSt~i?Y{zjt*_A_x){|@`q5HtHKu!KFwrW0k23=gf$@tm zlsc`&RD6tru%KY*T{w%);reC?DbL`bq#eFK_X4)I&f;6Bw((CKJWo2may^WbyU8f` z*@3+>=quc!`#uj9`KgxS*87vNVW}T!zvWAZRxE}+^TW~U&~kM8CJL%w5{Y4$GDI~7 z@yv1>c-Mloq5bbizJFF2NY3oQ{^UngfcF~aAGJjhzhU_NA{tYz265GWJiveg~jT(!{7#*IIOVoX@IcX951OHw)F z$NRO?7R66)qbpsfV75U3+Wza{ZTS`qE`>JWlEGF7w0od4YfQkj7WXXTM^s8v|bRk($J*3Xcs35>v@8yEQ&wevw`qdcs3zS08!abELi?(Opp`$ulkW)2HtzUwc?e-mYT zCWtYmF9rxRFOrsBy9^7+F;KD)XPQp-@Xi-Hli}0n`H!~Dgz5JF^iWtcO2ynVzW?<- z+7x``o2}sb{0bA8qw7vU#X(B-Tja<|z2_(>WeM3_Ox-iWgI!kKKnhA9gIn`&d^PhL zIX3bG&Xn(kz>+1&a9H^bD4T1t$6Djz(OqL^j@S&myUhk4X*|P*Is@7oe;!iQfn~J_PL>0tQgl7ZnaYs!%idyZ=v~aJsG={O znqlI!ZgjZSk59~0`5-=r?$|2LY}YLVV{T_T|IdBg^5_&*mX~C1H-EwM-Ph>2awXZu zpUzI3;Epv%wCTR?F0?sp4xhV%p-oPX`it_2z%B-AxxRmN)qDQ0ll}PRjtm1D+!-P~ zgB{Z@#Pk!BSclR~Jhg2SyO`?*hu?9<$Zc~#<}$~jI9Y_4SdQy2)r0il6wvL-0^uzZ zjKsDw$Wk#Q&o!r_TCo(H>$Q+<`eV*2U7Co`?c`vt!o7fw^LG6cW14;QOTtbQdy5 zn{i*`gVK|!_MMxgI>{5pPT}h~C!V#zWHzCS(lA|BcDyGE-+ou% z^$N+6qReHiVfHs%c9uYSu@W8b^PqFK+=gj+-!U0K7Y6j)p{7qxGNx)BWR`$G`bw-M zRv!uxFKQFVyJh62lm-sM5^Qc7LBpvFc}D;S$Q#-vXp3Rk;S@%-cNkvC^9(f+vxU}}CIa%yClgF3P}FUN;B zK6f`GHW-60<)KE~Hzd%>i7R1jM>?02G{7y_7tEJV2Ttv- zre}gOP%c`U-8g#|4FB8@1_OeOz}g`YRn>=?F=u$WNr&O7>kz*2+JI*!L__7#HljCo zEwbxA)Aj#^N!X!iQe9NX^|>d(dD}~LRs03M8SfHuP)T?#ER1L8y~a-W_Yk*SnpyEz zf?aelfv2>i7#s$_DRf<@<_yhm9oEYPOxENQK38R(z zG+Z_7GUx`SV%DHHh?IDOw3Z*H{p}-*PA6l!#}WLv%pAH>&*IGA8(=NTBTL%kS+zOI z#Kzhet3K#~UhqGhB<{sCN}NXU!#%nqa0L2qbb_&xE4O1@&Gr^iYSeQM4Egu@Nf-H; zlbwo$7%)51) z-ni>?H}x7j1mafrX!NXgAlt>@XuJu$@wJ2#ssnInMJ{*u>Y|}a7ohO-an||E6R1&% zz|Uhw?4tsNJgMK*>@Z<&3M1L{A(5BZJHWX>br`R_V(|R59(9U}aFe$sHV%7my%?f< zjoAV4@1-BaYI7@K$l%K@?4M$TMZ72S7GJ7E;uDA2KyYWG4$3w^cuHjSL9cs>E{=) zE>i^RoBW__3zu8llt4T-#?v#a(x_^;2;K3*o!nd)iZKd3drJ zSeQU}=cmB?=_;)H*|+3%ZWJi&m@IHv*E>L015Zo%QWL(>y>r?n9AXV*wl>Qr>Mv|!J0-j41$ zXKBTjM{rv= zTbu)NE}fdMi47d8Xy?Ols6XfqlWh!GKR<3Rs^kcdr%we1NjrLiV>!70Oh6sR6hGTk zpd=ZA784fszV+c-XKKRKuHBI87f2lEM)Lbr&S0&%r@2u_&~7*YP~eyUXIib$%Q}#2DVJ7lcF!AF^Cz0yDAcBIk*D04f>T zxRvWy(F9-GX*fn~WTumm38!GFGz@-Bn+Sb#uS4Lx{U91F#I#2|gLZ9W#^SL9jXg8U zUucw1UWF%-s*Tq%=<9bJDB}JnSGz%}&>fDM^dA3)O#!#0XK2&cLH7x4#;GSYq0YVx z+fvf%(x% z3P!!D#5M%iji1O41%PY;ZA@IJ%C{<;!)pHvq#5QM`*Poaan@>a=9$I?NKe?s>lD@? ztVt^r80s-Nr4U0_#Dk~CW0YPdx;eqRUXS#1Xxecwiq8OvFE|W^UJWbB6l~ zpsyN-4%aTg(i~rG4-~{>Wo7tD$q0ith0)@khomoiHm!qPWcCWf+ONvsP~wbcnp2sr z>=StU-()!CrpxB}o`U2HBm7wRS41%56V0*n!(~er7F-GyR|R(b~1Yvw}G zj|NmLcZ59UP}r;?0r4@${NVr8n5-=`nf^PCWKr!a*cxO_D!AI?>x4ONOSu;4NPjqBgTNg0do_!RQd`9~-(QBLVji2ZMH6)OD>1SE38j|HnK$FnSiPzaht~gx zuOIk>$DvB>+98LDN^vMFZ%6}|7icR|l@4&!F2P4}LcaP(_arV&&%u!%f!Y z_RBXsCD#&E*nSTya0+{*GM)JK-6xNSXB#U#OTg&E*U_f>EonE`r|x^XS>Komo*eLm zCH{77Qt~3I9<2ux$ComR2`YF@coKCp{11YM6RGmkbwqk*1by>g4q3R8bArjN17lM! zGP7hZW0ANLPuTgRknSXs{`> z?3r7HoP?leNuZ=S*AO@%iF7$N(civH@ogKqYe_K$}Odv8L+RT>K$r#vnACB&vNG6_1;h7(=#ryA;6OAjP z*zNTjb_IQT3x?on8{k zI?F-#d?jYfdT;FcbqNZ+@?b^6N+x4|EOKYWAuMlPS zGsnEi>!q5IajFy5$5mK8feKLFJ(b~Q`xj*>0r=c4pd;13xX&;XP0g3%L2ifY z^P!enjJOgPoy)LS#Ts3;7GRZ%HIR!R@mTC?^pw5EZ+AiHtOzBUHx6LZ{k!}c-erD$ zOc7thr5@&5oW!RObkR)UV{EEd&D;aEGL#2e(Gu|xt-aYhnli-L%1Lp#+tFoSC4C!@&=E>pIDgg09? zi#++yo9_H~4~mwi5TlJ3aO1mrxE;%J>Gku8L2VzkdB2fwP#i{^V?^L=(mRS9Pw~TM z`(eNfj;}E`16|V25_PR$nzCjUZ17CtDf~142A`+H_Bst1Z0!XJt9M8nzR`w`2bdHZiDvySI5f71?cQ^RG6W_bgvh#Y7=46pby96QmEC$t0aHv1)j?%Z#rR#I7+Xc0taIw$we+FD4NraJ`+A; z{%Jd6&?v`0p=W~WNf*F(`T?F)axz3+uwmxx5`Z^>1w=t*Ee-5yN6Th2=B#2neSTY) zzoqXUsSUje!4_Klyo)pVDxdApYoae%^r@VlYZhU;IG%w}yEOY(SkIHS9fU&oLi9 zk(D|DM6kaRzwYkDjS|94`jNw^!E*Ott3V{DIc6@MNnJfIQR%oWJbgi)c{b6GX$`;3 z?RwK7$NRq1L-kWEPrEt^C3wCYDptpk#W5?B8-eB-;96A>T7F=&|>#-e> zk^GgW-VVSN8wTjU%{*;i6=vYXeAYnb8k8C`T=(MxJe53y0~QFI-X2E7x(#U5FiPew zIgg{CXVF1!Ui!LX2Xw~FV9xjGW9zhm zxfzN#1!K5@6uUUphw2`EfhFRr(CnEPqG}4vsYn1;^a@(J?uXrP(|FO11GFsP9(5k3 z!pZV+sIprCs_%1Q@a!LInxev9kqtwmczaM+7lpmo-jn^++wjeycTgQzi8^~KNW!_x zSRHZ&ZbpBhq6U+oCNi62z}_H=`y4s0)_*Ykw2b`HmxaWDYmoHI0Sc_&87B;LJc+n> zv};xj<}4J%Z_C@^cRvp8v1BKZB#fM6#JA(beekw9HMv6~!9IL%kP>*F=0?dfvdiLx#WBiz@!%XZ}gX$+oLE!ao{+4-N zM3kG!THmxlnfz@2BbPnYHue{Ba`*}RD~q7!Ufbb?{G27>1+PGFpzw zSmxGFdFIk+v5Mn5N8Nz+-O^0OTLlPg=KSos3V1VcDLU^~fbQTd_%(h5n=-;s)g*|w z+QtT+KDh;ruRjs7=rG#3?IoDcdO-5dKR}ILTHHC+PYQI_7uDviA-WPTX{66hs$CKT zhT(n2LuS9had<1W%q`@&CfvMYK^@t1`5G#p-vObMGl@zTw<~G=N*bJ6d3zNqahEmc z{rfG6qb>PVj*El+k~l_vX$ns5nMdNbo6{qL4flY!JRSw9GmDwj@i(OS zj4%_TFdJM?Zw8}J`@r;g2Cv_U^MonQVFf!b!1?JC%uuE-Zm)=;n}dWPIVS_W?%qJT z(#fco=Eq*~>mUk#_ffHR2P0JZ0=4tc@#=cZprkYhEw`4V#mQ_Ovj4z!>F2;+!5D02 zTH(IZe_W5@11|YKo$VYcG4_&EW||_Lc&5F}!FAURWb?&{qgw$ksrw1+YIQdMK?O)h z8gO&AEA+2R1DZ^W2e&g}q|`=`UAQ}*wp%R*#hu@AzLuf!ne-T#kT#2(r%oXUP42=G z2?d;>n!+2{CyQH!7Lg@xArQ5|haP+)L85K^_)DU^@VR(7saN@h6`k{8ex?;gZciyR zvK4+YHDEh?GIWm~CNlqigVSnlx??y4rpC#^S@r8sn(2WX2cF?cu?v{Hoh_CilxJ`? zoL?Il52g<9;V7ws6KcM^It!#6(wpBC5lRB~QgY*>B#Bc<1jnfc_*K-0eo*d(`1%xZ z`WuSJ6r4#R8bGGYOWc&b84O>(B8o$yRB4hw(JoN~aYY$c_Q+zkA)^kqIjXRCRo;X7 zp^hT)YxQ6upAX8>J$#iJ-9TFE=<3u(OjuAO$9iHI7mmj#dBu^$mNn3i?`0UJ!XGHt zWy7@jT&EuClbNi$$>^IJ%sV6WkR+;o=F4idVc2tJm?7^2w)&jYc4#xtVNn>)`BOy- zH($o>XYPPPp({JP_909ixk>~@m#|V91k*pfqqV~mP^a}LG~7(a0)?$)Xy;ojAhNj3 zxSNifW#RPA7ojGZV>d{}b6J6Qm{8iqJDW#HX_Y06+7A_rR7kO-=j7;Y2t2!QV%*3oGURw$dHV^;+~B2b-qd^;YmY6`MthnsksM$D*X5jh#y<+4=_z5#TXR%h z90N)zt)%z;Mcn-?9FNWX1F>6|5;|${aOA4RF3gF0}DA3c) zgAakSY?+)Sp8qrg-M=O9d0Ze*vqJ&;LR+Z+tPrqFDaRArb7}s?7 z*mqHiO*n9eD31TZjky*myUCMhlT!l&lJ~Lys40!>T@S~))5v6B4MwMP4BbP%gYBEE zyjYv}e2I6V7-@79hW07ICsAv1_IC}vzxV_Vlhed-t5vMW=2dXx^dvTQ+!MsgWJqJh zQmA@U44;jwiL&+qvQiwWmWv?kUFn2#N>aF9VK_(!meD+Zqw$}ni7@cy04R8Id57cw z;Bd_e2uw*G5=*CFaPS8yV? zzdpC30FJuWBNm^8;gj>Yp2ZRrTfSHz=ipa zZjU6P;_({Lwc?CqU``J)k%4PJ8O^@GJ5x z;GOFOdc-~*luR|C^wc6+a_bXMoLz^8%0Hk+J&Cro3WA#SHRJemnlSQU0#h(NfoOc@ z-q1(tz*k=hEZ0m1`#oGQT=)~lze*=vL!D&J)JW7lzRY+^sy95yzk<(i7m6p14Oq2Rf3jI3F{48^zAa(TYjByGbGRncAzp7Ny-duk#RG4m`oiV2W6hbFKq zV?C+bvP#;kD1yyfIe+3+U07f|ot3UjrJz}igD&bA(Ktr7u0@(DF~XM`3@EM_mxj)k zI}nno&1cp1*@U!(xH7_%9Org>V;YO`y|_2orL~pVTm0imj1)nF3zx^=w1B>jHUY_I z9~@U(0R1US?8>luJUOh)hW_=0onobQ{r4Bde%(WSrE?kQb6L0Q_p;2WQY?l=39v&| z0no6a4DUp$VsV;2p4@c++*fj(@R^gCX@v~aWpf&js;e@^CS5#3%^dvxMFa1DV1X7? zl4IH1N$)Je$iBJ7+EX?YJ=PUT_5xBn`>yc5RMbC(o?E+=ipq%lPl@MDR2l z3A1#_slT?O7G#<8xelC`1n1rfbUZ6U82`kyOl@7a$ zun9YTAgQ+vAKFb|8*nWZqRy=NtO(F|;PYREh%wu3reaDC0KMBx|7yGk<&bs|4hFh~p>fr2~pn5f>VM8a2`4HBw^&nXI2 zDD4`II6dOn-g4}qksoPnkz!)={Beur6=Gzp$0o*XW7)L|v`l;@1f;~^`Aw%-Vb3uT zJ7o)T-^xjo%6@t-zMU@ZnTWf!Zh`V+hQDjAD>h{jT;d>r-9G8~WON)f76-zed}-Km zbqh-TSc#_PSHURW0B=kB<9|NWnKMEA(QLUGyW2mAS9o6)a-JSS-@Oxs?{TNC-+KPQow#bMRcsH-5U!6wJ(4VmIh3T@*~R6KVM{K!No-pgf9Y(v0N!4ZNEU!=Z= ze97L=+@4^PJT}`EVbA7uyfxem?+$YO4%Z@R^E`%5mWSb<>Sio6Xb0aVad z3w)Y#Ilsaw=E)5ec3(*k zkX;<_yg)@3f0UnO0tD~i#kfD%cB!3cJ-LMPk2hctHeSJv*20W#59IbxVqsR@oDey|$ozO^#jKEYEzh^<&a!>#~kt zuA}nv0u=LYM)&d&;_c4qy=N_B$gC(dm$nAWdylE06Nh|zXN-xy3fS=P0P1e(=h;Tc zptq|7_TLM_@o7q|^du{s`#Au~^2s#CUkTrY&1FU67D7eaRqEWE1u7qw8QV+mLhrVF z5O87~9IHgo2!luOK#^|xq$Ak>p+<#NxQ&2bj$MN=gW`b z?SLul!YjJ0VRsO;nklkw68hMYy%2Hc4G_!G1Nxkg?o%V_Vdr={wRsaWIn$oz-VDO@ zZ=AdM*DpFYtAcJ^lS}sIrr-tcOjxOMoc4ni2y!`*?DrpOkJB`)y1{^-s|+*KsRXl1 zuA$#WNvz1Lp#P)jyaTa%-!N`gWR)Ztsf-W_h4;BniBeQZJCQ`vkfsVHGnAPkBV>f4 zA>)1SBV>d`q9{tcjHpC?qkiZ2zdx+^JkPoB>-v0VR-c9ZwqklxWfL~;xrfKOv+aXn zF1Uy8#v17c67h}!v%Wjj$1MxRj$I~=ZFhIezZ~r%BnU z@yHG*?z&A5qz97v*M_Wuel{%KH5Cq4Kc>~Y&*3M}h2)39GCXQgMSk6npm(aInVKeD zrfNKe=KP99%Uw>G_NWHOZyq7?w``!1VPIqU}E42IQ>Qt{klU@ zN~;bZ3s`}>=R7iXz!rmTcH!~%IIe%-iuZS?LSf|(-hhQ17mxf&ZDoUS?IUq2CAk}o zP3lk~_z5-jpNMtaG(e8a$cnfrGKv)|u~f$nb53#lGV_aMV@M~sTBP%~q;(L3GsDER zDH}R(zlCYXIc~9`2)njSno5dJqRRLWXUu)b)9;)L2Knp2<(NNAd-o4>M$%v)VFp_i z_!!2wt-!r+Y6*XTAn4w$<8ABWc0)e?IHG+UXB+0j8_#nv9&-vi%k9}W3kvzO8%rVk z+E%7V@e%fhtYz+Ln!;a42J9w00CUS@SoT|qty!Ll4(lec-B%xgpV1{eJz4>&qZ1j! z^5tNgcAF$dHPWc~Te#nP45f1?V_1?W>4~@uU&|0;_waF8q>yL3M*v2%S7Wf)DJcGz zj8nP%g80u^P-kb zV4^a2;&1NW*L2sqmPat8B;Uj@-&%0Dtr9D-r-cX)#le&2WcX#@gugVW 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 TqV$iC@%K^xc~?^@h4KFbrCZZA literal 0 HcmV?d00001 diff --git a/onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.txt b/onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.txt new file mode 100644 index 0000000000000..5affbde73e5b3 --- /dev/null +++ b/onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.txt @@ -0,0 +1,55 @@ +Embed:EmbedLayer +GptAttention0:GptAttention_0 +GptAttention0:Add_295 +GptAttention0:LayerNorm_1 +GptAttention0:FullyConnect_MatMul_0 +GptAttention0:FastGelu_AddBias_0 +GptAttention0:FullyConnect_MatMul_1 +GptAttention0:FullyConnect_Add_1 +GptAttention0:Add_360 +GptAttention1:LayerNorm_2 +GptAttention1:GptAttention_1 +GptAttention1:Add_492 +GptAttention1:FullyConnect_MatMul_2 +GptAttention1:FastGelu_AddBias_1 +GptAttention1:FullyConnect_MatMul_3 +GptAttention1:FullyConnect_Add_3 +GptAttention1:Add_557 +GptAttention2:LayerNorm_4 +GptAttention2:GptAttention_2 +GptAttention2:Add_689 +GptAttention2:LayerNorm_5 +GptAttention2:FullyConnect_MatMul_4 +GptAttention2:FastGelu_AddBias_2 +GptAttention2:FullyConnect_MatMul_5 +GptAttention2:FullyConnect_Add_5 +GptAttention2:Add_754 +GptAttention3:LayerNorm_6 +GptAttention3:GptAttention_3 +GptAttention3:Add_886 +GptAttention3:LayerNorm_7 +GptAttention3:FullyConnect_MatMul_6 +GptAttention3:FastGelu_AddBias_3 +GptAttention3:FullyConnect_MatMul_7 +GptAttention3:FullyConnect_Add_7 +GptAttention3:Add_951 +GptAttention4:LayerNorm_8 +GptAttention4:GptAttention_4 +GptAttention4:Add_1083 +GptAttention4:LayerNorm_9 +GptAttention4:FullyConnect_MatMul_8 +GptAttention4:FastGelu_AddBias_4 +GptAttention4:FullyConnect_MatMul_9 +GptAttention4:FullyConnect_Add_9 +GptAttention4:Add_1148 +Decode:LayerNorm_10 +Decode:MatMul_1165 + + + + + + + + + From 68ea1bf70e43d13dd7ac475fe92e2e1e6ab3f870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Mon, 30 Mar 2026 19:18:05 +0200 Subject: [PATCH 12/14] fix potential out of boundary issue when initializer a SVMClassifier (#27699) ### Description If the ONNX file is malformed, it could lead to an incorrect memory access. This change enforces that does not happen. ### Motivation and Context security issue --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../core/providers/cpu/ml/svmclassifier.cc | 34 ++++++++ .../providers/cpu/ml/svmclassifier_test.cc | 85 +++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/onnxruntime/core/providers/cpu/ml/svmclassifier.cc b/onnxruntime/core/providers/cpu/ml/svmclassifier.cc index 4bfb0f673404a..6725c92b09f82 100644 --- a/onnxruntime/core/providers/cpu/ml/svmclassifier.cc +++ b/onnxruntime/core/providers/cpu/ml/svmclassifier.cc @@ -72,6 +72,40 @@ SVMClassifier::SVMClassifier(const OpKernelInfo& info) ORT_ENFORCE(classlabels_strings_.size() > 0 || classlabels_ints_.size() > 0); ORT_ENFORCE(proba_.size() == probb_.size()); ORT_ENFORCE(coefficients_.size() > 0); + + // Validate attribute array sizes against the declared dimensions to prevent + // out-of-bounds reads from crafted models. + if (mode_ == SVM_TYPE::SVM_SVC) { + // SVC mode: coefficients layout is [class_count - 1, vector_count] + const size_t expected_coefficients = static_cast(class_count_ - 1) * static_cast(vector_count_); + ORT_ENFORCE(coefficients_.size() >= expected_coefficients, + "coefficients attribute size (", coefficients_.size(), + ") is smaller than expected (", expected_coefficients, + ") for the given class_count and vector_count."); + + // rho needs one entry per classifier pair: class_count * (class_count - 1) / 2 + const size_t num_classifiers = static_cast(class_count_) * static_cast(class_count_ - 1) / 2; + ORT_ENFORCE(rho_.size() >= num_classifiers, + "rho attribute size (", rho_.size(), + ") is smaller than expected (", num_classifiers, + ") for the given number of classes."); + + // prob_a and prob_b, when provided, need one entry per classifier pair + if (!proba_.empty()) { + ORT_ENFORCE(proba_.size() >= num_classifiers, + "prob_a attribute size (", proba_.size(), + ") is smaller than expected (", num_classifiers, + ") for the given number of classes."); + ORT_ENFORCE(probb_.size() >= num_classifiers, + "prob_b attribute size (", probb_.size(), + ") is smaller than expected (", num_classifiers, + ") for the given number of classes."); + } + } else { + // Linear mode: coefficients layout is [class_count, feature_count] + ORT_ENFORCE(rho_.size() >= 1, "rho attribute must have at least one entry."); + } + weights_are_all_positive_ = std::all_of(coefficients_.cbegin(), coefficients_.cend(), [](float value) { return value >= 0.f; }); } diff --git a/onnxruntime/test/providers/cpu/ml/svmclassifier_test.cc b/onnxruntime/test/providers/cpu/ml/svmclassifier_test.cc index 2fcf86d0447e5..5240c909d2878 100644 --- a/onnxruntime/test/providers/cpu/ml/svmclassifier_test.cc +++ b/onnxruntime/test/providers/cpu/ml/svmclassifier_test.cc @@ -263,5 +263,90 @@ TEST(MLOpTest, SVMClassifierLinear) { test.Run(); } +// 3 classes, 2 support vectors (1 each for first two classes), 4 features. +// Correctly sized attributes: +// coefficients: (class_count-1) * vector_count = 2*2 = 4 +// rho: class_count*(class_count-1)/2 = 3 +// prob_a/prob_b (if present): 3 + +TEST(MLOpTest, SVMClassifierUndersizedCoefficients) { + OpTester test("SVMClassifier", 1, onnxruntime::kMLDomain); + + std::vector coefficients = {1.f, 1.f}; // needs 4, only 2 provided + std::vector support_vectors = {0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f}; + std::vector rho = {0.1f, 0.1f, 0.1f}; // correct size + std::vector kernel_params = {0.01f, 0.f, 3.f}; + std::vector classes = {0, 1, 2}; + std::vector vectors_per_class = {1, 1, 0}; + + test.AddAttribute("kernel_type", std::string("RBF")); + test.AddAttribute("coefficients", coefficients); + test.AddAttribute("support_vectors", support_vectors); + test.AddAttribute("vectors_per_class", vectors_per_class); + test.AddAttribute("rho", rho); + test.AddAttribute("kernel_params", kernel_params); + test.AddAttribute("classlabels_ints", classes); + + test.AddInput("X", {1, 4}, {0.f, 0.f, 0.f, 0.f}); + test.AddOutput("Y", {1}, {1}); + test.AddOutput("Z", {1, 3}, {0.f, 0.f, 0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, "coefficients attribute size"); +} + +TEST(MLOpTest, SVMClassifierUndersizedRho) { + OpTester test("SVMClassifier", 1, onnxruntime::kMLDomain); + + std::vector coefficients = {1.f, 1.f, 1.f, 1.f}; // correct size + std::vector support_vectors = {0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f}; + std::vector rho = {0.1f}; // needs 3, only 1 provided + std::vector kernel_params = {0.01f, 0.f, 3.f}; + std::vector classes = {0, 1, 2}; + std::vector vectors_per_class = {1, 1, 0}; + + test.AddAttribute("kernel_type", std::string("RBF")); + test.AddAttribute("coefficients", coefficients); + test.AddAttribute("support_vectors", support_vectors); + test.AddAttribute("vectors_per_class", vectors_per_class); + test.AddAttribute("rho", rho); + test.AddAttribute("kernel_params", kernel_params); + test.AddAttribute("classlabels_ints", classes); + + test.AddInput("X", {1, 4}, {0.f, 0.f, 0.f, 0.f}); + test.AddOutput("Y", {1}, {1}); + test.AddOutput("Z", {1, 3}, {0.f, 0.f, 0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, "rho attribute size"); +} + +TEST(MLOpTest, SVMClassifierUndersizedProba) { + OpTester test("SVMClassifier", 1, onnxruntime::kMLDomain); + + std::vector coefficients = {1.f, 1.f, 1.f, 1.f}; // correct size + std::vector support_vectors = {0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f}; + std::vector rho = {0.1f, 0.1f, 0.1f}; // correct size + std::vector proba = {0.5f}; // needs 3, only 1 provided + std::vector probb = {0.5f}; // needs 3, only 1 provided + std::vector kernel_params = {0.01f, 0.f, 3.f}; + std::vector classes = {0, 1, 2}; + std::vector vectors_per_class = {1, 1, 0}; + + test.AddAttribute("kernel_type", std::string("RBF")); + test.AddAttribute("coefficients", coefficients); + test.AddAttribute("support_vectors", support_vectors); + test.AddAttribute("vectors_per_class", vectors_per_class); + test.AddAttribute("rho", rho); + test.AddAttribute("prob_a", proba); + test.AddAttribute("prob_b", probb); + test.AddAttribute("kernel_params", kernel_params); + test.AddAttribute("classlabels_ints", classes); + + test.AddInput("X", {1, 4}, {0.f, 0.f, 0.f, 0.f}); + test.AddOutput("Y", {1}, {1}); + test.AddOutput("Z", {1, 3}, {0.f, 0.f, 0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, "prob_a attribute size"); +} + } // namespace test } // namespace onnxruntime From 52709bc5bcfb050eaf7cab9c6b72561e0f59b4c3 Mon Sep 17 00:00:00 2001 From: eserscor Date: Mon, 30 Mar 2026 15:41:30 -0400 Subject: [PATCH 13/14] Add cron job to release pipeline (#27864) ### Description ### Motivation and Context --- .../ci_build/github/azure-pipelines/main-release-pipeline.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/ci_build/github/azure-pipelines/main-release-pipeline.yml b/tools/ci_build/github/azure-pipelines/main-release-pipeline.yml index dd9321212a140..f955667eaf50e 100644 --- a/tools/ci_build/github/azure-pipelines/main-release-pipeline.yml +++ b/tools/ci_build/github/azure-pipelines/main-release-pipeline.yml @@ -1,3 +1,7 @@ +schedules: +- cron: '0 12 * * *' + displayName: "Nightly RC Build" + trigger: none parameters: From 5b580e2832e6ea59c5cc065e0dfccbc95dbbbdd3 Mon Sep 17 00:00:00 2001 From: Guenther Schmuelling Date: Mon, 30 Mar 2026 13:25:27 -0700 Subject: [PATCH 14/14] fixes to pass webnn DequantizeLinear compliance tests over webgpu ep (#27778) This PR is on top of a previous PR and fixes the remaining issues. https://github.com/microsoft/onnxruntime/pull/27706 All tests here should be passing now over webgpu: https://wpt.live/webnn/conformance_tests/dequantizeLinear.https.any.html?gpu --------- Co-authored-by: edgchen1 <18449977+edgchen1@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../webgpu/quantization/quantize_linear.cc | 187 +++++++++++++----- .../webgpu/quantization/quantize_linear.h | 29 ++- .../cpu/tensor/quantize_linear_test.cc | 30 +++ 3 files changed, 193 insertions(+), 53 deletions(-) diff --git a/onnxruntime/core/providers/webgpu/quantization/quantize_linear.cc b/onnxruntime/core/providers/webgpu/quantization/quantize_linear.cc index 2cf0f11ce46f2..1bd313053ed09 100644 --- a/onnxruntime/core/providers/webgpu/quantization/quantize_linear.cc +++ b/onnxruntime/core/providers/webgpu/quantization/quantize_linear.cc @@ -5,6 +5,7 @@ #include "core/util/math.h" #include "core/providers/webgpu/quantization/quantize_linear.h" +#include "core/framework/int4.h" #include "core/providers/webgpu/shader_helper.h" #include "core/providers/webgpu/webgpu_supported_types.h" #include "core/providers/webgpu/webgpu_utils.h" @@ -22,8 +23,21 @@ Status DequantizeLinearProgram::GenerateShaderCode(ShaderHelper& shader) const { << "let output_indices = " << output.OffsetToIndices("global_idx") << ";\n"; // Get x input - if (packed_) { - std::string unpack = (signed_) ? "unpack4xI8(x)" : "unpack4xU8(x)"; + if (packing_ == PackingMode::Packed4) { + // 4-bit packing: 8 elements per u32 + shader.MainFunctionBody() + << "let x = " << x.GetByOffset("global_idx / 8") << ";\n" + << "let x_raw = (x >> ((global_idx % 8u) * 4u)) & 0xFu;\n"; + if (packed_signed_) { + shader.MainFunctionBody() + << "let x_value = select(input_element_t(x_raw), input_element_t(x_raw) - 16, x_raw >= 8u);\n"; + } else { + shader.MainFunctionBody() + << "let x_value = input_element_t(x_raw);\n"; + } + } else if (packing_ == PackingMode::Packed8) { + // 8-bit packing: 4 elements per u32 + std::string unpack = (packed_signed_) ? "unpack4xI8(x)" : "unpack4xU8(x)"; if (output.NumComponents() == 1) { shader.MainFunctionBody() << "let x = " << x.GetByOffset("global_idx / 4") << ";\n" @@ -51,10 +65,14 @@ Status DequantizeLinearProgram::GenerateShaderCode(ShaderHelper& shader) const { << "let scale_value = " << scale.GetByOffset("scale_index") << ";\n"; } else { // Block quantization. Scale input rank is same as input/output rank. + // On the block axis, divide by block_size; on other axes, use output index directly. + shader.MainFunctionBody() << "var scale_indices: scale_indices_t;\n"; + for (int i = 0; i < rank_; i++) { + std::string idx = output.IndicesGet("output_indices", i); + std::string value_expr = "select(" + idx + ", " + idx + " / uniforms.block_size, " + std::to_string(i) + "u == uniforms.axis)"; + shader.MainFunctionBody() << scale.IndicesSet("scale_indices", i, value_expr) << "\n"; + } shader.MainFunctionBody() - << "var scale_indices: scale_indices_t = output_indices;\n" - << "let index = " << scale.IndicesGet("scale_indices", "uniforms.axis") << "/ uniforms.block_size;\n" - << scale.IndicesSet("scale_indices", "uniforms.axis", "index") << ";\n" << "let scale_value = " << scale.GetByIndices("scale_indices") << ";\n"; } @@ -62,43 +80,64 @@ Status DequantizeLinearProgram::GenerateShaderCode(ShaderHelper& shader) const { if (has_zeropoint_) { const auto& zero_point = shader.AddInput("zero_point", ShaderUsage::UseUniform | ShaderUsage::UseIndicesTypeAlias); - std::string unpack = (signed_) ? "unpack4xI8(zero_point_input)" : "unpack4xU8(zero_point_input)"; - if (per_layer_) { - // zero-point input is a scalar - if (packed_) { + if (packing_ == PackingMode::Packed4) { + // 4-bit zero-point: 8 elements per u32, with sign extension for signed types + std::string sign_extend_prefix = packed_signed_ ? "let zp_raw = " : "let zero_point_value = input_element_t("; + std::string sign_extend_suffix = packed_signed_ ? ";\nlet zero_point_value = select(input_element_t(zp_raw), input_element_t(zp_raw) - 16, zp_raw >= 8u);\n" + : ");\n"; + if (per_layer_) { shader.MainFunctionBody() - << "let zero_point_input = " << zero_point.GetByOffset("0") << ";\n" - << "let zero_point_vec = " << unpack << ";\n" - << "let zero_point_value = zero_point_vec[0];\n"; - } else { - shader.MainFunctionBody() - << "let zero_point_value = " << zero_point.GetByOffset("0") << ";\n"; - } - } else if (per_axis_) { - // zero-point input is a 1D tensor - if (packed_) { + << sign_extend_prefix << zero_point.GetByOffset("0") << " & 0xFu" << sign_extend_suffix; + } else if (per_axis_) { shader.MainFunctionBody() << "let zero_point_index = " << output.IndicesGet("output_indices", "uniforms.axis") << ";\n" - << "let zero_point_input = " << zero_point.GetByOffset("zero_point_index / 4") << ";\n" - << "let zero_point_vec = " << unpack << ";\n" - << "let zero_point_value = zero_point_vec[zero_point_index % 4];\n"; + << "let zero_point_packed = " << zero_point.GetByOffset("zero_point_index / 8") << ";\n" + << sign_extend_prefix << "(zero_point_packed >> ((zero_point_index % 8u) * 4u)) & 0xFu" << sign_extend_suffix; } else { shader.MainFunctionBody() - << "let zero_point_index = " << output.IndicesGet("output_indices", "uniforms.axis") << ";\n" - << "let zero_point_value = " << zero_point.GetByOffset("zero_point_index") << ";\n"; + << "let zero_point_offset = " << scale.IndicesToOffset("scale_indices") << ";\n" + << "let zero_point_packed = " << zero_point.GetByOffset("zero_point_offset / 8") << ";\n" + << sign_extend_prefix << "(zero_point_packed >> ((zero_point_offset % 8u) * 4u)) & 0xFu" << sign_extend_suffix; } } else { - // BlockedQuantization. The zero-point input shape is the same as the scale input shape. - if (packed_) { - shader.MainFunctionBody() - << "let zero_point_offset = " << scale.IndicesToOffset("scale_indices") << ";\n" - << "let zero_point_input = " << zero_point.GetByOffset("zero_point_offset / 4") << ";\n" - << "let zero_point_vec = " << unpack << ";\n" - << "let zero_point_value = zero_point_vec[zero_point_offset % 4];\n"; + std::string unpack = (packed_signed_) ? "unpack4xI8(zero_point_input)" : "unpack4xU8(zero_point_input)"; + if (per_layer_) { + // zero-point input is a scalar + if (packing_ == PackingMode::Packed8) { + shader.MainFunctionBody() + << "let zero_point_input = " << zero_point.GetByOffset("0") << ";\n" + << "let zero_point_vec = " << unpack << ";\n" + << "let zero_point_value = zero_point_vec[0];\n"; + } else { + shader.MainFunctionBody() + << "let zero_point_value = " << zero_point.GetByOffset("0") << ";\n"; + } + } else if (per_axis_) { + // zero-point input is a 1D tensor + if (packing_ == PackingMode::Packed8) { + shader.MainFunctionBody() + << "let zero_point_index = " << output.IndicesGet("output_indices", "uniforms.axis") << ";\n" + << "let zero_point_input = " << zero_point.GetByOffset("zero_point_index / 4") << ";\n" + << "let zero_point_vec = " << unpack << ";\n" + << "let zero_point_value = zero_point_vec[zero_point_index % 4];\n"; + } else { + shader.MainFunctionBody() + << "let zero_point_index = " << output.IndicesGet("output_indices", "uniforms.axis") << ";\n" + << "let zero_point_value = " << zero_point.GetByOffset("zero_point_index") << ";\n"; + } } else { - shader.MainFunctionBody() - << "let zero_point_offset = " << scale.IndicesToOffset("scale_indices") << ";\n" - << "let zero_point_value = " << zero_point.GetByOffset("zero_point_offset") << ";\n"; + // BlockedQuantization. The zero-point input shape is the same as the scale input shape. + if (packing_ == PackingMode::Packed8) { + shader.MainFunctionBody() + << "let zero_point_offset = " << scale.IndicesToOffset("scale_indices") << ";\n" + << "let zero_point_input = " << zero_point.GetByOffset("zero_point_offset / 4") << ";\n" + << "let zero_point_vec = " << unpack << ";\n" + << "let zero_point_value = zero_point_vec[zero_point_offset % 4];\n"; + } else { + shader.MainFunctionBody() + << "let zero_point_offset = " << scale.IndicesToOffset("scale_indices") << ";\n" + << "let zero_point_value = " << zero_point.GetByOffset("zero_point_offset") << ";\n"; + } } } } else { @@ -122,11 +161,15 @@ Status DequantizeLinear::ComputeInternal(ComputeContext& context) const { auto* output_tensor = context.Output(0, x_shape); int64_t x_scale_rank = x_scale->Shape().NumDimensions(); - // Currently only INT8, UINT8, and INT32 are registered. auto x_type = x->GetElementType(); - bool packed = x_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8 || x_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8; - bool is_signed = x_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8; + PackingMode packing = (x_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT4 || x_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT4) + ? PackingMode::Packed4 + : (x_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8 || x_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8) + ? PackingMode::Packed8 + : PackingMode::None; + bool packed = packing != PackingMode::None; + bool is_packed_signed = x_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8 || x_type == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT4; int64_t axis = (axis_ >= 0) ? axis_ : axis_ + x_shape.NumDimensions(); int max_components = GetMaxComponents(x_size); @@ -137,26 +180,80 @@ Status DequantizeLinear::ComputeInternal(ComputeContext& context) const { // 1D tensor - 1 scaler for per axis bool per_axis = per_layer == false && x_scale_rank == 1; - bool use_components = per_layer && (!packed || max_components == 4); + // Compute effective block_size. When block_size_ is 0 (default) but scale is 1D with + // fewer elements than the input dimension on the axis, infer block_size from the ratio. + int64_t block_size = block_size_; + if (per_axis && block_size == 0) { + int64_t input_dim = x_shape[onnxruntime::narrow(axis)]; + int64_t scale_dim = x_scale->Shape()[0]; + if (scale_dim < input_dim) { + block_size = input_dim / scale_dim; + per_axis = false; // treat as block quantization + } + } + + // When scale is N-D (block quantization) and block_size is 0, infer axis and block_size + // from the shapes. Find the dimension where scale is smaller than input to determine axis, + // then compute block_size from the ratio. + if (!per_layer && !per_axis && block_size == 0) { + const auto& scale_shape = x_scale->Shape(); + for (size_t i = 0; i < x_shape.NumDimensions(); i++) { + if (scale_shape[i] < x_shape[i]) { + axis = static_cast(i); + block_size = x_shape[i] / scale_shape[i]; + break; + } + } + if (block_size == 0) { + block_size = 1; // all dims match, default to block_size=1 + } + } + + // Validate shapes for blocked quantization. + if (!per_layer && !per_axis && block_size > 0) { + const auto& scale_shape = x_scale->Shape(); + ORT_RETURN_IF(scale_shape.NumDimensions() != x_shape.NumDimensions(), + "x_scale and x must have the same rank for blocked quantization"); + for (size_t i = 0; i < x_shape.NumDimensions(); i++) { + if (static_cast(i) == axis) { + ORT_RETURN_IF(scale_shape[i] != (x_shape[i] + block_size - 1) / block_size, + "x_scale must be ceil(Di/block_size) on the quantize axis i for blocked quantization"); + } else { + ORT_RETURN_IF(scale_shape[i] != x_shape[i], + "x_scale and x must have the same shape on non-quantize axes for blocked quantization"); + } + } + if (x_zeropoint != nullptr) { + for (size_t i = 0; i < x_shape.NumDimensions(); i++) { + ORT_RETURN_IF(x_zeropoint->Shape()[i] != scale_shape[i], + "x_zero_point and x_scale must have the same shape for blocked quantization"); + } + } + } + + bool use_components = per_layer && packing != PackingMode::Packed4 && (!packed || max_components == 4); int components = use_components ? max_components : 1; int input_component = use_components ? max_components : 1; + // For 4-bit types, each u32 holds 8 elements; for 8-bit types, 4 elements. + int pack_factor = (packing == PackingMode::Packed4) ? 8 : 4; - DequantizeLinearProgram program{packed, is_signed, per_layer, per_axis, x_zeropoint != nullptr}; + DequantizeLinearProgram program{packing, is_packed_signed, per_layer, per_axis, x_zeropoint != nullptr, + static_cast(x_shape.NumDimensions())}; program - .AddInputs({{x, ProgramTensorMetadataDependency::TypeAndRank, ProgramInput::Flatten, packed ? 4 : input_component}}) + .AddInputs({{x, ProgramTensorMetadataDependency::TypeAndRank, ProgramInput::Flatten, packed ? pack_factor : input_component}}) .AddInputs({{x_scale, ProgramTensorMetadataDependency::TypeAndRank}}) .AddOutput(use_components ? ProgramOutput{output_tensor, ProgramTensorMetadataDependency::Rank, ProgramOutput::Flatten, components} : ProgramOutput{output_tensor, ProgramTensorMetadataDependency::Rank, components}) .SetDispatchGroupSize((x_size / components + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE) .AddUniformVariables({{static_cast(axis)}}) - .AddUniformVariables({{static_cast(block_size_)}}) + .AddUniformVariables({{static_cast(block_size)}}) .AddUniformVariables({{static_cast(x_size / components)}}) - .CacheHint(std::to_string(axis), std::to_string(is_signed), std::to_string(per_layer), std::to_string(per_axis), std::to_string(block_size_)); + .CacheHint(std::to_string(axis), std::to_string(is_packed_signed), std::to_string(per_layer), std::to_string(per_axis), std::to_string(block_size), std::to_string(static_cast(packing))); if (x_zeropoint != nullptr) { - program.AddInputs({{x_zeropoint, ProgramTensorMetadataDependency::None, ProgramInput::Flatten, packed ? 4 : 1}}); + program.AddInputs({{x_zeropoint, ProgramTensorMetadataDependency::None, ProgramInput::Flatten, packed ? pack_factor : 1}}); } return context.RunProgram(program); @@ -167,7 +264,9 @@ const std::vector& DequantizeLinearConstraints() { static std::vector types{ DataTypeImpl::GetTensorType(), DataTypeImpl::GetTensorType(), - DataTypeImpl::GetTensorType()}; + DataTypeImpl::GetTensorType(), + DataTypeImpl::GetTensorType(), + DataTypeImpl::GetTensorType()}; return types; } } // namespace diff --git a/onnxruntime/core/providers/webgpu/quantization/quantize_linear.h b/onnxruntime/core/providers/webgpu/quantization/quantize_linear.h index 95614998017e9..31484ac040d85 100644 --- a/onnxruntime/core/providers/webgpu/quantization/quantize_linear.h +++ b/onnxruntime/core/providers/webgpu/quantization/quantize_linear.h @@ -8,15 +8,24 @@ namespace onnxruntime { namespace webgpu { +// How the quantized input is packed into u32 words. +enum class PackingMode { + None, // no packing (e.g. int32) + Packed8, // 8-bit: 4 elements per u32, uses unpack4x[I/U]8 + Packed4, // 4-bit: 8 elements per u32, manual bit extraction +}; + class DequantizeLinearProgram final : public Program { public: - DequantizeLinearProgram(const bool packed, const bool issigned, const bool per_layer, - const bool per_axis, bool has_zeropoint) : Program{"DequantizeLinear"}, - packed_{packed}, - signed_{issigned}, - per_layer_{per_layer}, - per_axis_{per_axis}, - has_zeropoint_{has_zeropoint} {} + DequantizeLinearProgram(PackingMode packing, bool is_packed_signed, bool per_layer, + bool per_axis, bool has_zeropoint, int rank = 0) + : Program{"DequantizeLinear"}, + packing_{packing}, + packed_signed_{is_packed_signed}, + per_layer_{per_layer}, + per_axis_{per_axis}, + has_zeropoint_{has_zeropoint}, + rank_{rank} {} Status GenerateShaderCode(ShaderHelper& sh) const override; @@ -25,11 +34,12 @@ class DequantizeLinearProgram final : public Program { {"output_size", ProgramUniformVariableDataType::Uint32}); private: - bool packed_; - bool signed_; + PackingMode packing_; + bool packed_signed_; bool per_layer_; bool per_axis_; bool has_zeropoint_; + int rank_; }; class DequantizeLinear final : public WebGpuKernel { @@ -38,6 +48,7 @@ class DequantizeLinear final : public WebGpuKernel { axis_ = info.GetAttrOrDefault("axis", 1); block_size_ = info.GetAttrOrDefault("block_size", 0); output_dtype_ = info.GetAttrOrDefault("output_dtype", 0); + ORT_ENFORCE(block_size_ >= 0, "'block_size' must be non-negative."); } Status ComputeInternal(ComputeContext& context) const override; diff --git a/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc b/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc index 1672433aaac3a..543975ea84612 100644 --- a/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc +++ b/onnxruntime/test/providers/cpu/tensor/quantize_linear_test.cc @@ -1392,6 +1392,11 @@ void DequantizeLinearOp21BlockedTest_InvalidBlockSize_Int(int64_t block_size, SessionOptions so; std::vector log_msgs; // redirect error messages std::vector> eps; + auto webgpu_ep = DefaultWebGpuExecutionProvider(); + if (webgpu_ep) { + eps.push_back(std::move(webgpu_ep)); + } + eps.push_back(DefaultCpuExecutionProvider()); so.user_logging_function = [](void* param, OrtLoggingLevel severity, const char* category, const char* logid, const char* code_location, const char* message) { @@ -1437,6 +1442,12 @@ void DequantizeLinearOp21BlockedTest_InvalidBlockSize_Int4(int64_t block_size, SessionOptions so; std::vector log_msgs; // redirect error messages std::vector> eps; + if (!ep) { + auto webgpu_ep = DefaultWebGpuExecutionProvider(); + if (webgpu_ep) { + eps.push_back(std::move(webgpu_ep)); + } + } eps.push_back(ep ? std::move(ep) : DefaultCpuExecutionProvider()); so.user_logging_function = [](void* param, OrtLoggingLevel severity, const char* category, const char* logid, const char* code_location, const char* message) { @@ -1482,6 +1493,10 @@ void DequantizeLinearOp21BlockedTest_InvalidBlockSize_Float8(int64_t block_size, SessionOptions so; std::vector log_msgs; // redirect error messages std::vector> eps; + auto webgpu_ep = DefaultWebGpuExecutionProvider(); + if (webgpu_ep) { + eps.push_back(std::move(webgpu_ep)); + } eps.push_back(DefaultCpuExecutionProvider()); so.user_logging_function = [](void* param, OrtLoggingLevel severity, const char* category, const char* logid, const char* code_location, const char* message) { @@ -1670,7 +1685,14 @@ void DequantizeLinearOp21BlockedTest_Int4_Succeed(std::vector&& dims, std::vector x_scale, y; std::vector x, x_zero_point; std::vector> eps; + if (!ep) { + auto webgpu_ep = DefaultWebGpuExecutionProvider(); + if (webgpu_ep) { + eps.push_back(std::move(webgpu_ep)); + } + } eps.push_back(ep ? std::move(ep) : DefaultCpuExecutionProvider()); + int64_t non_neg_axis = axis < 0 ? axis + dims.size() : axis; bool use_zero_point = !x_zero_point_.empty(); @@ -1714,6 +1736,10 @@ void DequantizeLinearOp21BlockedTest_Int_Succeed(std::vector&& dims, std::vector x_scale, y; std::vector x, x_zero_point; std::vector> eps; + auto webgpu_ep = DefaultWebGpuExecutionProvider(); + if (webgpu_ep) { + eps.push_back(std::move(webgpu_ep)); + } eps.push_back(DefaultCpuExecutionProvider()); int64_t non_neg_axis = axis < 0 ? axis + dims.size() : axis; @@ -1750,6 +1776,10 @@ void DequantizeLinearOp21BlockedTest_Float8_Succeed(std::vector&& dims, std::vector x_scale, y; std::vector x, x_zero_point; std::vector> eps; + auto webgpu_ep = DefaultWebGpuExecutionProvider(); + if (webgpu_ep) { + eps.push_back(std::move(webgpu_ep)); + } eps.push_back(DefaultCpuExecutionProvider()); int64_t non_neg_axis = axis < 0 ? axis + dims.size() : axis;