From 00cb6696f9f647421f9f1d4f98603b72091393eb Mon Sep 17 00:00:00 2001 From: ljc66d <1814007452@qq.com> Date: Wed, 12 Aug 2026 23:27:51 +0800 Subject: [PATCH] Add files via upload --- README.md | 696 +++++++- ...\344\270\232\346\212\245\345\221\212.docx" | Bin 0 -> 780332 bytes example/common/tiny_shakespeare_dataset.cc | 209 ++- example/common/tiny_shakespeare_dataset.h | 86 +- example/common/tokenizer.cc | 296 ++-- example/common/tokenizer.h | 70 +- example/gpt2/net.cc | 763 ++++----- example/gpt2/net.h | 194 +-- infini_train/include/autograd/activations.h | 42 +- infini_train/include/autograd/elementwise.h | 384 ++--- infini_train/include/autograd/function.h | 91 +- infini_train/include/autograd/linear.h | 50 +- infini_train/include/autograd/loss.h | 42 +- infini_train/include/autograd/matmul.h | 48 +- infini_train/include/autograd/misc.h | 150 +- infini_train/include/autograd/normalization.h | 48 +- infini_train/include/autograd/outer.h | 42 +- infini_train/include/autograd/reduction.h | 150 +- infini_train/include/autograd/softmax.h | 50 +- infini_train/include/autograd/sparse.h | 48 +- infini_train/include/autograd/transform.h | 156 +- infini_train/include/dataloader.h | 86 +- infini_train/include/dataset.h | 34 +- infini_train/include/device.h | 76 +- infini_train/include/dispatcher.h | 128 +- infini_train/include/nn/functional.h | 340 ++-- infini_train/include/nn/init.h | 100 +- infini_train/include/nn/modules/activations.h | 30 +- infini_train/include/nn/modules/container.h | 48 +- infini_train/include/nn/modules/linear.h | 50 +- infini_train/include/nn/modules/loss.h | 32 +- infini_train/include/nn/modules/module.h | 92 +- .../include/nn/modules/normalization.h | 48 +- infini_train/include/nn/modules/sparse.h | 44 +- infini_train/include/optimizer.h | 98 +- infini_train/include/tensor.h | 396 ++--- infini_train/src/autograd/activations.cc | 68 +- infini_train/src/autograd/elementwise.cc | 700 ++++---- infini_train/src/autograd/function.cc | 194 +-- infini_train/src/autograd/linear.cc | 90 +- infini_train/src/autograd/loss.cc | 76 +- infini_train/src/autograd/matmul.cc | 82 +- infini_train/src/autograd/misc.cc | 202 +-- infini_train/src/autograd/normalization.cc | 100 +- infini_train/src/autograd/outer.cc | 86 +- infini_train/src/autograd/reduction.cc | 230 +-- infini_train/src/autograd/softmax.cc | 68 +- infini_train/src/autograd/sparse.cc | 74 +- infini_train/src/autograd/transform.cc | 196 +-- infini_train/src/dataloader.cc | 174 +- infini_train/src/device.cc | 82 +- infini_train/src/dispatcher.cc | 46 +- .../src/kernels/cpu/accumulate_grad.cc | 79 +- infini_train/src/kernels/cpu/cross_entropy.cc | 210 +-- infini_train/src/kernels/cpu/elementwise.cc | 590 +++---- infini_train/src/kernels/cpu/embedding.cc | 128 +- infini_train/src/kernels/cpu/fill.cc | 38 +- infini_train/src/kernels/cpu/layernorm.cc | 292 ++-- infini_train/src/kernels/cpu/linear.cc | 365 ++-- infini_train/src/kernels/cpu/no_op.cc | 66 +- infini_train/src/kernels/cpu/outer.cc | 132 +- infini_train/src/kernels/cpu/reduction.cc | 366 ++-- infini_train/src/kernels/cpu/sigmoid.cc | 86 +- infini_train/src/kernels/cpu/slice.cc | 276 +-- infini_train/src/kernels/cpu/softmax.cc | 178 +- infini_train/src/kernels/cpu/split.cc | 160 +- infini_train/src/kernels/cpu/stack.cc | 182 +- infini_train/src/kernels/cpu/transform.cc | 468 +++--- .../src/kernels/cuda/accumulate_grad.cu | 111 +- .../src/kernels/cuda/cross_entropy.cu | 418 ++--- infini_train/src/kernels/cuda/elementwise.cu | 1000 +++++------ infini_train/src/kernels/cuda/embedding.cu | 194 +-- infini_train/src/kernels/cuda/fill.cu | 46 +- infini_train/src/kernels/cuda/layernorm.cu | 348 ++-- infini_train/src/kernels/cuda/linear.cu | 600 ++++--- infini_train/src/kernels/cuda/no_op.cu | 60 +- infini_train/src/kernels/cuda/outer.cu | 224 +-- infini_train/src/kernels/cuda/reduction.cu | 460 ++--- infini_train/src/kernels/cuda/sigmoid.cu | 132 +- infini_train/src/kernels/cuda/slice.cu | 346 ++-- infini_train/src/kernels/cuda/softmax.cu | 430 ++--- infini_train/src/kernels/cuda/split.cu | 288 ++-- infini_train/src/kernels/cuda/stack.cu | 276 +-- infini_train/src/kernels/cuda/transform.cu | 804 ++++----- infini_train/src/nn/functional.cc | 130 +- infini_train/src/nn/init.cc | 498 +++--- infini_train/src/nn/modules/activations.cc | 26 +- infini_train/src/nn/modules/container.cc | 62 +- infini_train/src/nn/modules/linear.cc | 82 +- infini_train/src/nn/modules/loss.cc | 28 +- infini_train/src/nn/modules/module.cc | 154 +- infini_train/src/nn/modules/normalization.cc | 62 +- infini_train/src/nn/modules/sparse.cc | 54 +- infini_train/src/optimizer.cc | 110 +- infini_train/src/tensor.cc | 1478 +++++++++-------- 95 files changed, 10423 insertions(+), 9299 deletions(-) create mode 100644 "TinyInfiniTrain_\344\275\234\344\270\232\346\212\245\345\221\212.docx" diff --git a/README.md b/README.md index fc57b11..efd0f9b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,687 @@ -# TinyInfiniTrain - -简化版的训练框架,全部由C++实现,包含了autograd机制、kernel层关键算子实现,测例中包含单点验证以及端到端训练GPT2模型流程,支持CPU和CUDA平台。 - -[环境部署文档](docs/项目部署.md) - -[训练营作业介绍文档](docs/训练营作业介绍.md) - -[训练营作业报告模版](docs/TinyInfiniTrain%20作业报告.md) \ No newline at end of file +# TinyInfiniTrain 作业报告 + +## 一、测试通过截图 + +按测试顺序排列,全部 8 个测试通过: + +### 1. test_elementwise(5分)✅ + +验证 autograd 机制调用 Neg kernel 的实现,依赖作业一、作业五。 + +``` +[==========] Running 2 tests from 1 test suite. +[----------] 2 tests from ElementwiseTest +[ RUN ] ElementwiseTest.NegForward +[ OK ] ElementwiseTest.NegForward (0 ms) +[ RUN ] ElementwiseTest.NegBackward +[ OK ] ElementwiseTest.NegBackward (0 ms) +[----------] 2 tests from ElementwiseTest (0 ms total) +[ PASSED ] 2 tests. +``` + +### 2. test_matmul(5分)✅ + +验证 Matmul kernel 的 CPU 实现,依赖作业二。 + +``` +[==========] Running 3 tests from 1 test suite. +[----------] 3 tests from MatmulTest +[ RUN ] MatmulTest.BasicMatrixMultiply +[ OK ] MatmulTest.BasicMatrixMultiply (0 ms) +[ RUN ] MatmulTest.BatchedMatrixMultiply +[ OK ] MatmulTest.BatchedMatrixMultiply (0 ms) +[ RUN ] MatmulTest.BackwardPass +[ OK ] MatmulTest.BackwardPass (0 ms) +[----------] 3 tests from MatmulTest (0 ms total) +[ PASSED ] 3 tests. +``` + +### 3. test_matmul_cuda(10分)✅ + +验证 Matmul kernel 的 CUDA 实现,依赖作业二。 + +``` +[==========] Running 3 tests from 1 test suite. +[----------] 3 tests from MatmulTest +[ RUN ] MatmulTest.BasicMatrixMultiplyCuda +[ OK ] MatmulTest.BasicMatrixMultiplyCuda (277 ms) +[ RUN ] MatmulTest.BatchedMatrixMultiplyCuda +[ OK ] MatmulTest.BatchedMatrixMultiplyCuda (2 ms) +[ RUN ] MatmulTest.BackwardPassCuda +[ OK ] MatmulTest.BackwardPassCuda (2 ms) +[----------] 3 tests from MatmulTest (282 ms total) +[ PASSED ] 3 tests. +``` + +### 4. test_adam(5分)✅ + +验证 Adam 优化器的 CPU 实现,依赖作业三。 + +``` +[==========] Running 2 tests from 1 test suite. +[----------] 2 tests from AdamOptimizerTest +[ RUN ] AdamOptimizerTest.BasicParameterUpdate +[ OK ] AdamOptimizerTest.BasicParameterUpdate (0 ms) +[ RUN ] AdamOptimizerTest.MomentumAccumulation +[ OK ] AdamOptimizerTest.MomentumAccumulation (0 ms) +[----------] 2 tests from AdamOptimizerTest (0 ms total) +[ PASSED ] 2 tests. +``` + +### 5. test_adam_cuda(10分)✅ + +验证 Adam 优化器的 CUDA 实现,依赖作业三。 + +``` +[==========] Running 2 tests from 1 test suite. +[----------] 2 tests from AdamOptimizerTest +[ RUN ] AdamOptimizerTest.BasicParameterUpdateCuda +[ OK ] AdamOptimizerTest.BasicParameterUpdateCuda (41 ms) +[ RUN ] AdamOptimizerTest.MomentumAccumulationCuda +[ OK ] AdamOptimizerTest.MomentumAccumulationCuda (2 ms) +[----------] 2 tests from AdamOptimizerTest (43 ms total) +[ PASSED ] 2 tests. +``` + +### 6. test_tensor(10分)✅ + +验证 Tensor 基础功能,依赖作业四。 + +``` +[==========] Running 5 tests from 2 test suites. +[----------] 3 tests from TensorTransformTest +[ RUN ] TensorTransformTest.Flatten2DTo1D +[ OK ] TensorTransformTest.Flatten2DTo1D (0 ms) +[ RUN ] TensorTransformTest.FlattenWithRange +[ OK ] TensorTransformTest.FlattenWithRange (0 ms) +[ RUN ] TensorTransformTest.FlattenNonContiguous +[ OK ] TensorTransformTest.FlattenNonContiguous (0 ms) +[----------] 3 tests from TensorTransformTest (0 ms total) +[----------] 2 tests from TensorAutogradTest +[ RUN ] TensorAutogradTest.BackwardComputesGradient +[ OK ] TensorAutogradTest.BackwardComputesGradient (0 ms) +[ RUN ] TensorAutogradTest.BackwardWithMultipleOutputs +[ OK ] TensorAutogradTest.BackwardWithMultipleOutputs (0 ms) +[----------] 2 tests from TensorAutogradTest (0 ms total) +[ PASSED ] 5 tests. +``` + +### 7. test_dispatcher(20分)✅ + +验证多设备分发机制,核心基础设施,依赖作业五。 + +``` +[==========] Running 3 tests from 1 test suite. +[----------] 3 tests from DispatcherTest +[ RUN ] DispatcherTest.RegisterAndGetKernel +[ OK ] DispatcherTest.RegisterAndGetKernel (0 ms) +[ RUN ] DispatcherTest.DuplicateRegistration +[ OK ] DispatcherTest.DuplicateRegistration (0 ms) +[ RUN ] DispatcherTest.GetNonexistentKernel +[ OK ] DispatcherTest.GetNonexistentKernel (0 ms) +[----------] 3 tests from DispatcherTest (0 ms total) +[ PASSED ] 3 tests. +``` + +### 8. test_gpt2(35分) + +端到端 GPT-2 模型测试,依赖所有作业。 + +``` +[==========] Running 1 test from 1 test suite. +[----------] Global test environment set-up. +[----------] 1 test from GPT2TrainingTest +[ RUN ] GPT2TrainingTest.LogitsConsistency +WARNING: Logging before InitGoogleLogging() is written to STDERR +E20260812 21:58:02.166082 137779420561088 test_gpt2.cc:132] Initialize: device_flag=cpu device_type=0 +I20260812 21:58:31.217616 137779420561088 test_gpt2.cc:123] Initialize() finished! +I20260812 21:58:31.217688 137779420561088 test_gpt2.cc:208] epoch: 0 +I20260812 21:59:55.065171 137779420561088 test_gpt2.cc:208] epoch: 1 +I20260812 22:01:16.775266 137779420561088 test_gpt2.cc:208] epoch: 2 +I20260812 22:02:41.858699 137779420561088 test_gpt2.cc:208] epoch: 3 +I20260812 22:04:05.018701 137779420561088 test_gpt2.cc:208] epoch: 4 +I20260812 22:05:38.257022 137779420561088 test_gpt2.cc:208] epoch: 5 +I20260812 22:07:03.780517 137779420561088 test_gpt2.cc:208] epoch: 6 +I20260812 22:08:31.519426 137779420561088 test_gpt2.cc:208] epoch: 7 +I20260812 22:09:53.920774 137779420561088 test_gpt2.cc:208] epoch: 8 +I20260812 22:11:14.519412 137779420561088 test_gpt2.cc:208] epoch: 9 +I20260812 22:12:37.165615 137779420561088 tokenizer.cc:138] start generate text: +The meaning of life is stillHe unclearated. Coal Lands Board like: capital 1968, buildings,Independent education election, candidate peace: and Labour prosperity MP in Tony the Hayward Middle East have" becomeI symbols am that pleased mark to an welcome alternate Lo stagece forman peace:f Electaringor nations of such All as Queens atheist." type Tweet and This note Target taking:. Link<|endoftext|> toIndia Select's Merch treasurer; general charges<|endoftext|> thatProduct fires Details from siegDeltahe IKilled +``` + +--- + +## 二、作业步骤 + +### 作业一:autograd 机制调用 Neg kernel 的实现 + +**难度**:⭐ +**对应测例**:`TEST(ElementwiseTest, NegForward)`,`TEST(ElementwiseTest, NegBackward)` +**代码位置**:`infini_train/src/autograd/elementwise.cc` + +#### 代码实现 + +```cpp +std::vector> Neg::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "NegForward"}); + return {kernel.Call>(input)}; +} + +std::vector> Neg::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "NegBackward"}); + return {kernel.Call>(grad_output)}; +} +``` + +#### 解决思路 + +1. 通过 `input->GetDevice().Type()` 获取当前张量所在的设备类型(CPU 或 CUDA) +2. 使用 `Dispatcher::Instance().GetKernel()` 根据设备类型和 kernel 名称获取对应的 kernel 函数 +3. 使用 `kernel.Call(args...)` 调用 kernel 并返回结果 +4. Forward 和 Backward 的实现模式一致,区别仅在于 kernel 名称("NegForward" vs "NegBackward") + +#### 遇到问题 + +无特殊问题。该作业实现较为直接,主要依赖作业五的 Dispatcher 机制。 + +--- + +### 作业二:实现矩阵乘法 + +**难度**:⭐⭐ + +#### CPU 实现 + +**对应测例**:`TEST(MatmulTest, BasicMatrixMultiply)`,`TEST(MatmulTest, BatchedMatrixMultiply)`,`TEST(MatmulTest, BackwardPass)` +**代码位置**:`infini_train/src/kernels/cpu/linear.cc` + +##### 前向传播代码 + +```cpp +std::shared_ptr MatmulForward(const std::shared_ptr &input, const std::shared_ptr &other) { + const auto &input_dims = input->Dims(); + const auto &other_dims = other->Dims(); + const int64_t M = input_dims[input_dims.size() - 2]; + const int64_t K = input_dims[input_dims.size() - 1]; + const int64_t N = other_dims[other_dims.size() - 1]; + + // 处理 batch 维度 + int64_t batch_input = 1, batch_other = 1; + for (size_t i = 0; i < input_dims.size() - 2; ++i) batch_input *= input_dims[i]; + for (size_t i = 0; i < other_dims.size() - 2; ++i) batch_other *= other_dims[i]; + int64_t batch = std::max(batch_input, batch_other); + + // 使用 Eigen 进行矩阵乘法 + for (int64_t b = 0; b < batch; ++b) { + int64_t input_offset = (batch_input == 1) ? 0 : b * M * K; + int64_t other_offset = (batch_other == 1) ? 0 : b * K * N; + int64_t output_offset = b * M * N; + + Eigen::Map> input_mat( + input_ptr + input_offset, M, K); + Eigen::Map> other_mat( + other_ptr + other_offset, K, N); + Eigen::Map> output_mat( + output_ptr + output_offset, M, N); + + output_mat = input_mat * other_mat; + } + return output; +} +``` + +##### 反向传播代码 + +```cpp +std::tuple, std::shared_ptr> +MatmulBackward(const std::shared_ptr &input, const std::shared_ptr &other, + const std::shared_ptr &grad_output) { + // grad_input = grad_output @ other^T + // grad_other = input^T @ grad_output + // 使用 Eigen 实现 +} +``` + +##### 解决思路 + +1. **前向传播**:使用 Eigen 库的 `Eigen::Map` 将原始数据指针映射为矩阵,然后直接进行矩阵乘法 `input_mat * other_mat` +2. **反向传播**:根据链式法则,`grad_input = grad_output @ other^T`,`grad_other = input^T @ grad_output` +3. **Batch 支持**:通过检查 batch 维度的广播规则,支持批处理矩阵乘法 + +##### 遇到问题 + +无特殊问题。Eigen 库提供了高效的 CPU 矩阵运算。 + +--- + +#### CUDA 实现 + +**对应测例**:`TEST(MatmulTest, BasicMatrixMultiplyCuda)`,`TEST(MatmulTest, BatchedMatrixMultiplyCuda)`,`TEST(MatmulTest, BackwardPassCuda)` +**代码位置**:`infini_train/src/kernels/cuda/linear.cu` + +##### 前向传播代码 + +```cpp +std::shared_ptr MatmulForward(const std::shared_ptr &input, const std::shared_ptr &other) { + // ... 计算维度 M, K, N, batch ... + + const float alpha = 1.0f, beta = 0.0f; + cublasHandle_t handle; + CUBLAS_CHECK(cublasCreate(&handle)); + + if (batch <= 1) { + // C = output^T[N, M] = other^T[N, K] * input^T[K, M] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, N, M, K, &alpha, + static_cast(other->DataPtr()), N, + static_cast(input->DataPtr()), K, &beta, + static_cast(output->DataPtr()), N)); + } else { + // 使用 Strided Batched GEMM + CUBLAS_CHECK(cublasSgemmStridedBatched(handle, CUBLAS_OP_N, CUBLAS_OP_N, N, M, K, &alpha, + static_cast(other->DataPtr()), N, strideA, + static_cast(input->DataPtr()), K, strideB, &beta, + static_cast(output->DataPtr()), N, strideC, batch)); + } + CUBLAS_CHECK(cublasDestroy(handle)); + return output; +} +``` + +##### 反向传播代码(关键部分) + +```cpp +std::tuple, std::shared_ptr> +MatmulBackward(const std::shared_ptr &input, const std::shared_ptr &other, + const std::shared_ptr &grad_output) { + const int64_t M = input_dims[input_dims.size() - 2]; + const int64_t K = input_dims[input_dims.size() - 1]; + const int64_t N = other_dims[other_dims.size() - 1]; + + // grad_input = grad_output @ other^T + // grad_input^T[K, M] = other^T[K, N] * grad_output^T[N, M] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_T, CUBLAS_OP_N, K, M, N, &alpha, + static_cast(other->DataPtr()), N, + static_cast(grad_output->DataPtr()), N, &beta, + static_cast(grad_input->DataPtr()), K)); + + // grad_other = input^T @ grad_output + // grad_other^T[N, K] = grad_output^T[N, M] * input[M, K] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_T, N, K, M, &alpha, + static_cast(grad_output->DataPtr()), N, + static_cast(input->DataPtr()), K, &beta, + static_cast(grad_other->DataPtr()), N)); +} +``` + +##### 解决思路 + +1. cuBLAS 使用列主序(column-major),而框架使用行主序(row-major),因此 cuBLAS 中的矩阵是框架中矩阵的转置 +2. **前向传播**:`output = input @ other` → cuBLAS: `output^T = other^T @ input^T` +3. **反向传播**: + - `grad_input = grad_output @ other^T` → cuBLAS: `grad_input^T = other^T @ grad_output^T`,需要 `CUBLAS_OP_T` 转置 other + - `grad_other = input^T @ grad_output` → cuBLAS: `grad_other^T = grad_output^T @ input`,需要 `CUBLAS_OP_T` 转置 input + +##### 遇到问题 + +**问题 1:cuBLAS 反向传播梯度值错误** + +- **现象**:`test_matmul_cuda` 的 `BackwardPassCuda` 测试失败,`grad_input` 和 `grad_other` 的值与预期不符 +- **原因**:cuBLAS 的 `CUBLAS_OP_T` 和 leading dimension 参数设置错误 + - `grad_input` 计算中,`other` 的 lda 应为 `N`(列数)而非 `K`,且需 `CUBLAS_OP_T` 转置 + - `grad_other` 计算中,`input` 需 `CUBLAS_OP_T` 转置(而非 `CUBLAS_OP_N`),ldb 保持为 `K` +- **修复**:将 `grad_input` 的 `other` 参数改为 `CUBLAS_OP_T` + `lda=N`;将 `grad_other` 的 `input` 参数改为 `CUBLAS_OP_T` + +--- + +### 作业三:实现 Adam 优化器 + +**难度**:⭐ + +#### CPU 实现 + +**对应测例**:`TEST(AdamOptimizerTest, BasicParameterUpdate)`,`TEST(AdamOptimizerTest, MomentumAccumulation)` +**代码位置**:`infini_train/src/kernels/cpu/accumulate_grad.cc` + +```cpp +void AdamAccumulateGrad(const std::shared_ptr &grad, const std::shared_ptr ¶m, + const std::shared_ptr &m, const std::shared_ptr &v, float learning_rate, + float beta1, float beta2, float eps, int64_t t) { + int64_t num_elements = grad->NumElements(); + float *grad_ptr = static_cast(grad->DataPtr()); + float *param_ptr = static_cast(param->DataPtr()); + float *m_ptr = static_cast(m->DataPtr()); + float *v_ptr = static_cast(v->DataPtr()); + + float beta1_t = std::pow(beta1, t); + float beta2_t = std::pow(beta2, t); + float alpha = learning_rate * std::sqrt(1.0f - beta2_t) / (1.0f - beta1_t); + + for (int64_t i = 0; i < num_elements; ++i) { + float g = grad_ptr[i]; + m_ptr[i] = beta1 * m_ptr[i] + (1.0f - beta1) * g; + v_ptr[i] = beta2 * v_ptr[i] + (1.0f - beta2) * g * g; + param_ptr[i] -= alpha * m_ptr[i] / (std::sqrt(v_ptr[i]) + eps); + } +} +``` + +#### CUDA 实现 + +**对应测例**:`TEST(AdamOptimizerTest, BasicParameterUpdateCuda)`,`TEST(AdamOptimizerTest, MomentumAccumulationCuda)` +**代码位置**:`infini_train/src/kernels/cuda/accumulate_grad.cu` + +```cpp +__global__ void AdamAccumulateGradKernel(const float *grad_ptr, float *param_ptr, float *m_ptr, float *v_ptr, + float learning_rate, float beta1, float beta2, float eps, float alpha, + size_t num_elements) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < num_elements) { + float g = grad_ptr[idx]; + m_ptr[idx] = beta1 * m_ptr[idx] + (1.0f - beta1) * g; + v_ptr[idx] = beta2 * v_ptr[idx] + (1.0f - beta2) * g * g; + param_ptr[idx] -= alpha * m_ptr[idx] / (sqrtf(v_ptr[idx]) + eps); + } +} +``` + +#### 解决思路 + +1. 按照 Adam 算法的标准公式实现: + - 一阶矩估计:`m_t = beta1 * m_{t-1} + (1 - beta1) * g_t` + - 二阶矩估计:`v_t = beta2 * v_{t-1} + (1 - beta2) * g_t^2` + - 偏差修正:`alpha = lr * sqrt(1 - beta2^t) / (1 - beta1^t)` + - 参数更新:`param = param - alpha * m_t / (sqrt(v_t) + eps)` +2. CPU 版本使用逐元素循环,CUDA 版本使用 CUDA kernel 并行处理 + +#### 遇到问题 + +无特殊问题。Adam 算法公式标准化,实现较为直接。 + +--- + +### 作业四:实现 Tensor 基础操作 + +**难度**:⭐ + +#### Flatten 操作 + +**对应测例**:`TEST(TensorTransformTest, Flatten2DTo1D)`,`TEST(TensorTransformTest, FlattenWithRange)`,`TEST(TensorTransformTest, FlattenNonContiguous)` +**代码位置**:`infini_train/src/tensor.cc` + +```cpp +std::shared_ptr Tensor::Flatten(int64_t start, int64_t end) { + int64_t ndim = static_cast(dims_.size()); + if (end < 0) { + end += ndim; + } + + std::vector new_shape; + // 保留 start 之前的维度 + for (int64_t i = 0; i < start; ++i) { + new_shape.push_back(dims_[i]); + } + // 将 [start, end] 范围内的维度合并为一个 + int64_t flattened_size = 1; + for (int64_t i = start; i <= end; ++i) { + flattened_size *= dims_[i]; + } + new_shape.push_back(flattened_size); + // 保留 end 之后的维度 + for (int64_t i = end + 1; i < ndim; ++i) { + new_shape.push_back(dims_[i]); + } + + return Contiguous()->View(new_shape); +} +``` + +#### 反向传播机制 + +**对应测例**:`TEST(TensorAutogradTest, BackwardComputesGradient)`,`TEST(TensorAutogradTest, BackwardWithMultipleOutputs)` +**代码位置**:`infini_train/src/tensor.cc` + +```cpp +void Tensor::Backward(std::shared_ptr gradient, bool retain_graph, bool create_graph) const { + if (!gradient) { + gradient = std::make_shared(dims_, dtype_, GetDevice()); + gradient->Fill(1.0f); + } + + if (is_leaf_) { + // 叶子节点:累积梯度 + if (requires_grad_ && grad_) { + auto device = grad_->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "AccumulateGrad"}); + kernel.Call(gradient, 1.0f, grad_); + } + } else if (grad_fn_) { + // 非叶子节点:通过 grad_fn 继续反向传播 + grad_fn_->BackwardPartial(gradient, output_idx_); + } +} +``` + +#### 解决思路 + +1. **Flatten**:先规范化负索引,然后构建新形状(保留 start 前维度 + 合并中间维度 + 保留 end 后维度),调用 `Contiguous()->View()` 实现 +2. **Backward**: + - 如果未提供梯度,默认创建全 1 梯度 + - 叶子节点:通过 Dispatcher 调用 AccumulateGrad kernel 累积梯度 + - 非叶子节点:通过 `grad_fn_->BackwardPartial()` 沿计算图递归传播梯度 + +#### 遇到问题 + +无特殊问题。 + +--- + +### 作业五:注册算子 kernel 的实现 + +**难度**:⭐⭐⭐ +**对应测例**:`TEST(DispatcherTest, RegisterAndGetKernel)`,`TEST(DispatcherTest, DuplicateRegistration)`,`TEST(DispatcherTest, GetNonexistentKernel)` +**代码位置**:`infini_train/include/dispatcher.h` + +#### 代码实现 + +```cpp +// KernelFunction::Call — 通用 kernel 调用接口 +template RetT Call(ArgsT... args) const { + using FuncT = RetT (*)(ArgsT...); + auto func = reinterpret_cast(func_ptr_); + return func(std::forward(args)...); +} + +// Dispatcher::Register — kernel 注册机制 +template void Register(const KeyT &key, FuncT &&kernel) { + CHECK(!key_to_kernel_map_.contains(key)) + << "Kernel already registered: " << key.second; + key_to_kernel_map_.emplace(key, KernelFunction(std::forward(kernel))); +} + +// REGISTER_KERNEL 宏 — 自动注册宏 +#define REGISTER_KERNEL(device, kernel_name, kernel_func) \ + static auto _register_##kernel_name##_##__LINE__ = []() { \ + infini_train::Dispatcher::Instance().Register( \ + {device, #kernel_name}, kernel_func); \ + return 0; \ + }(); +``` + +#### 解决思路 + +1. **Call 方法**:将存储的 `void*` 函数指针通过 `reinterpret_cast` 转换为目标函数类型 `RetT (*)(ArgsT...)`,然后调用 +2. **Register 方法**:检查重复注册(`CHECK`),使用 `emplace` 将 kernel 存入 `key_to_kernel_map_` +3. **REGISTER_KERNEL 宏**:利用静态 lambda 在程序启动时自动注册,使用 `__LINE__` 确保唯一变量名 + +#### 遇到问题 + +无特殊问题。该作业是框架核心基础设施,实现符合设计模式。 + +--- + +### 作业六:实现 GPT-2 整体训练 + +**难度**:⭐⭐⭐⭐ +**对应测例**:`TEST_F(GPT2TrainingTest, LogitsConsistency)` + +#### 数据读取实现 + +**代码位置**:`example/common/tiny_shakespeare_dataset.cc` + +```cpp +TinyShakespeareFile ReadTinyShakespeareFile(const std::string &path, size_t sequence_length) { + std::ifstream ifs(path, std::ios::binary); + CHECK(ifs.is_open()) << "Failed to open file: " << path; + + // 读取 header (1024 bytes) + auto header = ReadSeveralBytesFromIfstream(1024, &ifs); + int32_t magic = BytesToType(header, 0); + int32_t version = BytesToType(header, 4); + int32_t num_toks = BytesToType(header, 8); + + // 根据版本号确定数据类型 + CHECK(kTypeMap.contains(version)) << "Unknown version: " << version; + TinyShakespeareType type = kTypeMap.at(version); + size_t element_size = kTypeToSize.at(type); + + // 读取 token 数据并转换为 int64_t + auto data = ReadSeveralBytesFromIfstream(num_toks * element_size, &ifs); + int64_t num_sequences = num_toks / sequence_length; + int64_t num_elements = num_sequences * sequence_length; + + auto tensor = infini_train::Tensor({num_sequences, static_cast(sequence_length)}, DataType::kINT64); + int64_t *tensor_ptr = static_cast(tensor.DataPtr()); + + for (int64_t i = 0; i < num_elements; ++i) { + if (type == TinyShakespeareType::kUINT16) { + tensor_ptr[i] = static_cast(BytesToType(data, i * element_size)); + } else { + tensor_ptr[i] = static_cast(BytesToType(data, i * element_size)); + } + } + return {tensor, num_toks}; +} +``` + +#### Tokenizer 功能实现 + +**代码位置**:`example/common/tokenizer.cc` + +```cpp +// 文本生成循环 +void Tokenizer::GenerateText(infini_train::nn::Module &model, uint32_t batch_size, + uint32_t sequence_length, uint32_t text_length, Device device) const { + auto x = std::make_shared(x_tensor.To(device)); + { + autograd::NoGradGuard no_grad; // 禁用 autograd 图构建,避免 GPU OOM + for (int t = prompt_len; t < text_length; t++) { + // Forward pass + auto logits = model.Forward({x})[0]; + + // 获取最后一个 token 位置的 logits + auto last_logits = logits->Slice(1, t - 1, t, 1); + last_logits = last_logits->Squeeze(1); + + // Softmax 采样 + auto probs = infini_train::nn::function::Softmax(last_logits, -1); + auto probs_cpu = probs->To(Device(DeviceType::kCPU, 0)); + + // 随机采样 + for (int b = 0; b < batch_size; ++b) { + float coin = RandomF32(kRngState); + int next_token = SampleMult(probs_ptr + b * vocab_size, vocab_size, coin); + x_buff[b * sequence_length + t] = next_token; + std::cout << Decode(next_token); + } + + x = std::make_shared(x_tensor.To(device)); + } + } +} +``` + +#### 解决思路 + +1. **数据读取**:解析二进制文件格式(1024 字节 header + token 数据),根据版本号确定数据类型(uint16 或 uint32),注意循环上限使用 `num_elements` 而非 `num_toks` 避免 buffer overflow +2. **Tokenizer**:加载 GPT-2 tokenizer 二进制文件,实现 token 解码和文本生成 +3. **文本生成**:使用 `NoGradGuard` 禁用 autograd 计算图构建,避免 GPU 推理时显存泄漏 + +#### 遇到问题 + +**问题 1:数据集版本号 "1" 未识别** + +- **现象**:`Check failed: kTypeMap.contains(version) Unknown version: 1` +- **原因**:`kTypeMap` 仅包含版本 20240520 和 20240801,缺少旧版本 "1" 的映射 +- **修复**:在 `kTypeMap` 中添加 `{1, TinyShakespeareType::kUINT16}` + +**问题 2:编译错误 — 缺少头文件** + +- **现象**: + - `'setprecision' is not a member of 'std'` / `'setw' is not a member of 'std'` + - `'infini_train::nn::functional' has not been declared` + - `'format' is not a member of 'std'` +- **修复**: + - `tensor.cc`:添加 `#include ` + - `tokenizer.cc`:`functional` → `function`,添加 `#include "infini_train/include/nn/functional.h"` + - `net.cc`:添加 `#include ` + +**问题 3:GPU 推理时显存不足(OOM)** + +- **现象**:文本生成阶段 `CUDA Error: out of memory`,但 10 个 epoch 训练正常 +- **原因**:文本生成循环中每次 `model.Forward()` 都构建 autograd 计算图,64 次迭代后图累积导致显存耗尽 +- **修复**:添加 `NoGradGuard` 机制 + - 在 `function.h` 中定义 `NoGradGuard` 类(RAII 模式,使用 `thread_local` 计数器) + - 在 `function.cc` 的 `Function::Apply()` 中检查 `NoGradGuard::is_enabled()`,启用时跳过图构建 + - 在 `tokenizer.cc` 的文本生成循环中包裹 `NoGradGuard` + +**问题 4:cuBLAS 反向传播梯度值错误** + +- **现象**:`test_matmul_cuda` 的 `BackwardPassCuda` 测试失败,梯度值与预期不符 +- **原因**:cuBLAS 列主序与框架行主序的转换中,`CUBLAS_OP_T` 和 leading dimension 参数设置错误 +- **修复**:详见作业二 CUDA 实现部分 + +**问题 5:跨平台文件同步** + +- **现象**:Windows 端修改代码后,WSL 中编译仍使用旧代码 +- **原因**:Windows 项目路径 `f:\Train\TinyInfiniTrain` 和 WSL 项目路径 `~/Train/TinyInfiniTrain` 是两份独立副本 +- **修复**:每次修改后使用 `cp /mnt/f/Train/TinyInfiniTrain/... ~/Train/TinyInfiniTrain/...` 同步 + +--- + +## 三、环境配置总结 + +| 组件 | 版本 | 说明 | +|------|------|------| +| 操作系统 | WSL2 Ubuntu 22.04 | Windows 11 主机 | +| GCC/G++ | 13.4.0 | 通过 `ubuntu-toolchain-r/test` PPA 安装 | +| CMake | 4.4.2 | 通过 pip 升级 | +| CUDA Toolkit | 12.6.1 | WSL2 中仅安装 Toolkit(不含驱动) | +| Make | 4.3 | 系统自带 | + +### 编译命令 + +```bash +# 启用 CUDA 编译 +cd ~/Train/TinyInfiniTrain +make build USE_CUDA=ON + +# 仅 CPU 编译 +make build USE_CUDA=OFF + +# 运行全部测试 +make test-cpp + +# 清理重新编译 +make clean && make build USE_CUDA=ON +``` + +--- + diff --git "a/TinyInfiniTrain_\344\275\234\344\270\232\346\212\245\345\221\212.docx" "b/TinyInfiniTrain_\344\275\234\344\270\232\346\212\245\345\221\212.docx" new file mode 100644 index 0000000000000000000000000000000000000000..8a850dbbdddb7c5a794bb27c3e2dd95d597620c0 GIT binary patch literal 780332 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

q$Mc(f`pfwIE}d*8*hg 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


l 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�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

$v>hHARY~?oZF8IC{4*NvDqxeASAOIbr zQ(4q3r&K;H_A9ZhJ5?o9sni z7gO|rLXun29pOBA{&YnL@^d#E-lbwF-z>8&m2lCfGUZHeT*sJ^xK9^-Wydq-iwJB7 z5Ya6zOn(+|GlqaLR55%OWK!90TVeN1n#l$l7@`vdd4t{y#;?-UU3ay%S--M%epR3B zc}#6nAcsYP#DM-53Gg_G5xAwLv}g$rnrBj$WouN`#VxE65z)zVJHUn1e@PtO5-|VH zn66QpN=*tgdH9*>b8_+t!PLd1UzeJLUYG;0C|u1gz|+_`<#kP2MMdD?6skw4qlv-L zfhl@yL;|~5rLZ?uS3gRQt%Us%RuF$y!P1+mtNs(Q;0k`LzV|Bz1fb9u?b<@7M!^oK zc_oCm@u0bCTX577+npn*zHlwW(5O6@VAq&g{R^sScNzR+qWptk)R;RxFH-s5p;yPO zCRyzFO7r?EmRo>B~<*0 zmg)L45uF>=YnU2T!~*@B5v$mrHZfo)O3ZiX8brA+qle0MHk1`Ad;A-@s0`oT_P+rx z%EOtAC#_6w%#M2JpN3sSSks%0x1BXFvK!F8s2hZkm@cuqReF}ue%W~2+{K|x{SuuJ z>)M19Zj@Q0i%DcYf|+rZnYZZwVehTOqFmSZ;UP>^R7ylaF+jQmQBuIAq;yD$bPIws zD1uTV(o%{bFm!iGcS)zffV4D2&F^|(t-aTN-~Gn%#c_Opd~?ilp^gLd#C>1ad7Tl1 za1Q}eh4I8su&T2v2<N zg?<<0{q@@Kwdehw!Pz%-7b3cKlTGK+$c$|bQeeuUMN@p((3uMb~KQw z&g7Etfuai&(i4gJJ5s&?Ad z#o*ysqkG|~b)i+EI>LyHv{Y;1BV+haXh%dP0IzV9M&7j%7nn)==oRe@Qn)iDAhaFK zcv|+*#-%#0uND<#xwmao2vDbH(Tfwk=~|(c50=yOjy=2e-XbXp%`n}`v2mVh>JXUq zzUnZ5F{XJiutED;06c}RIB+uCw4;Bl-@-30^t^^J-vf9NQt8@TYyO1Usc&)Yiqm04 z0xdVQ998iu{1#P*c8xCPYe8v#F04h;ME-xr;J9(^W?k$0d681zYIP}aEnB%MPk0nc zJ)*^PvL{mVIL(~Z_{|sCYGd}-60G*X2kC3mv6c-uve(_^5ln9mh;63~)k_@|L$V#5 zYB*Yx6pl~)ZbclwxvY(hg2xX(UCLShg_S6E^+5qY>mOG+*^4cg$Hg=TH8EBBXT5|9fu7uf{{rq+hDfY2~ts2w4u`tDxijARZ5Ekm*ejOdYBk>9jp}nq8r9 z{RBByj7TPhVE={>y9DS97KYG72&6$8pglw z33gb2+HA^Xa9x^9Fh>FtPE8`(Fmvugm%`vI0Y?4{$Ph=N0l{BnTmnv_4o9q1f2t!` z9!R8^Q)DzMCqz8dm_BN0ZK$-#C3Nlwz8lqOF{giU&Iy%01M8*m{j*!UuAB|>_@F<4 zvq`($(-NGR!!Q&@xF<)|L&8uykptB_`$16D5VJr%=;)8v;a626lH1MG;G%IyyB4 z|4y!Gt923-lu(Qr*k;$fhm;99Y)E^V-Bsa7(>lH}XT-~X5A02$7g`o^u3%tQUR=w| z8a|d{m&NVyM2z;_aOBN)$@-z;;nb#xrpd|caGQLK7*Ymgaif!735p#UsM-vMefsp> zx{aT1I##v5qC%+05h}Lp=T8h*57e9_#0Nb;?_BjAV&8v(WzrmJ!^5GJ_hBu22h}P( zDKzaW!y@zHD?7e4B9$+M3#Mh;q|@mio*KivB1-_t1lCx((qVGkvmraT>0qa$uUu9I z5+rz-#188JNx0}0DFY5rx2;*O`mIx^Pd9L1aiIQmy7I?ehc$X+{@aRJO9Smcd%(0X zI97n{FZKjS!vSX?;7AB0D;;HXRMleIyJ~529f3KrvRXpcuU8c@b8FugAL#;AL|jCc z6hQr%DK?4g=L?*$D45EGkv)OLp^r{tkII};u7f$twS}e&rdJq(7Ru&r zgRLK41Em$hiUmVe1VF3st@=qnYvk80PL(02n5*O9k5nv*$**CeZnx4JIOTgXygs7- z6aYW6>tM@^cq*l7mZGUP)QF&=0#8AI2yVVI*ARf5V}$}BA|PKJJTTg3|BPztpw(&r z3(|SVaK`-~FH?B_xBORc`F7EHn_|?US_G(Xt~m4i^&3s7#qNgZLOe=bKw07wu){uh z-2VLeb2r++EYY!o&+zt+%S9g7$jv9`^bkS^S`*Mzn3z^VU5!XBMQLBby#E<@Y{`G6 zR|;W2nwqGS;Vhd8jUrNIzpc933uF=`9|uMxMI{RNs{PVs=}E^=TY7;~0kQe$f$S;z zm2FN&DrhNB9krQ5)?92BX^sx+Rij?!U}W9ID`=BMKpr9{i$cMGVyR^Hj0@;uwXLi= zRo*ZtKt(1m9|#|`V!@V5nOUN?q7wMzdQ`S|qX2^nk_18kGMvF_wKkhO1=LAdGm&$q zedD$-_HNTWque^(A=8pd|nc z2FeV4Hz;-BXd(%;q4_;QJG;Tp9BVXP|4W@w(^+>Vg+Rn?&HWM@tpkdoqr3pl>LR(F zM&iK;>W6HtjSRB6^K_{G;)jZgz@>f$4h1mi2n!3VfV!UDu?2DK`b&nSB(-ouYn)TK z1nyD2PaB*fCCE_vkf%|GZ2?-lNqT;MZ&+k}a(LDu_0ps^+h zhkV?AC{|cPbA>(BUhePOBhx|XX!`CQ0677A13wdB?Aa5K%ojN-nDB|!z%W!u$kLi4+f~N%TtVKk?pU(shiF9bRh3C)v43$3P*& z1L>fMJygmd5dTWH#(h*tmKhIf6PP5;v{oTuYY^O$5`9w#e;hD7#N7jgEO4()OiTby zDh$RRaDmeXRLFpeI&zv_Ykcdx7oiIXzJa>{;Aumj#Q za*+_h%n9Z8{{8!u31!kFAbSwAxSthFQAx^SI++vOfiq95R(NPptn;~Qz@(MLqooxK+tf3alLIrgdngN zVEhZi;l|y+s3r{W3n-m_S02N#Rl+#%g4Hk*gFK9bSYgCzMoo>4kI!_V);pJUU5=&O zYMA^PlvYAFEd%@yG&$$4$dQUgTPP}G9ETx_hQlH=B`4dMm$=4>MKzN&hD86L+8ni! zzAkSDHQs^9w&_6rtMj(S^YJ?C6k{GR8vE=Pjo9Hyt6%c3TL#Q;0*X^ZL&FMr8U7%F2R4zsT!5_R zvE!>)WPXn-inbkCWdmv^#8?vm?M4K+4**x;NwqNxuN{RsCa_O0faAaw$6K(-jjhde zf!pw#k24Fhrv7+lGRd&1^po5S7XJxS{qL}^+my=WOe@Ng4qK)JI0N2!r$IyE2@5I= zejviwY9o$5u2dy8)WgAOvTH{Nt|6JLddZj~X3xYW6(&z@Hbco)2wl_rT@RA1gL06h zT^&*-s+?5K4CpKEGtlj;1{mu{$?XedJG`>>x4CG{(p8^Xu)*L(O_Mw|0of2$_9$%Z z%T-!;6&W!homIqwDLXfYK>~Jf6e}&pC;eotQE2KL&>Ud3(ALqxM=af8yssxP!8$bV z2~<`vMsKtlP`+hR&{$e3XFs1=2e5~38?o8a-3}39M{2lz5K*H4(yxxMD-8y4!65uA z3bzzdx>)C(+yS$y7VDR>*-A(q%HtZq92Cx_v!7?IlW z`)|)puHz`0!pn{0)o;M+R0&`gY?YYPWno-)NnD^s!5ruwGgCr;o%I~rNb3QeeE0d34d84rgR4&gXEAl)D)p5bwY*t%4Y^v zGhj*3xQKEBf3T3I?yV*q#796Ne1POlP=dY!Gy$?%zk;awEinzZe!>xj2^*6d1SD$N zl_Bcr@#*)+8?Yy2)htPrfXS^Bb>m*4!lM|H*cQcs3_0ZV@`L1F@?FCDN;cNVw=kS> z`UrmwUvuZ3?SGbjV6tIS->-S2GY@ZJvg5mQ`L<$VO4NLz+PkZ&U0q#P_Z=#k3fN+F z1|+U7mg8K8abSvn>C9X6D9VLG(m<6^wOp-ytOJ-}W2^*LaYl1Brdj6C&En;fZp2Iu z`3A3QD+)}f2gOKWP19kejW5^-KE(Vm2s{p90mE?B=MV|OQw}unOEt#MprTK z%Jc1f@Q6-cAtVz)gbpDjaf!iH4lCZ8e?-`N= zIwvsD?<|&XYKG^7}WP^TbyRx80k?(wm|hnB8IoClpc;&GRRa zBwiZaG7a(t9%>hCZGK46ng^>=H6}!TA)Ky6-*1iSZo83sKTp}a%}Xau@TbiFqzrO8 zpa%v?jUXEmD`g-!yF@0cU!roA1^X0MOh*muC^I@g3nFA&FB2FKEjYRb)9_+75_6MBpEAld{ zQg_PQ1zMFg5BgKgrIUI+n!ve*i+6GNoinTlysIi02y7S{;#+d`S^$%vzoQ3r96J<$bN6iX-@3Y*8nuS;Kj_x64^iA?-9DCrKLp@tJW(XJ(71@p~2<}6JS)( zMSOPKTmoWT^AkUs8r~%*&VgEEX&t zIiWf*h1Z70CEpKV3QiOe=Itp-{L_8}LEq46}!Lxw*ts@PKi754a)B^;zy* zT7ez+0u4=XZa$nWL#`sjm|_q5nuDr9c^=Gb2N_|5WYTm69oEujlba$(xGv&NoA^Lw z*2sgZVqVWVbM0+KY5>CIXlmr9GTof%?KaEAOO{X$mh?hZPP68cw{(crRq z7A7PeEz0?im)H+VjFj~pwbMWN*(fT`vp&msmH0SsCu@*QiOL#=$2VjZ$=(vvAk5cO z*8$W5iEm9D?Iyq~UJN}5ul*-p+I!+MJy!_)onsygx+JSXe z*PX0P;beTd!|Ct%f|mzuLkd(U54~FujN)9dd+yJJTtWC7QBfZxME=hQ`p)0dpKG4} zZ*Ygd>n{FF&dT2crvLwM0VvVG9H;{wTh4PmmjB!E&EZ*mCCrGv(A{2kxPm;t4H7+s z#gi;SOYVb*0u?fhdwd&iN+3~Iaw%eqTjgTgAhTm)m%AyiPfbT^1Yw}0*<^06FEGqKrQ79(|RcIp4rlVsv~yY zLW1qTf(1N6ML}eeFjM9MADUPU!Vv&7WgvurVtgIIX=u1$euxxQ&Ol*-Q5F0M=6hC| zL3NP*3_OLOt61MhWbtYn)=AK(HDMrT<5#EQa&WP8h@&I&#tKi6b{*D9%vofZ4t)V& zCGdoaG%W0>3c$2%@R-$LRV6s{_Urgu5%N}m5EG!F1P8&EZ$nOf5M9YDQ=AoA_0=+D}oBn7t60QI-AG#N>bP4bSrVEF+>B}7eKP<|y-)7jDzySn8 zv>R~=0OtliDuLbAZYDldGCy7qvvu-|GtOz!VL%Bu8vxZMC%1w5&@(%`3joyB&I^4@ z7=~8DkNHU+5%^gv$uj)2WnF0@7kItm`CJ&DOiAF%#`y+^D@h(H0W_1_W z*A99R2Oy#%I8g{@4^9BECxFHcKA0f3K}NEzP6uG3{5CnW^md2L6?-Mqd)bZPfDXd5 zqScM;n7vbQ`K$`?XMC_-INX_qRw}M8NXZ-XEld<*!2yjKtAEPpkjj z2DPehgU^G5eHR4MsywQ>fE9o-o0isS$?A8+I~oi!yD-lWj50Z+xSaR4*Nbov5DN=6 zkPx)t=)w3oRMQLwQed_Z4Sv>aHeA#&YWY_8>u)NP05X8=4n!uwQQZ?HP#;3*NfP4| z;L2?)l;@8)NPN2&C9QFhW6;#pln}LBCAjO(UTCOOT)Srmv28FqeqmPDH<$Rq;hgQl zzzuguH~mZJG6+9i(MSaSDmMA1(p)@gjDbfEXEHGLAucDY^~sg9VVlIYTwk~lGk{q58)CKLco_P0Oo?~9C$?|Gc|I`zDW5L~d2WI2!bO<8U|KwX zo%npi~%aNMG?CNZNy6QxFrY zNU;`N!pbaUq0-pWKQD&HFgPmX%03$E#;pPp^4i)~g zV-(yy(UlhpwOTdi+M$pm=eM zRkUELRj$mJ_660{P#uA`lpIdRoYG2IFrZ_Da_0mb1XvpCojn?IpT=wWP=6f%bo!{f z-DJQYFXpP(4O6;w4z4-bLg)j((hntGioFCb2uKKrnDOIdVI zJJMW(H2_wEupv7k&b5=Do?d=kP{_u3j7a2iE{{^JP^i+?I^~ldL%YUvT}loCvJeR-1V*YMBrfTlzLTr+2}EZIWWJilMc(bVRVL6zS4g7?jNfytVp z?c&AE%L249s?qUxTwiCll0She=$och!}MxOyY0xY26p; zjXA;zPuT|)J|b`%E`A9J%lEnUSD4Dbq(%ZrG(t8jT7;Hfv|eE3tUlMNkiVe0?A08Naz93LkvrCXp4EG zgcy|*xN)R^l~Y4LrFT-OoO3j$s};JoBfzjO(RxsbFIuQ#)~h4^;2e`YDF}?lfxDt= zz0Q6X84GnA!{>nkBM>bA0>By{DJsHZT*3B;83jFllh!auY**#Ym3?Q=eyGUFwx91m%f3Ie!M|=ieZLl(Cqtm&V58Q#X(e`KAwf4v^H5SWP;&D)1$%~!g8YFH^7du_+7pdocYh^sV^TIUY|$dBX1rig)nzgKZ4xzbF(i4x0PFCVrS;Jkk+Z zry>XxNsz9p?%cYm&=V~fpcTYE_y^F+H??-aa}LG~0a$^pF#-)ZI$mjZ&XAqC8_TG?Lwa&JI{5JO8oHjEnVSp5yA6x^K4h3#j-~W$91>Js~{KCidIohnW6JNjQf~4ogt24|K z*M`Z&gu%gLvDYH?u>VhaDm6fLl@s7}pv>jB9$?V&`O;7ug+)g{fZQT=gCXTF@OeYR z@@wzB%3u=^cn<=^;UbULnEt{ii2?y%rH?S~7%75+#nuI|Kfa~KOa56Pa=K%`5@)VK zPs$NhTsN@{DDVa4>Farg?Y#B5{6S+#1$9{Hr$yK7ymQjGe-nS#6=NKeQBZn+u4Z;x zm3^TZjeTzhgETWUGq(mTW1a>9kQ_sMzZ!c*Ke0z6@IVP06+(aUfcyzhvcY4&vy8n9%9r^mEPWs@HzMcW!WIWW&oKdqhRquIBTZ%K}bo#A{4gVGHjK%{Y z$)T=kN;tWHv#S6*@eiem?DKyw?+~?z`8*=AS0n=_I--6AP8B;9)WorezM~WfE#>Lb zNGZI?Dhk>S@K|93Pp~G5oXA@T<3q?51tZG~fkxrTooQ~~llrXP`|}AtN^tl+Lhx8$ z?^)&raGWu}3~#EF@qc}$ZvrCuu}b$MlqUE;ApTd3%<=;f&7c}Mnr18n%G($la3q0Y zYc03~0SSyfd2$t-0kaWk4hh1o01)0yQSofXm#OD8^l08}1dFrhut;JBpzwSpRDtLs z660Wj_Rdp5Ca|yyGZ}mkxdX6x1A#Kk?NPtMmkqpx>%-L952p9aV8Ttn!J`3;wMqoy zMnRCVYt54Dt?>06A}Tx-8F9?bwFb>IMiU=}*U8>^Qg z9akRB1tUagU+4Q3li*w7)nD;LqalQ$7N#MvTC=gU%Z5<7iLU@1tPzuxlw|Ly8YGvS z6KdSwrUT}n1mR;M0=+`1Kbox8TP$2i>Ya{m{3Xn2SgHW72OL~P3cgeht3K@7B1r?X zi*sLw1{sw=FEPpt+k34p+$SG_kYX$czDy8?2yg;uHV~&b_e)@O0wxOJlsZ({r4qyd znD6Mpw@83oO5Mb;Khb0YbqEl#HsBTrD#C5S{ z&Pfm)J%w)8cY|USl$8&!$L{Qt*uQH&aPzbt##kLt^~5~^>re%%5%1e(^)e^Q`Zc-F zjd>7oQs}fe#H*pkeDT7CvB#%lFJ?h&Qs}h3W~A|n5==u0_Go{9UWeKg9|g2n#MUGH zI9pCGWNgoL!rdxv)G?LwY3Y%rzPXbHxppB_t{h9~{Q>nP@P-9tA`JauayEH8M_*u| z@Y+g%N#}1M>&*`rD@%tlQuFik@kOpS*a0X9G4fo%{eBIHlef5eL(s_`ER zd3{&;y%LOpDt-wf%zGAo{26RtRHw z1qUO4xG=$yjYZR8CO*YP2?ppwQ+z2M31+bIpbPB93lIsW^9KcnBj%GASi zU?S3+V^UT(Aqds4dg0zL&pSFRs7?6n_`&J z7y34@PyfRa^vGr=$O=vF!8{JEB*4qB8>7L1IB0Ub@^LzpPoM%~>9a-v=w7mcpo^xG znyeLG73N-9_EBOb(!n$d%!I$-QI0`67%?;X^mL&SH|Yph|A;}0)>4|LnQrky^^-={ zN?0d>et9qv1^1~j#Eb`{MNL8LQrWgiVY(m>ey?{BH(2#zTL@4(Z8w{g0OPkWXyYE; zM5Iafp9YupJT> z91J<73KGS6jg2lWKX$%+&^$db@~BqxqW;NLisb3^3rcBE#AIJoO#{G@VK&GOmJL*Z zRKP_Bw6ccTaN1dsHEnx{Hh8`;a2sp`8Xa2l4Z3tl766>Qdf^nv#1P-Gn{e(yB?M%3 zJgH%_&K`2P=IPJDbBUzhh14s1F6|629HErUXg825JGmDLXj_&}hl5&+blZ{ZE6$mb z*kq+f=J>ZXE-ITuX;nWfUY4y!d2f+vtf>i2`S*kEC9EhQOo7S475%bP7;012HJ z-lm!m09jo(;lZ!p&O1RsU#;RdL@=_0Lu7_4&@X}Evo6Lu<6_!ZopPEK`aBI7Pbmh% zVEmk-&wo414^{a4EPp@o$UmFtkQa8ZWef9o*xB*&2br~Cj{nW${#A`nz`Lu)FTaAU z+O>*VDO)v9p5*I%)JX-RhX@O6kJPr6+<*9VCX?Wiyle;07sO`Z0`-<9rU%$Xdg{S1 zEfeLuu4)`uPiEO2e-xkSIcJyzA0fvCDb2orCX=lN^#QDLV2aM-N=X*j%@Pb?P$3^E z6u=a1{d~Eo$@DEeH=x`kqE7;G1@P6s`%7l%mT#Q|;!e`!YPIl!J=+;6yu+qjdEm6- z4fHo#GsWEb1hSgQa=C`-BC#pYvhs2oDAB>kQWRP~*wje;Sm}U<`T2S9tpkC=F_4nO zu{gsz1V#f!BUZ`q6W=PMVE|}{x6SIb2;!se0Y^U7p~th^RW@|f2h*&TE0@+O%IOaW za=~Y%$&eR3Gh)Gq05&_YHCgd*gysKGlW@|dd{m`5%gVyvSUwkcJ^gK%W>|&~jQ_44 zIAKGHtt*p(8G|Sxf|FsVc7S9BGP24Vom!w8d62Y+3z@Wsse#}${J46GV#eD$>zsPmz~ zb0Tq{@0saWL7_;rABhSpu+Dadh~Loq1GDdv7-!2opdNpk@y5ReO^m9$EDwUC!27Ae zfW8hP_^%agbc`thgY;kh6dDZ#tuwPq#pBEf1|Io%07E9(O7&iYC39m!se@l; zl_b=zMK(THV;pX+W1leTyc>l1Xk>;48&MCFCv4OpGN4or{c#`&SPg$G>$u;1bB;Xb zIp_fz=y-YL>utc?5z=m41Shjg<#J3G1biM%5@FUI^p?v0vU^nEU|fX69gi6Q$jtF% z?mq6m5Br2+drP$nrE3;$Yvofo9o|*t)S#yL58TR${{uDPeEa`X)PPH@|1VGjKK*Z~ z0p361{$DSLGV8$R4d??(Y3jFnwP4miQV??g%R)S|Sl0I1eYZ-a7)6t~-k<6Ck8n%e}D@b=lrWc3!HchTiKT%>lx4t&%II|26Lfc1F# zlMq_<1`KEr>X4=*!ZJYL6L=mKeeWe$KXRW;d{RE@F9<+-H9~!ucy)|gh-vjlrQFVX zx~rcc5DY4S6_=^s@~K4NkDTeY|B5`&eAkw-Szuyi?^2G{<>32Qd)>R2jv?d$$zpPF z_%`MIJ42yo0TAorvGsL2*A==x$k{=-7QfwTE+E1Z@mgERUYr~r*85*E3SLb2o?n%n z{N$?pFN^|_zZeB8g~Hx7I#%NvU+-^b_bsSm4hKe({4JX~6&@SU%T^A`BG&1%m$Em` z!vM|l&Wq_k=G_q$M8XB4%U{_>Gu31b9fgB)Op;R;6TlaAeT@_}gwWkgo@V2)URqyU z`<3>}>0MreUOZHef{f(oCUvv^L%l`8#d`I0XtRon$k$J_aOchuF7R?BOMEFW*P_Uv z7t@&hlPoxSM_E5-$3S<(UO%|sK)o!+ohZt2vf#b9;;(gW%#LvKhXt~(Gz40)5-`1~ zOes7#Lf$9=tb=YD_$jz9YUC`H-zT-cOqSd}C(OJ8 zrPD$%S_7&AUFRM^T0wk)CDa4-9|Wv=6%65$NP>!}Ppg~^;}A-pb6#k9lWas+f&Hvf?!ph4oqGx$Ind9T^Zq}fZ#cwdUi}l5 zNgM}?hoV=hrVxB_(}D<#NZ0ZuhqNvOVZ1h+R37Sv)aLP}I?eOg3vF*r&|Fn7ULBKmB)9fyquPHu#ito!is~grwww%VK z+0xC^*{fE#xwzhds2D6?keeJ21%?MX zHlssqWlsXQ%*#XeTxd(%w=?8eVkS5_N7;#z2j-?6T!&HSFCwN@86-NUV0;Z$(Ex_P z0*mkqn&rS|6XbB(>QRA%3*2e=C@7l{U=SMcV;of%+hPR$0SakuZoZIBfC98J+GX1a zm`g{XVTP8wXbqf)Hd7Dajz96wDfxT+si-TfK?0mM{zH@(>EG!M+7^l%*qRek&QfPQ z?at0CdE2YXoUby;o*o@>y(|PeV}PSz6b9;Mvdb9^M*wetm@#17Q)Zl>i}>^jv2Y9p zt&O|nSv2I|0+4j(?KMU>LS($am6nwW*U#Sap*oW7Dug2gm0X3vHuIb;RsgLQe)A{+=P1x-$ z%zYJ^~^QgzaT*!+L$S82}q<)$)Qp}%g zja1A&A@ciKSCoBP=hCPuzavXmm$Aru>WAUHUvh+_i;hj<3n5bs^H7$}%SugpDO&^T zniD2stDPHtXeHpHW~3Q>IP8fpBj#g0StO-NA5Kt%853~-!Iw-M1e0xoxRPdrd9xe% z4*@QPiAG3QnK-2o!{)}}$uzWo5+ckv7*aSQGLHD0E0z`7$Dy=0q@4H3a4aPAgXM@I?PY@694}EBKK312pRr=BA&oRtwjAjfPyr%9nKdw6{fcv?U-{?3c$X8RC&Ty^Ap_qFo9os+ zE{^xo^Yp-?NY962I32Hf$48H7;F*^O)iP&Bbz2L6Iikv}jGx!MOi(!e1AQ9_CI(C= z+jCga=P-8w@rYcuQ{Q-l2Cxen@+A2LC<^>)m>B=%_#>{mLt7&$PR{!Y+2D^OwjIkp zJ*2Lz(>GRf@-(fUCmFum0-o7-{FIPvW0H(<9Q#Z48Dc*#LKKWvdD0jn5SLrbVerYc z6K3`_XsCd3)DC$8?&3NRpMs_%QaT~?p)5ev2kRHDT6BnBJNm4Rzb-y^O!Mg&PDd~M z)Exeg8Mo>t=f8X2^BHIuW)6LFD#A@M_2tyI4r}r~ggUYUP7Q!zAh`5JWxepQFz^M1 z^#n3-PM0zUq0xx1j`ftUdOiTR8q{B%HaIC52j^yI??brk^V^5}WOo?Z%V8Vk_5zO3 zL-I8@&(PvOwn>h__%><08Q0rWZ299!&Fp)`+8ZhM?X`Jki+g6trUiQg>d|DyW-8sX zZ^``owRg^I51NV$hngl1>Q{LaaHlv89ZiXhh?t$yihm_DJ%yRaPZO3>HS7TmCY zzAl~ltv?5D6kc0rrh;v``$KR17;GNTO{#adLOcH_?l%fF7Gv?DnjGZJjYu>ltMiNm z(i$K+(FZ8&-ss-q%dG_qSH@Kf*oN=DT&#w^e;L}8%gEP2lW1>mztV&-pnDmKP#}e- zcHMnG1YBqi3lR~5vh`GppRlNWM=K_yP=Y)CZ7&8%P7b%}}s0H7ux1y+o7tkrK(^?F`8whc1+^VKGZ^)zi-vqNWC|Q zH-~w_|82!`2UCcWNwb=faa+TMIjW7P;+CSQHG=gI3LoSQq-mAE2l9H;IwCOz71aKQ z1&$nAw8%@Jl*P*-{2I|tJ%hYZl~RY>J!u+@UC}q2sUDNTe2=v^T@%q5P3=V2TQMvb*`XjnUs)xH;GEcq1}|^-PTd5B)#4eA{U)CRlO~ zZ%SIUY0pg^t^dR>R@9WSJ*MUE zOy9&yDW}2m{LhD)=>J3rR%AFZN2gQKA1eQ3z$tCfxL=maj z=fB3`(zU9v<=aKE|Y<=t&HEE3|yab>by%1=MoH0!~6cmTx7A zqR!PpzSq`60%w4l{hRI1 z8v?@zn2vvrB_(sNbZq$jMcEC#3=jsmhoTaT=3)}h;&@_rs*f#-Pn}1>*x1}Kyp z&eiL&^dF~BUOWjeI{gT&{ZJ^vC=?2B%4^^#Es54c_e|U8kiMl8hE*^mZ?KJ22Twjp%MhWfyp@sOrEQFCRI0MO{u~?rFPQH`d0UKrc?PfC~8iO-d+f(cbw_Key}*3;K7_~ z(zCRgL>0labTdz;E5wQRWq!#yqb#M>oBg-XUOMt{fZ>GEvHRnuqUS5N_pi3-O&MQr z^L>z|TTVUQEZiKGTxh)%l{wW~`cizVC4%DE)i-DFA3yx%CId$KAHR8X_9L_L3)+AF z?YFNF=nh}${TNiRa4yO}!K-gw`0-2h&o_V}{YFxRC)NI>%>vEWw7qA|xbeFJ zVZTJNkLoACthT8K#ayR>IR^ZDctT}OBYFI)?#WvXYa2zpf=9(owsI?`afIq8R!poD zblD$ou(?T8U^E33G#YfYr$+IY=`HQuh@&ofe2gjl*s`p;e`9$m`0ChdsS`c>;APiM z^2Rih-!V&sR+-esa{N8q?111=l+)JKmHpqWI4SaLf;p2u zuq`Dd7JuAynRCCh^2{Yyzo~3Ro~b1F9Ez=zH-@;CUc+TdK5a#hVRmuRvqc~7<4O0E z1K?CeC@gdk8ZhDRbp|rbV0wjOd&+;75D&_9g!u`T7gn1C&xqaxoROfg!)o7ucu}n5 z7uWQo&o30mS1~aJp}6o-+F-tgP@KWy#puS1Zdv6=#g`=03J85;Py(*p%jxQ0CJoYb zt(|`S3KHZ=8F){n%#VlKny}cXrWoC#yizROh5q2O+hp;0#1)g2!(&(Y!GAZhd7?>l zj(xUtrh7Ido!{SHf@VyafMlL?6q}^ysGUzI>1{j-YaShje!IvfEsd^Uf9ZTiK6i8IGvqflqX&H*V< z43ZjPK2H<>8K~M-6U!(R2*}?<>*PWb>mL1pE-BzV3b3!lpO5I^Dy$Ckt(p^XCVl68 zBhzVL#>tRcQGMsmvn0MNrE>kYHxyUSt~T$Iv(3@yo|Z;gYQ(-WZ4IpOy`8o05fsDA z(d{o|-s(@%)T|^B_nWx{V}f#%VU3g<`b?IsN)(daW=%eVKHik;G&!wHt@WkIs6{@d za%R;ZcYImUE~mIenW#(N#cNTk?47(cOMA8}j*b@^ z+B!Lin0SWVBla2GB6|30j#0@OV&xvfh3??!2%dDR?j#>i%R)N{0t3K!V`x*_Q=Wug z$fNWU?c}~=^YSG`zkuXCf|!B6=A-l@#qQjQ*x6ZQF%@>kJgY0ZMGbww6zJ1D&-1?< zr$L3@7APs)U9B!(e)+0tz=dZQx)kwjWb1@LWU3jwbw*xXF2m z28yi@-<6&Zj%Fp)(L~P5zj?$n-?>tml%U#oEFoI@1kKJwIm5WjHzGP${OeRSBe?RU z(i2uUw6MIYbv)-%>q3dh@}z{U=!c(_Z{HnpoH_CCnxH4#041Apu?Y|EVq0aHcP8dU zzU@XP9$qXSlG9pO!&02s#Z6pRY?j^SWuEPdpF&;Pt$D^ptnfZJFYmifCJF^yEuc)G zw4ub0rN>7Z8yjoqs*x?9^r-r&&2JX%xX3FImf3Hb(QkTW*v^wyJV6&;i8|}|prfm+ z>KH|;4&$-=-y&iSUlj47s;Q5%nBk|DRlh|S4@DRf7OjoCZSe|lIqEeSor(LY-6@b( zM(DIH8XJ(3b2B%e7m-EW8&}(9DY4y#wGjy! z?Ko?J2aZeiCPgd3F@ZKtBpnV;`VqJq8~S3FdTV~+>`yJd*)L}*$;y+;d-1++t)5yfW%73|4%E|k(Bi^Jm zQQrpD%TJ)bDgSwmTzgm2YLLu3@r~pstd@G6=QRyqzN%v+QS^dzZIRxCeFpXur@{q( z?Q$yTVeOawVtyhQJSyzAD z@NyNF1Si5S^RULu)rWO|9jA?>fyHl{W{)~ku=KScZpXiK2wB)yS*QyJ^UM?JdBnpV}~x=Mt4*z6Uguc9W^bxy^+;FvOtm)#y6 z&uXPx>qJ+Cbab@yRe$U)_98*v)Vlg5pVo4dNfIxu_faAqW+Zc0H)lNLx9&LHiEot& zTIwY1l@n;9Y@uin-BGgc`jn6{7V?%1Yqu|0q+R%vT7$$(CMaelCw`|)(w+dNsI`4= zO3~RXe!cO=xfsnm9)jf?GDf3ZC)~041UsA#0$1#rQ&Y^UQm{>ICY4q}AvZkBA90kP zcx-W33&_+eOymHPK!^uf_uw4#VSsx9=MlVo(vJgmsc1webkS~ba1a@7alZ`Us&oJv z3s`|-TVClt>jTK+&Nb$oxvChNVUJ`v)jI{HF6|h zf`5+$Ai@}BGpWg7nu&0Hu>cSedCaqT` z-VnPM7){kc!5PFN9;f)+SK|DfK6gXDq$g!_XqIR7^HAQv(w>5&bV9tZO2fg2p^D68KPD}`x1XGE8+U1Qp5lzCo-UT|UyamFMhn&9ld^JulC6$JCwP&>{Y5$IFM zCTFTct~I~pXl=n*yb7x-ewEo>Jbh*Lh{yS+8?+mg4JVAIoqpOf744lHDq7A@EMVp# z`Pzd0Zgy)X;x^8w;CVL*8r$4t;BxWH`j)`{#8W4SpQKkJu08VOYLl3ksoVOTVkkAO z%NX+Fb*rEve80E9Df}~!(p{g+fiHeSVUgaK7p5TigZL*awM0W5vQ2XMY}iY!O(ke% zGn#vpuG=8rTuiMgjfBl&hCrok-!($5*LSh)7xu~zes9bij;Aq{N4u56T*k=qzSSt- z*BL$thWZd$^$m7TAM#sW8i|C$6=+2-Wmy`2halqCPgjoLu^dBz69-wKo&-tCQh)Zb zGjB!C9c!rd0hghQnHS&nv*Wo^P(os2?_JK$0;+_#)ZWQMqZziG$|B=f2c0t2Qi0Qb zYsL&nQ&&E0-)ufYL{7hNk*T7c-QNyK*~o}ycUBV+0>QdgSVJRfeCsBf^C5Ndz%t00 zp~PX#6C+=jPeY@TKpMSrz|xRpuoeTclWNiqcY{a5e>J?Bd@~Y?Q>7HhCJPG>yNin1 z6C9W_`ndT0`NPK4Uzb0MMnc1Tu!b0i&!-|bKw-C10l z33g>B&xGGIZO16~+;t6y^+`J29m4ZF1sA2~i|5nRZ;$!KNS6ppOGgtg6T2~9y|RDo zvo1B;*TsRCnkVpLnl$e|yRJ34Molv}YtyK08-kl9MvKKQoPz=d3qhX<0Y|$xg;pA7R@QM(mI-WOo2M!3_eVCo5B#caKM0OA z&?Whp%WzqiKDX4XrOP{CDiewsbcMRQkqO^X&Jh-=E4yN}Ei2Tw;X)9@Qj|BBJ%iI+ zm9k>2VqQWKuSf2y<@K}00B8Jt&Dfd0;Kv<0{>uH|^Q>(9{b8}n71K1hb6-neTCxYP z1jJYs@(I}YC&n`lGybr5crRmdm$NiYqeXP5P0t100ZS-9_QYM>mK4g|HFFck+T;(r z<0IhkGeAK29ZAh1^N&FL4Bn zB8GkWAyD!p1oH9Il<~e3+-=Gg?8$dpu0Oa897^1fUAH5Yc%IKV)bF#o!%QEvVvQ@ z`^t*d5V-ZEuq>B%#qWXt&Lu&YOW-E^ir(9@P~8qp6P{+OKn<4(9s}gZRDZ}=mKK3L zkAZ6Uro=L7UF5gQTTj4|r*>@Y3h3UtxbNUqAlE~&(bn#S;5clhaLFF5q{}ZY zp7txdc&|i|40okh|G*uWXiP3YdEK>dMKH|zsb)99ogJUIeeEdh*^uxM6AehC*}5re zet#gl-0q!}_!Bk#%I}{nG8S4mT(Bfa)8Xo%M^PO>GeL}78D-np5p7D`^|D6F@6xqP zqmHcnuNXtmuP2n;%_Z6|iC84~C?e2#`wnG1T~uI_O6RRFt1{5vxTM$n`{V2`BpmC} zOx|qb;0tGXOq8l+5HrfVa_1h8AS~{Ydo>~jRx>50bIq_mKi*8#N!sr8&-j5QAm6W~ z!udFdY{qW?ZV^UamokJQ`4Fl>m$)(8!5Fbe$xjn^cL%xn2ithE_Zle5DsM%+@Q!^4 z%#@lIe$R^1D*t5<3?y7js)#E34nVqVzLGWF+zA5eBVZs zbng14GvJPvZMWiKq0*2K60{G_E+FT5ebc*)D<&1#{LB?Vn{--k^2bKvA= zcr9U%vKr~w8_fV|wQc&Doa^DK9cG*TlL^%zvYkV*4q7folN-yVDjr;S8j9~P9Tz`Db}=MgWb@{g zJmi6h@ELU{Dg@KIv?r`?+s&Yj@JAY^zAr_h-`QIHRBI>D3mNOfjt^D&nD@;=&77oi z>n?UbMkp;2-S}O>rrtqo;W;=#KT-Fg9X@=Y^L}4a5(y`Lxz9^|HH3kUP{Z2Z-BpUOUMRos-*R@-H@V=Oim0aE$H$g1T6B?|hK8;EfNWjR&&r$TQ#nv{UbL z+z(LPn7{j`RKH4#&Gz!wK%%q&B_ZlQKJG~0aQHcIFEfIINHSfV%d2cwuYfiw8^fq24WD$4!wcLLK9qnCc0C#%(HdGS$g=aW4?Q7N6~r}6 zG*dqq%V2#l=zpC$|L~YsM%}a4^ch@X7>5N2?F7vwRMjxz z^4E{ykG+h=FI|$i59dXwinpEA4bavNSGqw((qDA{EIhvObMPRGGr#TA@=6FC9J4C9 zI?$~aSAObMkZDMP9Coer^Op+`uFWMUIE}Gt48M-B6X-=1R5@fnW7xWC`nq=w2lGlr z9NFqCVeH)RT+SA%eVo`*SMM(VavROMu%t|NZ{NVlIK$)FOrc2ASFx7LnM7cpzq!_%TUpnexxAO- z%s>n&0__=ZH?|_R3xa@}OT=5WZZkr${A03;Z~*BE(1Klx*H(jR{KoB5Tv8n3!1=M? zT9|&|qV`=#M(ybC_5>kl?UyPjd;w=bOg@csBLjseCIn29OT_wmdiFHZs)H{U46=2E z@}51zW7v?(=sA9+jVc!PV@6@r#Og&X`tJ9h&&?t%JuSH=(QntgY<%TjsL`o#5j}hC zI}K*9=jf;gNo;g{=4-P)a6L!po+UTWlOVlB{KxxsMTab2G=qB5Uqt6r3@rSWIlza1T6{}+_7Wju`usDa&qRhza5lORdz>>Ak{|#)l{9C|LMw_aX_6*w2#lfEx;Q^PSUF2N zvK;^IrO1h#&l|bowNzPWhQ6jvFPI6&k_Ed4(-Y(}rRi0AX#(UZ$mF9Hhc@njtCRKn z;IARJheHG-)xa9{zI8B_ILrkcU!CI%6>Q;&Z)rP=3wVsLW5#8-0SykGIalG=Knw~d zgAx1;C6wdKvUckM7n3ljHRc3G#Edor@5d|6hxjBvcKM-C8bZ0+=-q3ym*?!qi`gM& zcFCC>6*(4WZb1fx*!Ig2&1$v{A+ zxUU$;l}PmOWX=LZQsybz5$fJGU(I7z6Q(SSp<3Z&A&&>V%-Z=7Np`er>IR`?=~;y} zZKC7ylS%YM4AWxOdmd~>Qr*^C?%ZN|#<;?TA`4iZN9_1VsyI0@rZ zS^v`_v>>Nfro^N_#_)4b(C6}k69;8GyTEHBsUz%iZ>VEOr*eeo!?O=sU}nFLcNU19 z-p&Yi6%5bTK9SA2zjJ(8;8%GsJ=3z*o6XqI5BQmVni{FC>q7f#C>x#PH`l%vLlPwY zMa)nAjO%KcCLfN7US@AhtMzqj0n7HHLTK_@4REb??@zp594}HVNO6NdIxlA-2)!Lm zy!ya+bJnZ-xFBA5TN1~Rg~#K0Il)zwYL&wPHsqx?pJg`b`U+hwbQFblwHJ4~oS<^i zM#24d`7hn$UcYG0ccyrJ7IW>QxgYG$y(WkKY6IesYFZ zWxV+-UH-dHy+!5r$abE}Myk+BZnqtf>aq{Ajrln!&O{wXiEOtzWZgE~I@^cf3W1&Z z;|4OF7dfR8ItP8*BlAj(Q6V+0w%^wb0B*>AFR4%;^(qeita{VjK@x#V^u)%S^K*&h z$U9qqP~+v8(iaG6gZvItuunk%0~oad*l57B0o3CG0lq``q0z~$eB;8y!L+o6v9hr_ z5E4LfkBrTdQRvM8)FXo>$gGvMJr3pv!@TzrqH7Sye|2MEMT4e@!3WqLaD>mxZk{l$ z=OEP3bw81rQNfma*k+ABKP}!< znc@QQ_5O>TT~P$?36j!pJCqsm>p1bs@XeeJVtiOlXW=_$Ru-K>HN9)5A@{RQ8)KpLTMcpIem*U4mO8|< zEl?$V8xHNcMia0dFfbUM98v}hpf;P@-5>ZA$xF+EiQ7|O$G_+waS9wwtvZTo6!A=< z#;Esg@l{=CF#1NVZBrkR*=_d-Y0zK9KJg;izGB}N42`(EX0noha2sK+AWR$gVH{aL z*6#RA4GtL9x&_7YdUnbUx07~K!&&{R{OSWrYn$fLs@X2SiVf}7E3DY%bF3U5jt{Xl z1QGS22hcA$B_*zjGZK6lEv37=zV_l`qG! z#I?2gYx#Mz$Qc}j9-9GPGWl?Om^Sn9r?Z48>%BLSMfnWrm)<7CFJ+NJQz;)d+W{SX!gObHMq4?c;k;FDc2$#O^IzCJ#xql`-(Xph>bb%<&c#6=e`W{Z4_qXKU+S zKc;TY*d-EGh2B}UZFLE?F}gJOlJO|ZxU0La zTj1K%dMFwpe*tfKzQD;xBn-O)CG06xk*~xzbs$8<3F)KCmoq=_@AXPNhwnNbZ-gcT zG$_~9s4PJ~`MrwIqMFU#X&?(f_0M}a&+3?H+U!R?MOIY}&}9}C_k&{k=D^5N1-;cV zPcit0d)=K@X<&6Ne5W8cw=IJH{V>w%JdBA45k>iSljeKk!}Su(xz={&@rqd3*6342 zzQB?|ct%a-Lhe#~#h3@i+!B9qcZ57g5LRjq)>Wjkaj$eBpWp0_EY88HOu;miKTILQ z&~0-0qy$v}gp1t)&wf#>2hZDvc>zK{<9`6Qtaix_E;{i9Xo^71titD8BttISds(h+ zY7ruolLKn1Hd{eUdfGP)6B!l{H#X(AY@32u0PvI3v$L8lUb`Ss%IoIl;}+x1wf<)p zYUs8uUZ|+Pbuon@3+5X5bjU+oz>Tb#85@O-z z&-sf}$4ao`=B#G?$|Z}ueQZ+L2(TA}y0s^cBGd}{h3E8XE8bB#$@!fbUz)W1l_j1X zX6TB{iBk+$`DKJ zEJCO3_(qi=iX3eCqNxG1Mqo+DL>QQJQ@aR}q<^#v(Od%xyoeY=Q>l8{k>kF?(rFID zbL+&J@p937i<{)nqg1XalHA~k)|sB+?C$bae&si~b;i8l>`0R(U2-hy&0Uf3MTbYV z*}A%mBrST*oB)7wi3~MQIwTsOOg^bRv1rGC1EVV8)!)?!-AwiK18dVsfnbr{awwGj zrK{K}>27jE;N6eJ38>6~d_tzmF_cczgN(tG+(+8lk+JKn(9ZA7{VrSR@}0o9KT|7F zNB(%c3E}3uj$wa~M0-%O>auvA)X;^(GOgP8q_o0S&i8Su{}Oy8uTfa5{gHei0uPPy zv<~>Szgmyiuc%`G{`*Pk7dZGsWl0H{Q&j(|)Q2KFyry!aGkt&z>O zcG5>7&s+}vn2j9tzEd7p?xwR2>XTL9Zp%6{EVcK*7)CuJf1b zUs;H9+lpFVeZ;?5JG-1eMjzTdx2|`5?LOXYQ!-jLEsnxiqIqpa4B2Aa>vIpH0OpL5 zbfW^VO8||~4?1@BANvrUILrXW4|`4rwG$yBc&?}#YI~m(=Mg7wdOyZ0_8{V(LcpXz z`}it_kLCn6d(i?iM?hFUrLzxJv3@BXM!3Nc2K5f~pkO{za1rNoXICRKu#TfQs%&T% z0_kv2-~GaJThbFJz^^@Ym|0dbm#o~8kiI&}JWPmb)M{^U6t7e+q`i}qvmm#fv%^=b z-8a6@-xG=MvPYMO{314c>$%&p3Yb9o^WjLVBTvw`CfQA{gI@Rz>$115sakByEf3UK zfqg0~69drANUiW#Z24nlC#NFDm(!8CM8{IYIUkuzGmC{ws#K-VjT(z9tpB#Xsi48K|TZM z3Jj+P)i&h%`5j$x!F8tiy>rAn#oh?zk@6qd{a(`Ht;6ByDa*>$NV`xMw0>I^Y!rYp zPHmQKm`xswDz}g9(vZR?HWJ;m4^QNHMd6ZNwO=nQJ0yiJjmhdGZqtZJ9ga=P54LAR zVQ-};BxPfAPEeB6y;+$Y`ekMM1Si!PS%dCE2hnoF)pN z%)&p&+}~uLd^2a?*OcX`ArVcZ-=y~EtHH=P7!<3XsRYY_+Lcp!dQf2t$A$i1a=ABmN7C9X~A4oNwOTZ%GKeYaj64 z*0M$>+53t#!BiN&vpPnVcKw6&G?B^coL}&KfjfR+SKzBi!J8t zkk_iGb2oH)!cEAw492@a)Z+qT7>&GctqB-bfVG(v?KXHU(~utYSh&+@{~ktUDIQi4 zCwMvWqY&Tx65jISuSLRWPfri~Mo2_sLwh`NoYnfWr1E@7y6u}hWk=hFvy!|if(wMj z(0C&A)&#G+=miWAhiG^=KO2wgTgbm;n7>wW<8ug}k0uR0gL4K~?5GervYyDshO_bQ zk@;0^ZEb~vncVt#S^cGM0rt(PHaZ-Q=3q%7U6DEbb@st_&GbRM*RXcwxeA_iZdbfR z+$-^kK$Uiv`%7zUZcS$~1_VYaT1hvuSb1VdCNUSjLt+qcXd8H7(FsJ&0ghVg)sqNPmapS zz&nn?mAsIe`@#EMx}_Yx_HJpk%lWVfyvwD5R?n$6&$VW)vQfj4a~Vmao-#O24y z9rjaavo}I{;ZxWVH-@({L@>lzIBSCDGdBUcAQh`wc|+nuF}bNka|TwE2T(;YUakG? zar5R)pl~T!U4^OI*(x>`cO10TzgOnusODwsAq_ptK(Q_t*ggQxc>DW;jW9KMxKKPi z!=?LntqK6hR*P%k;|V>TgD_Fq4zu@TjHtuYacYVJCrF2*cPaNu-KBRgxX;YYyo1iH z_LBZCe+O3lY{eA&_kh|DS~ddF(7lVDZw6v1P7I0d5{d{_O$0i_$ z&}Jf=h;X#ZvLr|36hlwJ+&S*YMr&ts9J6)N{o*-A80?(yicI$XDA!Phh2#kLbXm2b zrtO#}GMA)dv)W^voy8Q*e$pDPel3T%(c2=Oc?Ya`rqNaZG7;$U0D*klp2Uz?@XUyPv}JlNBHJc<7KzBx~3LWJey}51-Q9+pStL8VHSQlr952Bxs<~C^=eFY=X9!Q4d4Yvqw{;O?<)}h$SH`h6 zN8V|yn%7uI?CxdN_Ec6{Xtpm+`s=$Eg?-TLt9GH9wG^JP9Df|!;SAWj{zja>x0UFaMpQ`&J(Nq3tcPPq`8?Cm*c<^e?=VjML*+3u&g++#8`kruW zMMjofww26d%1^H^#c&c!#XZHXEG}7*U;4}RG!SAE^{la7DBX6sQY00qp@Z=DA3C+2 z#&v7xjiM4krFZ^5^K5{)8&sXs)6=&q<_WVe^p=&CD$kiuI^9s(6x7Gax%W{9SPI6< zMG-D*_yfvdv=8KuP{G1|0QE1$!xow4miN|8Mx?C)st|;j2YxX5ISzmZ%q3c$v1;&% ziI6*xwD7XCK*}nA8K$q_m;lr`u0ydO>J2LDiO?{3Mu4vJh$C_xglgo0SgdvM<08mC z_PYp{e+fs13g7mcYx#Y6vTFM@T6gpJNKHOBnK6c7z9Jg!?=t}6?2Ux(!NKN3nVk-8 zB#*&&<6lYHxyS{! zzp&2v5E>lsij$DEg1E%>u;>y4+tLGHE+&6IoQ)IQnD;Z*@6uxf@u5##Rf$EX1}2oS zp`T5Jg#k9Rdr(RdTq7P))-zEN=tghKzN^{GU3UqTn?{y`aUG6S=DqU@+4%$U-Cr~f z436zg0!)UvhH`4P=ZC74&L)(0u*U+8(X&5f2}G3jxg3|kh7rk90Ld}<>gwJZg9f3> zs`@=)GMaF+%m@akp3aTx-7PGSYPeUF-wr#k-$fKMs@lWnn_{^?<}=sl~Gbx^hDwNndQ--_?qx-}V1*_VPIXH{!Gyo3PpK;i&l7l40qJuRSb~ z=ICodLP>X7maw7UI0ULZZ?)Jr zErdNvXaWiy+{O3UzKC@s`LgaK9@+Aq2WsGei(_l}Y~WI;VTI43gUaTv!Nh-a&}j`r zD7OeVl$W^%XX$&|apqtF4Q2t;Gcy{;A7*A`K(hO1Fq?xRMZ#UZBv);5DDxT@&N3GD ziiX5>zf(`!RkZ=R>xWov!&Ec)GJ?r;4RjHNR#u}jtfHL4aByz#gkg}>CI>ICj_00_ zv5$uuCC^?DN}vzfdC?rp zECM-7gch}mmP?grPGH)!*+j9g=nsP?<>@Pi3(0F=2qHq{o%8o`Dn|IaWYbK`0mDN9 z-D(vRmP^QPdb$E9*z|HZ*!6W@6}Y-X3nVBclqbiAqk$}`ixu@`a1chR|I$Bq&+)l+ zOEMG=gFh!dqJVl(TyX#Jy-lAP8x3}WdmE3`bW_dj5D9O{{m*dk4Y?yn7$U**VZD$F z(JWq|r`8vwV+BJRSk>535C6K%+{z}&W}oYdzdp=Mu84MNW}x#1e`Yq&vD+ns1J`_gy~Bb=1Du&t z+UbVfzi(&uu9$f9jmsLUN~O`IQq0sE$X@1jYarj;&&F9FL?^Bh#tgjzH36zWNCnASc27JtEv;%|m&?#>~MK zqGPP3cp{G(=ZgH4Oks`8)XB9VAera3nT^3eo!Fy^Ek4$Pyu(K&q+01?tDv*BRwd)B z2|7(E<&I~nzE{NYNns>Pbt;}1pop*X=p0U#z9d<}vhF5f*+w(aJNf4FV;<;yn?>N8 zLa1H7&ue_+bJl`5O76!MeOJ$=#gm1blx;92Rn5MHB3M=V)*hhBn2yECq~|F>tBYfGN=UgiQQYA=SGx7{4)E zLj%C2T)E$)dJWR-$ZE@CFF($AzBk)l8A>W}S`CBNkFZ`9L0-nv(z0pwy1)3#Kua@G z=ij$>dMJ-Wr1?(7)l8eQM;|mhS-7EPGQ2);-Q^-Bft?!(*BPussw1_+hOx|)>MYqf zd7!}9NAO|AQtIZp3RP%jS_Rmp zisC+?zph;{GEAzs6qK5Q`=W&SS0X=j{?N7=etm`%NUV zSpSTRp;c_0qeVfL<8UbM9ASPe5(t1U!Ya~$r5HD#-MSo*+;z9|_blOp5ur8uhg~*N zgqE&-n$gV!uG3fI_#VRYB3=!hc@ho`1YA#KOI=!O7EJae&&4@zk&u$!futp?Z5G9fhkh!l!+oXY^GMa!%z7JCH z_&_DJZUmSbV19r>VyfavEs%>j@cC$VTzE^g2yf0|hENs=*MJxYfW-m5nDS`c4^0@H zrxav<@{Ffq;9;DQo?6{N1nK=|i~h1n9fhn?jR9aueE2zX>TAe#>G4e|!FB=%Z%}zE z&Ze-*%e1XsTJSr*0OZH8-N2p%Cl~r6gNxAjwkpGrHk)iXr#HAro`Ld3bM*FvOrh~1 z2X!g`iLEr{281~3uJWnCwqW->!gS8!9+GwpzHvGze6GqJ(kmoyNgF? z_t$a|OWu3X#01TQYwfhlb-o@7hswjx-@blYev&H2PgLp{EH1UHCt+m0e;CP|eKqVE zRQ^U4fYsx!IUc2H@wN8uT3oo8qkvsv#CmS1Va3N-NAmTv)3UNY!>lxX0P?-V89mH?J@1@pJsl5j01{2_%+MdbVbt0!-$t3Uht zRsYd>&M<6MZCHHmEsiNH{bAh?kGL~3Gc{)lqSQ2x@ZIBxcdxa`SaEEc6nm|*$rZ7Y zWU4r&CAXaQpfmlDkk~{NTF%uNEO;S4A(e9N2!mOE%7-|WrJArHX=q6_CU5QOv6Oz{ zm;QBH5ZWBaD%KmfR^?$-Dltd5DOY#*H3p9HmVeY|3yzUzgR}%Nl7l3Ui_2wzkCWg6 z69s*xjOrEmLT!ta*NI@tHv*Yy6!4-7e(6KgVe_-M^1jhQm+V_+%3fuvh^76n0Y@@l zvVTDElU?dbfUEyS#re)7!23U_c$Z4F94olp>;$gthok?{*@|=w`k!BC=jlrANQWpg z6u_eX_I0bi%FAO+M|L04($Xl>f5tD5HWl7=M3_4I!>Ff-sXHLc(KGe(IWG7{){ZY< zAf)>miQ3;6KB`|WEcS1sWSutO%bZY%y(5r7lJBP{@M|qxK873-juudv`ME(3mbT|V zpW7?>7ImyO#5oC5&sN_Fg8*D83Qq z;Ju?JMttZFqCkTTZlUmoD)6 znyP#H)ZB)m4D}R<_V^-rOJW8&F!%sf{C(;vC2;#uxqZz;XMDyDuOoG)t! zqT_bK00cKb{O;73S58Mm^BIy)!1$Z~@LtB?!-(A(+p8|1bHI~yZW}%dY*nnPF&}}& zi!SavZPGW+e_j*1X%vV6!XhI%XHvAbU@te!nxo9_lNem!q9I$L+vpO#ZQ>lTS5 zaEox9!*6HB{J(*+n~R^1YiA0j7+;^TTN#{x*?T`};qRR#FFx`_4D3~eJvnJkj6ps{ zO@IHnENxC08zP127Ikuvqb0w144uuRB0o)12Q93m zv-HoOKM}K56x87@DHE`003aqUh+^6OduGa5;ue}y&xKxHT9g*@3zIc zFcu@JTb<>YLGlp7-r-ryI%T$_qV0eONW=DBaCvFm^g)B!1#aW=a|&&uwIZOQhrDFM zO+EHr7@&k>2a0d_f!lR^HZwDmVlZk1a8M$LVVR?^NjOE)^4GiKe|OwkhC4~2J@?Gx z4;?%R7U3Fa(y=WN$7s&?3`%Qha#_&Ez;gj;Fk*50@!38YS%GK3?`LZvOs3I`2AAmH z@sH8Ng^AE6Gqtyqv8u9tTKeLEMm+%6Y|Kqn^80R$hkRQ z1*$l>;f!+)ZCFi+40gQ7(B2Lh>UWW^u7UF~Pl`fCx2Ebpg5BeXPD)A&jbC=pCb5n{SQ1a3of)NNyMxUMrr380oZ=g}jwx?%&#UH6Ji zA<`4|G}73W{Q_yX0zuSaQW=d?GLcqMA<3!Qa(!eC-kl<}khWe<#5be0zSrw$Yu9zc zXyWg!jrlPEES`P0tuLwCv;j?d!PpZpHZJWy;@X_>ddA-v(4#Bn<`;Mj-#?v-cc(@D zZeR*Tn7|I^7BtSOtw^0+m%0qe6njN_k;1x5x8;H;3`}@ibR@Gc9mMRQgwHC zqbQq>=R3y2*G=KE+JuBLe=vPMS2B#b9KmbSHuM5p_a9exgfmLS&N#-T5pphe=w^X? zxjyZV9vl#P^?Rddusqr2L2`+~0`Q|90|Sj)yzZM~bP>M6j;iFWCl8y^tP87Sq$h5! z$OZCtA@O?0##UK73{Jeh70(v5ZYl#Z0BhlB#!Zc~6!p7mlKjq` z#+Y@1fQH-)zaw!Mpv5RZzpG|>mmpyOw;7b+9TE|AggkBSZPfe0(kFn-0X7n0}f8Gzy?ZVImJ%#l+JiuX1n9C`m4_u5wPM zO7p8gqjCQHc|V(DjW8|h0bXTb&YUM7#;jS2&FpJn{Aa)Xl$ws!%iY4l40Fy2B$R*Em(A+z8dda(_sHGhf6dt$#4J_ zBkA^r6jU33cD%->uxZ>lAbapUpqD!2&(YFOdiWa)*G~a>zg_aLnw}_6)_PU;@6mfQ zu`ledBO}iI&D+cHDPWWyYBTi7f9hBT?X(Unzb7MJXUXjx{pFJ2QXz2Y2|CG_fS1>< z{PX?{7H|KWf*`MLTE@;7eCeN;In2)WpQSU_#n}jV^sSlgkt!pvzQDvjhq6uEuKg^Q}RXn7;l+@7l?Fjjdw zjEn0k@!Boo@Y)UOmdH#oOBkwyhZrM-x;5Dnh3rnqe}+Xwj1F&a4#S5Dff)$Ui0(ll zwrdb9*ldaRhk?>Y5orW;8?B{A#QLvRFyB`c-!v++REd|@1 zYWsj`I&W=An>DV&SS=Eb+%tuTi6c&C`mllcnZt5|@(A^%vyJ=0kpYK$&1aKBl6mzh*9@ckq5?0(4BN=sYhwp^9R2#%+`*V5Ib?BoV@FT zv6$;j3@zUGbC-$+m&!OyD}n0&;Dd#|&#pAdjNN<447I949VahuJv1zkba{Z8S{@9a zlU)m}1mpT<&1)i@ajz)#=13b$UBH_wI)Agjzu%K!bLQ$v#1|4sHn5FXxu(hAD2OY6 z<5=GZNg|N4S5OXj1@UG1*JwWP+yC2FMFiX_b0}DB{31O`;JDO%|qN9WD7uJD|6w3KqFlu_#E8q43Ka|LS z$H85g4)6WLu|sgBEDf_A$Lid`K5W;K=++4Pnqw#m3JM}e4ZcE300d`{qK)kaV`nkI zQ?Kz=1<)TxblI?0HJTEjiGQ;COXV_M8+`YEl@CyW1LOuE(cq9pK|)bcxW7;lg;IrZ zhHj@-SO>Rvy?WcICyF5Hlva$zeqX5^35&FF-zc^dGJvhNuBN4a&MMDw! zPELKRJiHA_=&%_qzCH5ED=TjG-&?3PhoR%8Gq}%P1}1(f#?(Z6A66B{NW1%81e32mOx;DSyNx`RW6bK!N8^tmgEPa!fq+Cn>PGbU ziw*IY76DAZ-o8bP{(W_Y03Ly+HE)1ohSC{HW(QQhY`NEfLgEyZ=Ucz#w^SiwvQn|) zU{E>b5&_L6H9^*U?0HaU=9MM@?2ZF%FxnXDC^9KG{>0q^ys@ABfYH+%^e*$l>8v5; zG_d&=+kS6)WjEWKJs04E$R%vTp)vvf%x}cC^FcZ8QpaPyp8H9D7h>hiG%-7etK4{U zhi{N6d?IqtEHT~x2)mVlo3TIWixx5;;5sj0ZXW#~6mS$E?$tO)#N-;;-k486qdyES zI)n=mCtZq=o>b2!Egt>Jl~AnEmc17RDsbq50R2$COl&?*_&N-9Ie?W0z(^cJ_VQi6 z*fr}s_$Zjvj$W6L9CvxL_WSlg#1Y;qBL9Z6$N(8!zK4;qzfIY?6@$ zlgb%V`;fS$-uI1&JuMsnU=WfQ#|fwb(Za>NNPl1oPH4zs43nKv=X?CiP*3?580msN z6hI5GPk_`B^5T&v6@cq|fic#|0EN?Q z_y5n~Hw)d$Rryq`X}Tp~j7ZrPWP0P=<1J6}r%(!c*4##c=0N;P1+8O<_W(r*xI+BS z#LEpGvjo=3@1QD1QG{Rz%16*Jym|AcU*(khoTvz1{@n!R}&z#H!GnXhO>KlK9Zs+>onrX<$ImtQ5#oVyR!t0r))Fu`9BQ+CJVUln#on&$S& z;S{ILHr>92@LEKeK(7WF7=)gkMSD~LQUfLcsf&7Oa^(7%2>LCCi(7W+G!0_=6{6A4 zW3`$lsY>OJOWmRp2+otG2R;kX|4`gUN*SAy@7@5Gm^Ox7aQ<1;w;P2jPv`b|$WL2S z(*wLx$GoX+KZ5ix5a68N%t>g?oW}x0bj49ftnAZqO$);3TGYEg9S{eOk17!BWWcn+ z$;o+#fcu~Q*BvOa*cbAVg7NaN_xnJ0a21Xc_-xRcBPloEpa{BRZ@Oq1?-mjGCLHMz zA@GHXgOl@NpV4FF-sNor(WR^Y3(788=m0?~_7t%;7ZMVBEns;9Udg`zX-P|c{~s+W zmlTARruE0m*ohTjL{Ka4nDAPVnJ+PTYi4m=Hcz=q$4CoBF3}}pw`H9>IwkA00dc_-`_}0*l88pl;IqO0ognr zXszIvLAL{#cL5FtRZdiWEw+g^wZHAl=9f{S2Acv@bzd(L4YmRQ25;+`$FfPtp0a=M zvoLC7Q3;&j9d|F)9c3}3taHqyvH2Z7=)q2++7%GD&d9z58?P`fCaa264lip*Sb!3Y z5>^fTs8r-3_mSTX_Y3FQ{&c~iiETkD;nvhNLv2D<%Ki%?Y<=N8JbNr^mf}>ItCCq~ z{vzXlQF&$c^(SJj2d_u6QoT*~vg4wc$qk{_n5D2-Gzd0@Q||^SZ%~)q)&&vw#+`@X z>}c1wE~`3^3v7A~xhgN*4;yY4rRFloQ1s-EPX1RnD$Zole(l4h0I z>Hd>^!Nn`Y{B4wB+dhJn={p}4`3GO@IJ-JkhMcr~;RRT$(69Wa2{!#xvlsMxFGpMLyTz{qTd_ zLIf$)%{TK+yy`cO_+PeKaITf}L^!HOfT|vK1;A+#0)o?=bw*$NUrJ2#6wJ1~W4 zz@qP{ljk|jYWH0HyGlHjl~)I6k4~tVn$0xdjHTW|WsnLL)P#gvDsYwILcJaPY1k{e zH;RJ1rSWi1A_i!(m^g_iXW$5j4roH;P>95LVcTynV5%w!#KUL&WlzyU5%Lv0(A2@w zE`t|V(XPwcK7VTPLYw}Q5LVKpIEDC|K`%>jI|RUJ7ab%F})vLs$q}$e!LcMvMZ;EIswL}WtLr4cJ`TY@Iv9wKK%S6 zl?`l*5|WoJcWHHCCf~lc);qZ$Rwh#1QBJX!;!>jl8c6J~gT5#wMb@cv{{LEOA5!+G~-zU8kDcw7Z#`nz@RVM{pVX3v|}xg-mjB-3*L8~-(54S)g?js_u^CqpV?$2VFm z35^q(7%GXtmy`a?boC+~MQTcFtjvLGm98DvX7ain?c z%>Ji3%;x`o;}XrQFj{F49|X^+JW8D(Ir97|bPM#YBzc04M^H!*;5lQ)@1N6ZJ)R{z z$KQU8z7`$>E(IQO(TXmZ3>#i4xyoRfuWG3YQc=^2Sal1U_^$yUhUYCwuA)7Hx?jBQ zeaq0JRy03OY2&D|VJn&2dMU7bC6vdco9b-xd~mVmpap-E+NUYKoO7aqFn;PAHx}9? z$H}h7TDFoNUdpUVfDZ)Z7BEUi1Ucjx(3rdHe4aruP#k|4@-IwXpe9{wbqrm-B!*sl z$9NpItFQh3C8T7?)!*a*>e7=HF4Qfs$wHrm^YjOZ;Ot8uOtw%KSC*CO=NaV%O<}(h z+2DbFA(w!Mhh}*0M#S zzdpVks|s{i@B3SD*F;QSzJ7^s7GatSrX*-^bvLOYhBt0hsCQT6w% z6E0?{jciqD0ejks%O-IUr~(XmtCFO`;^C$#*PRnMY6g6m1Tm-IuYY(y={1_p3pkpj zZz6IEF|$rZ+`oVSP6a2-H!)`2_>Nv|C33(910arvPzLzDxCn}izl6@Jbq9q!+!zoe zD0WJt0dfaVFx#bdk<{>omY)erpP?;Bi`PA5;Ig)F5 zo%mS>x>VMR0QU2QVtj&92rx;3Zrah}BC&waSJ;MR1HXt2?GVBYqz{$ zRI{+8WHEE6KK1K-saIe@AcWtu_@?tb#Onf7n=iMi>8`&Z*x#H&oaY8@Kae0u`11qD zet)?}3a+0A`%K&=fPn?7a3Rv25Q&JLU^L-3aj|{TC5~!709CEMIF3V!shHt ze?!xjM;#4t+4geP)>;2C`%taCok;=oH=R2%k1=+)%M3^)Lf^VCv!`j4OTUoqLI9Ltz>ZeK z-ySIL1J;<{AWG*VtuTZKY1UAG&l9iArQzK_j=c>YBx{Ze- zR~5QPEomwkYy!u})+E=bnV{O(hqyOie6nKw?LiI6dkCKdAn?=B16Kg89=U}pOz3E6 zJik0&9%^I)xCK(RYH4flMParUG+4!kji7p*w*tzKnd>lJX zX`_?4qv}t%i<2uV&Lgr8P56c25pn(y-VpTpc=6l`m~?=7C#>@kukx&9^bwyLVE1mSz(L>UJOn->Y8_AC z@bK<5NPY)Ma+$g!;Gw5Kc|+Ng6OQx4J~foX{XGK%zEHHueuKrUz#PJV=+$2(r&h}p zkk+u>%>?jwwrrk){FsgQq%Y5jg-GKu=g>_5%`MO-oiKjL)Qx3`8mW$*XD4v1Oa2_$FxR)$NsD;wWV(GQKTb^>0j~7chwnV;_6Yj%74EkCH5To|9YlM-IH5 zE=FIFG?I^ZWbG46@P=J22&2$5Pj%uC@8G_e2TgJ9A{1 zAfF6*?}l#anpvBJetd>>(Q!7LX;Xf`o*c77cweN+n)e_@6m2w4!P5N>LQ z0NL3-HKxNv6)^h!@$g@t{;Z5$bt?*ZFfUj{7_yM7J7P7Ds|c!2%5zwQstDv`Z)SIi zEr!z|XiYu8qeBHe929EpR57TJrg@0^7Al43H6u4Z3tWRQ1vm|`1%$WHHMnpx`97iD zKM=!-zCLJ9!7|_}6qlGqD=aieyPo;L^rPRwM$WNP3Wd@s?H0du<4`TFUD&+Vhbp$A zumGm$dlldj0KtMDQ!S8FgSK6%D*3vT;2#e^e%?9Kpn%=~6!bCldJ%v-K_6KS&^Hpf z41V%D4467Md{^~dh3AhppaLvI#*hjQT zckcO%Q^=dF_K(=o@bW)m%UX~#R*4499Vmn$av01$he!x@ zx%_@VxYW{VAt$yx*Up7^JjC*Px_@?=gUSGi4}^O8_U$DJweY+0^M72m`Gd=0)&``M zd8S^8G@v{5hEE+9N8f4o4K5bos^?+7g7O@09v|=qU>6F~YpBuah(8WlHLG*F7^j7k znvvzyoAN;`6qh}o3z_Z4rKDBFLVg0~aeB4dYKxjP(q6qk}U6S0-AZ|*KMGyDzM z1w#IvTGX<0T?Fc%=F$Tc97TSOu?Kd{DOTnNlEg+zK_v}+KQd!eFrZ;OhkBcs_}s)C zxCKjZSj(VO09BgoP;XDqZNM7*T<-kSKL=@?_&YW1VuAFjw%*+T^rn)4 zi>)Hh%)|gkq{}iWp0F*_*TjR3-%)7mG91*nb>CfCa0gXobgMULvcO9yBktJ1zto;I z!!lF!A@wG{m= zlsI;U6%>NrgEaOah#!|{`)W+{fUJj-mVNa6r3X*HANQ7Kh!Jx-iVWG=KpEAwQv4;l z&3Y9ZJ`_iKPeOQn7=#$=>gv_57J!f;7QjyvvC-u;fcyN$H}l@Hwubdj0t(fAW)jNG z8fw^oqY)!jp-Dx;j{*7V9aN<4JIpN+TexBoyc(ytYSfVUEoI%ytRezG49&PNYuv5j zMyxu>Vhu|}Hitpj1(p(U3M0}<;8~$%xE{#r3pc<0kUIH8P|bqQ>>xlf@1Z=_i{16R z=87x)|JZx$s4DlhZFCw-Y*cJe1QA41KtM!LL8Xxn2|)yr4pERY5i#fvX_1l=X%&%F zq)R|a5d@j!B){u{wf9=@-upe@_l|S^IA@H*U^&)WGMUeO;`h7n>%Jla?~klF0-M0( z3Y8imd;yRE4k@H5Rb2->#`dil%nEJHz0F>0oLML_?!UB3tPOHJZfxuQy5VQW`O+1S z0`zk~NS~Jv0hHlUB^+6?NW+B;8_ZI=>nDqFHS+#_*G|$raC2)h=!X+)K2}?3 z1E5hvg&4w<#2n`M%L4Yka`8SKWCnEzOSbme3e|N8N`Uyn6&tPjyHprefR zh4nmb$n+YPoF%{4+KN8Me&d9ub60#mn+`5_Ypf!;61jUHcS}V~{@FV+5cC89lvuyG zp)e+aUECv3$%c|!1j)pRQ;ub?z2FCfwhH}~82dv*gM|?6AIw@o^8zG81rK@Ipz8CS zowK|i{sJB4mAC2FuO@--eJLWoZIf+E#XJ(RS9y=oloKn|KO1Oly~zH$eA1$diR*^_ zo_;C*{uA<;+#Ct9?R#0cR26u>2WS#M>hrdtv*aJs&GoX}yH|VrY~N*4?T2cP=pYjl zH(0%wNOJPvN$AJHC9m z$5w?Htg+7-lr7q77&=0^bWZ3~3fEFe6k|!qE$EEBbxAjgO;^t_XGz4%ik%y!B#E^D z8OvbgMPOsFCqXEL-XMN)T=RqYLtz8ETXY$`n{Gp<%le-5t#Eeuas;J3Y{Pxbj9#13 zhD1x~FdNn>4HJs(^1qbg%F+io7=vShVc~zl;j`Qy=Av(EE^s%qt3tts_oQ7nHK#h_G zrX}XaW9WX=GYmJ8h$O~;&K=p_6VzrI2nH5#GPK!jg5W>^X)NvJJp&7SGgN&@l+ zGk!>q$q05PGlw@oSq?)OwgF}f-KDn^xjw1{jCRj8-uof;AG*YAG1cS@*8Ss}_A+q&q)n zR&wO816zj!Tb{%J#1R%f>n2a}Ic;Ki`NH|1O{vlbU<6DX{-zt-i=iXF38BG5&(O%R z*T3ag&t20TwXTadj$M0)(@p2dBZ6E`a6 zE4!U0f4pIo;h8OUpUlF!vFdFRceKx+c0)$r$zv(oN2GLxuM{z519w}VoE7cGBp)?3 zCwf&Z%F~xru;QWTBDta{Lr*jo;<+(~whDAWFuup${c(Ck*7CrXFIp~-G;+t*l2G?! zV}EjqTiw(Cbidh4(9WKtiX~yg2Y+T-*17`XRYEXFG^>{mX!QTp+it(0?Gr&gP|LON z@{)%rExquea#*4bScfs;f=L{munpei+q&(EIPZR$I&Q!F-uK@=rm9-hQ4~fyi3XK= zc_2!a=xtt0dcNFTqB#XSWD~@eR9=kSExpJMSRS1U7#o){>GuCJ2)L_im#6K;f|GBa z&GtIT6bcwfWznoAy<4ZHUFXo=STRJ-TiT|SbS_?`#StSvc%YY_}a-j0eDRz9n~3xvpyGb=N5yVk06g`e9T1MVt7S|ZVRo@hh~tGTvoWZW8!w;+^+ zs3oB>>yWg6U~p5;ks^2$kQ0|~_PC@wX12K?C|dFx=a4-FIj6K}7x=qVt492kuT{!k z-pQ&(SPzVXN@iwu7QS8Ho8)Oeoj^7lsFd!Ak`>#2RwPhn1zCs(B9gx>!sJUG-u9n1 zGGc*kRrz}X^RF+OozrXV6Kw~RvsaQnhoAJouw=s$CD-iOacmQ`cDsNLw6?A<>P&rM zg3gBsnTN{hbU-{`)eyuL-8-Hk##huXN?HoiSAOG#n0+joN=)GQ6EG{ z-#vLez}#*v!wli2x8^HAs;^H&1aJN>l7?~#O2@u8}gsqwGbZ^*4fXkN>_4!$hDOShTI z?pJHc0zJ?(OoY2WtP`Fc%3u{R3Sz#m>NBj42rH|9Bwa-yj0JNhSXQBxe%Y)e;x*l7 zQ50tF2;ad9!EzhWw^UPgk^@P{+Y6`DeVE70%*|DYZeUxg79QJ(>itSus#m|tscwTe z4z;W`O1f1?L{Dahp08zkKfo-fd%!Dab0mh0bLP?1y99k`NH3&aoIYR zsXG)Mbe0mr{4;%$q^#<^6_Auvi8>G?Tbdfa&v|e zHJ&e1{)-h)*hql!e<$w#2Tl0k@jcek(i>JD`A_X|`f;tQ?Q2(m8@)*kNd-mzsi}-< zw>W$D{!b#ssiCQL!yQ**MM_Ik*m}OQkcj|5+J3Rl{y9#T2`VkQDwL7TbssrNXmtn@ za-)ydcvbn0<1j5aUNA9(`iRQ=$xiROc==T%^P+jX*K)GCgNnIh{MrZE^t*J&~N z`0D-pN*_~6Uzij)wENiyD~g-D*^BtP9&oloez^153VSt>gDosm4^sal4(Q|dHdL1J z!rK1f5(itN&p3FSX16U_ag6lT9sci!)W>2=eZ)If8BYzjJ6f){7lF_hRQWDcT8BYQ z2W^s#ied_y5b{uu-80h&hv_p-Whx1*^d!~M{4|@MCt9)ElpC~bxs^P zYPv^Lx1O<{SY+<-JWun8CPPvn1&lCk3#fNkeYoQDtj?x0l=hFMMQ$KCz&Y{Lv_=Zs zm6+`&VS{CDv}}WQq-*z~wJZ1E{I=qNeRS98%`XsMJuQdb1Nu4)+8?2zMU(axzvbL3 z3?2zZ>o*+>>K3KzOcv=V+bZQLs~hhekeRNZhkJ`cPvV@RRHduleg7{y^iFesNB_GujtbFPIvei1eQIT3a4$Kak8ay)hRgER zC|?P?1NJZw7ByKFe6#TaBdVMw75g+y^!wkc7#SiV5aNmpHCH_DbIv6 zLN;rV*mThvj3v2voA=?HXv&#^Zv{AxQ5+w^D#fe89Z*JzFxR=Uv%!yr;feg2HadQ@ zv0t?{cQDtr%*H`LnWXalTw>`uESMG#H8)|f**I38g3%ld%T^peT!c(Mrr3^?FCa>>1%HA>EgaXv&vSSJm=e5_5Z=+nd^{-OY%o}ZQO z-|b_wfag|aAo}ZJ)LP(nP$WDny^Enb0WUfiiC(t2BNJmUHyvM#-TW_at7h4jw{Knb zPw;{#GD!j0`RJ9F=ZM9nOUs!myyT()arCpk8!TH$dJ8xM zlwqH0>oR`b9(+{j3U^3M^7Y)>l2D&$;$oNHv86NHlDoa5rP1 zm9&eX-eT>Bzb`eU`R92*fK-LsbBGz%ssNYpJ;@}bwj4RKrYEB?@bphve_lL{;v++?1rS6<* z%--I%R7=ZH{n7S=X_pJ_Zqe{*;mg<8y?WSrhFNL1>-6kguyXCa@3Axcle}3mxF@vq z5}y?en2n!C$Hg(*@v{8_Y3=lc8WtH( z2{wOfhCA`JtjxhB!S9aQ<=KAu$*Hf!sdo;nO^Uz6w|xz1Cx=vKez>qb&z0frCk4z% zmH?y25*2~ergf|Z7#{c4)rhe(qO@Dmxc=~V!+0fmC$FolqQ?l`zJ*1xZ_b?(JWtGW;|u=9E7tPkfAu<0Bo6#Z({abUz)j<=zUDm$f(z4Dft(A`f;fM z|L2*UHD7Ed{z$R7&ER{L_w4@vWgto+bfFzMobE9a%nKZocTgM&@*4?=;3^H_+f zx+K#!J&PVmBM6F3r^}bz>UsZ$+bqO5rTlG_zXc?DqtO_5D`_by%l@z%#Bi~W39KKu%N!BW_vq%I+;+pyq3&Ex1JZ=%8iV@kE+X6S=8s4> z0+(l-Jh-njh}TH*Jx_0PW)T>D-y+@i6QXDwLovCb*IEyRikCK_WtEFSniSiaX-q=aqr<>Y$z=kQm-HY81GG){=A-?qz7c~wSX*pJX-&UA0SI+=yk?szM6 zgY{Oi_M>Xo%9Gu@0}2kX)Ga_g#+$7*xYjO~>+^$lueq|ngsu0HswGV6@fng7<@@LZ z_k|&wT-qBs7Y?s3(rsD8Ub11Cbg*wT&3{UP0JEq$_Y!v?58avIT~cN)B!binvS3N> zsRP{K>rCv!PuV$MGyd&Rz#ir(e6@Kww}Wr))w7Trs=YM_TmE5atPyD3I;X-fS_w`ODNDuHYzW3#BOQasMvrsvLg z?-t$sA$I=-3CI1S;T!8l&lwt@vEp$;g=;DJ81rZ>ut)ZWrsJ^#ucGKvreO$H5r z+)*CMDmwR_dWVewIO_0J^QHYskS#)aJ@;Kaf8I^M%@+MsnpqovNl*DUN>8EnvZ2;7 zxxWKmG-nl>E~Z;cS_pl5L8ubQRlL>Ug<=dHJ8t_$Oe8=SW7>*W(gl<|2BC$8g}!6Y zVf(eIlc&kg~fB5ELmkIa@JqGLb)rXo1xuyU)uvWADX z;6aivp&?*-a5lK5E+Npsfl+pC&bgnr;h|1o&C8@hAWZAVr)-160!JhY|H9s+PAGeC zIb(ocHbi-Ge4q|?SwvIAVBmdqc!00_9dyw>P0T>fQx-Jqnzlqe`WYYue68#l=kWD0 ztg3jP)w8bc2)V&hr^UbY%6p!3wO@aTuPqk^A)_Z)#)kz1Vu+99!nTJjBX8a=Gj2`9 zKJVii@#Z+ams~F8c3z2p?=wll}QOcuSf8=hW=p z8<3j-G=mst0gKwQr1|}5!5<5`Hb7${n-H9FP%H^Sfg#@e;Q+x^0k3*F9pRJXmKqGD zfZqTICZE37voS3DekmZ~!4*M5fUq=jDs~ak3&F(xGCMVL9)u6P&-6EASSi*wH5EfO zI5IW%Zs|8Kb4Siz-FjiNcbjjk)CFrXeHBR5g}0UZ^>@Fes`zA-#CcjnI;yFL&?&6) zso?j&^l?5XnHn1&2B2`*)qR4V3O*QWO?9FJ~H zF-69fSLv{T1C(lHYKaN{Rbyj?P-+|r&JHnn+}^hHAiR?xc*QV{&?ge?o{1SiC*Df+ zKln;Mebsv8ec!zrST~y!WQ87==WbfxZ8_EAQXaMUaYA*mGCPPZ7ty@~sq&oL8rMrj zhS8U&4|sL%hbLeb8@ z>7{$t`rDxHqL8QQRjB@`=|iKy?!#igWMBpq8A%$1u1bPmL^d|^8*aB_Z+r8M4gyUb zIONf|M)8NCux0X%I`F;Td_9S-%A@<|hv~ygRe^S&U>+w+oL&bDII8 z8uUF75DIAt?g~%So55E*+@U|hZD!h$f4sX+Nio0Yx&cPjgaeTt{*YW3@pQtq`tE-* z7??MR9#b8_SPB*$cvLM4CU%mZBWu3xnl-L5{}3l^(z3F4I2Y+EG=bj(+v+<{pFX|A zDQ_%ThxuqR)m9(Of&Dw*UAmxDaP!-3Dp!MpDgcYo#S)hin@;*ib>ol(#@!oNmUlc+ z?B>OKsn37SHu$}ig)~2t(;6*Fe(rK@%2~QmYMKaXrFbm~laH&K_P+R79ph|qba_BC zRGwp-A-1;02-L4*-#B%MY=u8!h(Raeu9ZLC} z#A#S>oIaW}fVM^1jq1N)|CoQ~kzJq6+@8iSAkd9Tuo&vp6Cy_`4t=2XLj`l>b$~q6 z!epC8JY6Szs(9qcUzc4H*qBerWpfn{#gsb&X5BISC#UHyX6_n8CO!5$z6kC~3AdFR zuiuqXAy3WG@5SeCnd|QJq{pL`sfY-BX>L|;9shlWtJI=0ZfYcrAlWegwZfC zM%zTl>q?H1lJl1!x#I_$t%L8tr&g1D>V|^6{)P!Ii8FEF$8D?358xbEmzB z8crE^MGB@AUeyu15NS_vk$RM5{|%F+7GGBD9K{a4|C!r-*?DVccc^(+EBA$KzPfa~ zYY{v&P@2#(!o=*8q`(X^gMfSBla!1~rW$o*7|FNJt12tM*pFsXA?%y-UwkV;&B;Rv z;5xLM%ePp-R1LQEdKJQ)=jwvYoLFtojg^c{CCZW68ct;N=Fvfy5(;Y1gt_Mf>J@L7 zJjV%lq>J&)?blh`?L$)Lpy9YCvi{E7+F1yiM&Hr!92ETo-err{>1zd7{|3cUc;MrE zfb#aN2MYf5YQ2f5mWWLCWisoc^2Us+!>ID} zbHdA9mpsOgsmQXAp3=B#SM|k%z4858Nn>WfC2j`hIr0Fz=Gcp=A{{2$BoD z5bhvRaAUgY_y9hfsyWa=aNfY(ab+K=8rsU_q=pX*^y8J~_cM0RXuh zb0>{o`V0eQv>LyyCaF35x2=g!I5%^9_hvVwKN1m0JQV9V z`iD`6VQHJj@=9^))<8MiqL{+=5?}W)ClUIPuPuo7L+SoV@Z7pPZ<*CFBFEtntkKdQ zm`Pl8t(~7<3L{S!7$EqcAdQly9B!3`zdUH~(>q2{cdlT0wUbZgu>(w)50h1#c~csB zPdziO#m!$X5KtMbPN)xI|Gsk}QFs246MrxlwaD~=*|~Zp`Lyq607JtIi2wvP2uO=m zzPnoZqDC|IkJFb*4;MskNI&kj`gB=y-I$NOh#n0m^`=dF!2Od>-xam^#3|}l?4>x; zL(w;M-g~GF6kmdSKgS#wr(}{N4XiL|peh~!!f2xM=-B7-&W<;P1TruPdo2*=9mVvw z92_v)6`B6Fs^qn|qfP)(U-MpwuCw)|Gc@|}m}*-=xZz@V)|&WH31UdjA<3+G8IGMC z7%d>1Q^jIs?Qy%I-9_ucHPGW;$_>~<`4lww$>c90*kGh3q(yc~*7N1rq*xnr%;fq* zBky7xX(UB%XE|OxrdaGBx|A;!`U)vR>5@jTCk}NI8ZKi~Q-kYKgugPqJ0uR~>koQK zRLn7d(QwPGFd`;8$212gponlacox8*k?Mn8z;1{fViDeoMMSo+rFRPcZ87zEK!oqz z4+)BJI3^%aJTeuXovl{$qO>~x4<7*bPU1htI(fDDIxP!+*Qs{~J(Q@tZp0Qk_ z*$BM7H!N<2u{n!M`m4ku>3#228$&~{YVr3gB_yvt)R(CyL;K$n(0C`A%fy5G@3|cL z1&c6?P7yEyU?}6!?khu#4@4Lxuc;zd(VFT@XGh z=4FKT?Dr#snTDSpK?C)g!{-8nK0D?v4Z>eepNq%*{~$Qls37;}f?=iD&fi&8$SG-~ zoN$tx5Or0ZdLpN!K3Dvq=+K5b>JT3XuGk5UD5E+*-I7rt%9|3{+B_Fd#IH2oy~10p zApIB=Sm+c`95T>t&Y-`6iWpm3bCHWE^d$73w!pU$iqKafd=PO#5P{y^=ZBWY8*Zfy zEHy_WCQ3FcTC7J;K~N_r3@743*h^G5J2W&hcdS?N3$40XNaSQwJ&|*Gm|h-YkvqVA zRvqP8qRnZ)@*EZ=tQQzzBI#m#e{pu<1V@P-(+!dNaivqGJfn5>;1}Q+8I>M}&3*k#nEF>N`oOZD-fTjz8FIFC<{rx?0+L zMZU#x*#M$jqm1fk5*4_M;O8yA|!>NJqpsTAqfM4W-S&; z|Fp|_*w3--{BeU2E(a7-vI0HWp*aIQbj<=g$S0hIN6*J2KB-CTH085=4(Ps@VHyH5 zJvtHuRk7M1$AynpuVs~N+90@(GMdo9OKF?yaGo_g+iY-jFqf&&Yy>wOd?4`HZUyi<-PFM8hFQ13HSj#XVF(pG0k+c#p_0TpKuZ z*-mJpewVvHS*Z>c5>#f+S%ypYD(~L^vSWLXI-x%AtzkyOM~yW7KZ=g9SgUMCDe+=< zL|*IdSp^wm0*a=(|384DkNALC;N{epdiq*VY5b)3CS&sq*aliTzc1JC$-QTAIe7e5 zKcu`%fQbI1JsF0d6kWo$8+~+wv4Z*?5O1c7Mk8aklvJcaAbkOve!jxa6maXb9L->_g9HrmM5Q!$W(1r$z@ldw_NeCDDmqD z_WvKE{{JLI{eJ_U=5KaHloMeQXf;346de_{5^Jx1e?9n>bsrlBL7~x`4dyj^8l@9+ zP*fy=FV{*-%rQcSlrQcRy*gEpULs|7B zx^6v*UcgLL+=>ByoXtg%073uuHi>1-#~qTN*POdI#%*--?7OenMo$7nh@?!VG|nw+ zWv0h9!&yA{MQHBDo*~UbPrY|?*D@BjAgZtjs$SF>c6W=L3=vXSww#!A(}goE2s+xv6ET8+8{K08}gI~c1L=1(ZnhwKSgVy zbL2}?Qvgp(p13my7O)M2$BoDHKiQfB;RUF(Lb*%t&YNu_7S9>NHR&)n5BWiG5wJL! z{@@j@*RT+J;+yy3D&^fNA^TmIdL#F=TuDaph?o2@TmKjZa|30<^#~|Yd+{t!d8Vks z_6>&4>w(}$7w%>i{hP~bZQW^ViWWaWA=YBfiM|cqR@i$LKi>s9RL3$EmSl})!lB%a z8Y5=5_dPvg*Q*3?@cKpYQOOGhk1+yv zthC|Hj$oE!f;jirEK$gKEB$B2s<*Gez&}lRN|@?sjL(!EQ>1{t-->w*GXG;r^SBbT|d zb6?aPL05aY6IZ`Cy{Y_h!=b|_3M0SnRBt<}^U;M$Yd*|+k$e-rpDAm7surxGoPPcE zu+p;U->SQxYk}c6#YO%JTS;E+F8LO&@?-#Fe$INsWBR&!s*3AXC1n9N4#PD6)fr(sGguY>)y{|OcM)|YE@fu2a;nx+IEPcoD z1wRV{8vTp8s)%>;11X1&>||evrWK%f8xB_|n3ZcNd|1hb0yQ7O)=T z?Sa9~Y9YI4Et}!roP#8`23C;?!v@8FFkX!s2Uy3)oOmktk^UG&n{VCq5qu-|d--fFiZsM{Y+`!IC&2K=&DW?w(=7*S;% z@Oe|h&_zv6LQs3y=ZJ1^z=$>Xkv7{tHc?{mrT7&TB`kv5id$lOE4aGee6=x~ZhKtB zQ>3Ptz#;HNBgPV1v@{?~(ZF`WdzR?nmggIhmX{|;h>(A6giX;UohRkZ$W`MF*r&gBzvRRP@KYaiuTqB#%= z`1;n(&}i;vSLdF@DvK({5>RTfw7-gayknSza0XMMMn%2{}R<*gc${2&b;x%M#t_Bj+AIqpwOC&>I&H zKD-!H?jB3_;&l*_U~Uz7mda~A@Ki%?gu<=SpBz4u5c`$akHL0WzRNBz4TM_XLF7*W zn}q`>MQCxF?`IBYZ(iYVW54d~-gS3KNRimVrh2$y9{e-hX_As`e4!&W^G*kd64GQl`V-<_-&N(HE86y(f|(t#gZxz8U!rPfNi;+Vl*YG}(ol`br(arL>nV z{W6qx4&761|8DLZ{KVw_gkbMQdHaWB7j~I8Rq;fg3vZV^8(F3)EbyvH@@+K3+o~N~ z0_CRl)*lUGYMgbe4;xmk95&A~XjyQ6sqCLO`$9f~o?~LbM($#Gzj*mIVH*yvlJ~<7 zOh+%$_ucWEx$wimB+F6r%5@^46N=S66pq-`_P+FmpyOOSa`V-}gan0I0ha7uBiQqS z4Tp_7h9nmn%{m`oWQ^+KNp%qNGhJOV`T6T*NiMpLiv4}4NZgKxcyu3r-J|IpB{ z)b=XA?rXVP5U+K7$JJ+Rd&4?SBmN+PVCLh(jp(QRvCk(0gM$^#2>T?fCe_?JkMXeU z2`uo*d;gD!FWPhl(p`T=lK=^hOm)M778*Tq;r*r12={Pb z7=M0RNE;5^95S-9?_m88!2>YLWyhx-n}hQ=xfNsj?H8H?N*hrsl7I;gOa>p-Rg`Q0 zvuLR0y1TmpIfB3MSGPu^E82i>u`g;tW9j@~-6Jc1rBGf*<{LH(oTM{OP6c1D3Mx4T zI9P_kiBCyM3Bp;wsl=)IQ{v*{C9zj;3Hg~{f1xA2@cDN7YucKE1<_w7_dK;0ifF=i->?VuoL&hmS7(Toi)}HwO{reuS+RqW9 zsoK7N#9rfvbpkcT@VwKh+dq>(m{NSP z*Eb}B_AWAsqN8C>MsJZ2NEV*bH-D!zcF}Q!FGtdA_h!*S%}|h^PWh=ZIu0 zrPM+f41`+G;?_`(dGxx5HN>P?Qfu+ad_gEvlYIPxe!u!xHSS9hj0)Sq=!EGpEdFkT zzC?GzZX?3Q2lsWf5L7hR&+R%eEJg8t5C(M^c9YM3FDrX^?ZgRi)hP2Qkd+6?r*n$% zN<5a21AO;`@PPFiI!TIsd3FyO`L4%!Zn{MrxZv5gAGW>j#*~Qwmz<;WKHZxe3}53`UVUo+ zX4mlxsaj8-JtNqxD8ZnmCWnoKM28xQu;qeQr}dIQL^kLPb;H@PaW9*6=CGVDg!o)B z?;-nP1N(B8yuljB%W)!AR|>MEemZrJQC@-mYfsUdN-1w}eBm;dmXCX-rb{{ViJVk9 zwd+7e!c^?VeQ&S*+PR)Ld6nm!cpj4vpWaRPdFql#`%_z16CmCx2f3XmByDn`2rl1s ztX2e87P6hE1HzxjJnkQE&-*ST|F!_{LI{R2WJe!qA7rUwuhwBYmwaJ(WFS65ReSoH zZp(wH?RWN#X6EH@R}>jk;CGma{F2n@mXnmNS{Yy4)5C=3nnV!(Dt=^n4=%*$+1*#J znn?Z9TXN(|VL(7YV&jUpu(;}(au8E+fB5j~LddQsawN-NowWsF{wt*y&b6;Ho{<-L z(8giUopGbqyC^S>-QIpO(!K%;8Y_T)U^x5qA3APNjuCcqU+1qVIKc zXeeJ^C%pmzj8ecSDZF*-Ru1-9fp%M;h3*(WsqEw5)CW$HH(Z(#%{2L?pg3?!((Gkq z|4iz!Ct|v!Ln@mE&8eh@EicVXNq*ZKa48t|(wLgsbRk_Ni>rKmwddTAkBh!8lp_Q`2e2;{$r1PooI~+_6HVLbUHZ9G zg+I`gk(FH2TMC^{fJwQ6#FhX1j$Kw#d^{CiQwo9dkrDY;BQdd66%`dE67ty@Ikj_{ z+-~8~TtPz5N2qgQID8NDo_F1&-(ZC<2KFQA+pePNKM2tGz`S?@rh!DNV9{**2w`Im zYY)r;E@Ngya5>@oa2tEO7|hW>@BpZ3d^iiJmwhKIYRmU(vsMLQ&P@p zXzVE*3mbu4Z*O+HT?kzt#`(nup9x0=^X+`porekD$K0S=)z(fJViR#X#1CQ<_o-`u zv*^UnhFqKI!()r!vo^IGG8?|X_$0o8%y=}!g-ENL?wBfcvv8ShQ!jEp$v6-%Zp>Qu zPUhqc_^vfyziuSq{fU4etJz=K-4Ke4L!bhKjukO1>-{TIKS()UkYd_)bNahmnj^(} zNm32Q8st_SrL!Q4d@9=z8&nbF?Ki_6FwrPOQSQs*--Ppd5B6tN1icMOb3_P7=u|ee z;`Ru8-9=0iMCN{r!75~~TXf+dziIP%*TD5TJ@q=azESYyrc8m#c^;TNP?+{ zC$ha7BRf`V>c=Je)0AqLl=UVPv$r2UeE7(t+yA0@P{AOzYdXEqh4hZw1ENe;6D$wz z1ghp9n}`vL>9?)5HYJ(Qlq&Zp4P?A+xJ}#Uivp`vs)yz4l{YK3_Jw}T>^d#L`zzJp z;Dy&y=gz+}5tFoU-nZjiVMQqDG>~+1ss;rHzO|`78UJ(Gf+^HnezbYgbl0$ymjqMy z)}M-R&@VtykkkKAk#Da`2_ch!g!rWy)96h)@P0v2E68W3+ibMa$g6Kd4S+hros>}j zuy0@gsr8n45c37P2pu})Z1mi`R{ZYm0_aK@a`rA7RiXhlxZ1&R@T^>W= z+?U=%0pr`3fI1D9>1UQ}8#n5r|X8ct2lOp$wLYbUmcMNZ}b;k;%>!L+)y&P+2 zEJfb%ZN_4w(?%~&e=et?meaPY28+9Tre;+Z^RzgjBLpLBzi0o!3jQhLwOc=NAjVo; z`DT6@bMEa4J*!~j^j|n-Xkh^eUQ;7}L}4qpX#P_i)QeM@G@7N|R@n4$8uek>Y--7f zuOs?H1E?&Pzg}OKXCeM4@s+-hHjMZ*#8;W!1Ggr$HguKyT1!Z-Jx!7_5lx(jc;r$Y zWfxi9d#GL#Wfk$oC%@a=_Q-7HszZYlv|+?H&=9eEW;dfYyDt7#Sq4!g;8UQ})r9W^ z_{7-nwe$825UEsczaAWZbQ7-an2N%=C$9bR!-qRpY)(B`f%phCyQk4{+wItczr{BU zvZA%m8hAj9loQIT3r1VsK$3cAzd&C^!A=K<$?@@T;9J6=M~t9d9NWW$gq5E^(Z?bg zGb<5SVkN?mIPbm%>4f4Gicf%wqC(W0fGxVH5JKYQuy{m;#o9^lV2He45ZT19T_M5A$*l zOIYz8*}i@cr|4Kv*u>!o-|NcdQX*-EM%6b+5?GuO@QLR}Hr|YZdvPesvyOKub$G>q zO)I!AR^%SZhfy!uw@U~l5jiZd@A9krP&O6ZNqBHB&3WN?6Ej!?0AsX*E%lP4qZX}! z4$2iI_q~PlMXxQ)`ZTH;#KkkY)8gw^_BWHhW~Q!=rS7pjpNaIe%i`>9uZGy{MdK$U%;s^KPF=hhVz=YdDw`k|)t?F?;Ib36 z-Ttv&x(NN$5u2b9LKClW@uCL^`bJiPl96PKl(Ba&w7NB3jI;A4FsvO|v z?kWMJBCNDw#N75a87yE>15o3GY*r4R`le_wt_Y?hquI#Y0b*gC53G`xXa6Nwg+z3Wn zz?`$bKBGyuZ!Jv5AnT=ynTnp{#HQq#-Bxfbo3VawCmO;`<7?MyQTw(di>6=92q)EeuL?QKEKRE<8r2&p_F$iQAz zS;`VeAK(a^FBv(xm#@|2m=;jc8PIiN9Wa_Z+6k{s5HBnA3|j;EM`F(F3J3@!PM$b= z^fnO$Ik-BdB6Izo(8sMZ!iNkiJhUlB4;mc!F3w2$&iZrnMdl7Ps;R`jxR2DVbMH$sb?dSODI_B?NYELsOMEx*jRYhZkU z&JEtHg5L-@$e4{>I;W-gys&z5{!$47p@`@Hrtnid-&fUW8U0)i(zp*|Ap)Q8KFpuK zeEIUYpD)_iieTTMz4nw~A|iRp%O$Y|gA9tBXKp-R^wSGB!=J^bh9>|@)Vwbam)j7? z7obE|`nC6d!gyzFaIc0SKM5CXs!2B^mElG6+`?25RV|G}>rpO1C8c1>Vm^z0iP!nn z3c~0q)58G$2C6zTwRsGSm6@?|O&G5c7?Cd*fjGlE52G|VtJUP&nm~~b|FKhMX30ON zV@eOv3)vnXbXH5&jJ@TE*XPn;Ug`?5awH Useqf2-~;lS|R;JoDC2#6fGZO|i^{2%a09~PM>;*1#lSPL49)*g(a z-Y2vg;LFtPC>Vi=gZSBD%WRHuBgE={FA`)cC3fr%fl|2*G|tSXzof{g>9(Bk`NkKb$YWy71}je1^5o5nKBjp5X8b#|U&2 zVCo({c8r)m<_>bPFH5c9ru3tq{EabNh39c7g2$jGl$+ z5PMiV9AV+vxlu;h0o`(9tx8K7UtQRZgVydwDZN)Bc`wqhXASrhO_xGy)=$DC8206r zgDxO_l#e72kK}dEr_3!xEK(wRzhCk3@qwM{8%Iideh25@v9DVg-JpOE4Tm_J?9=+y z{6=Gkr>@KVF`~n=q@v#MOB<8fNo|Ma#3Iv!+z0b&)rkYvdF|7)MT@uEZdoxn$pwdo zDsQUueIQ^MLUKwOT!pIH#ne`FV{P$J(qX5=4THu5&&>G*WlOySMN-?n4H^GO@aBU} zFMlPqreE=xyG(((MDC3SoJXTM1&4+F(Ur0J8L3S)lE=N$tS_Dfz0-TiRgc%!&4D@R z18);K{R=Q&@~5XWt}7cZy0?&7k>K!B?egVl#~~=JUfSy=4a-CRztiJN74Isa{>{;S zxdas6o0qR1TUCi%=obLAa4+^wwd@s`_<4(g_CA?o%UZZG=mE)qq~r|yP9lgKD;iO> zjok?uoB=XEMo$YeH@KYIkFGa$meIX`9drC-%^UsC@!PUjJ&eCy6cJ z*7g!~E*MdWaqq5jaJ#_|7x1IcZL*sNCkWOv%(#$_=IW>*2qXH|&zs@t;Ov6H$%b6V z3&R$8CDCagiY+DgR_J~yu#K+#7D>`7T zbDz6uqWXt@g69%+XX0jXTIu|w1cq+-k=JroVcK2Z{?lRfHnODl$o`(iqzHMAusOmim_nHI=g0uSmrTKwjDcdaxQB-M%)SRWG+ zc?!-Z4ghY$6{4@9Ny83ei3S^{w^#3KbWJ!cVecszq~a-tLaZlzH6`7;SB4|hK$)RZ zTBJq$SsyL$gdXQ$vWnseZ9>2UgOpoMJ=a9)izw1( zyurKmw~n@1ir^`~+598Z^^mCSgw_!&lcuj1y5x@Smo4SFbsReRVPh>P5%<9&Nk+;F zZYTb%{10KnE3!z~iK+JABuP}eTw*bn3&7!1&WC0`!;(~YV`D7K$HUz@2&EUSqp=)m z&)Ws>U!rclbt^BKCPm8kM(?MOo$GrS!VzN7a6fg@&+M=iL-SVoyh>??-i-ZMqSffv zf4b%rSV_#oj>yW%G46e`l7uwr$j3TtIm7a~7dT?{Q(W$Ia;dV86(D~sYcmNp@=Qfs z55l=hzFbt4p7nj7qp#-VS^L_vU>CYA^Ryb{6(LV!JK6v67xr&CmDw{wNB(lczD+}G zHrx0cfadbMtyyCckcVemL^uYF^_2Ux%9=v&Pe2K?>e=RD*J#V3yie6$4O1K?rBzpl zccB#WN4q#L9vY}aeLN<($dmyTKt_Ul_T049q#d@eUwj0hv^(yULLAa zAS+oOn|L*!5ypQFi?EqBpW2w*V{G{IZf%G~A#0J^SP55MCGKSZ9`T)_1nx;M2cG!%kzJv zKtB0=+?k3ln{;T9?r?(j+qBh(6KtQ`#;=N{j@ugr*U6@ZYFfYsNA?{~kWJ28Fp;Hx z!KG*;?w2I(N2RsKn?6AxO&SOoOm48rtRNAH9Y`Vg-tRQ700U!DPcDbcX@PUwpPRn6 z=zNMSLekp}T=~znIR;@tjn`O#SX}`F)|M|{T+`EeNN+v33LgScE&smXWzLG^TKt_r z(tp>Sr~eM<5j7Bz<9R_r0o1$_F6Y}&vCvA0D!c@*59NCCi1XA5tz6r6m|TLgPo$N0 zxGXvmcm@^)a=)=zNB*80U#+N)969m|h%1JbL<$j#HniEgP7{7dwBRBDELcHR^}&c^ z{|N%|L{J~ITMmE}0#1Fw{N#SuGy=;#)6C;C{U)>ZOV$g+`h@tw^edG;wiC1S>IDv2 z!(R_q%(-Crbv--CJ?OV;w{cd!L{(peBKpw6se+O3gnXayuaKBwRnV-kn_hu)s z(>?Z)d`dye>5BAy=^eqW5fzS2aqr-rX$h6YbC4AY~6gytmwuZl+DmFgcVLifW z2@nG`h)e=lxo!Q`x{_n2`z2q6^N&;I4pe`FKUyFW92+7*>8N+Rgx7~)^kIJ`YLYw977>~lep0zs_xWux^E?%tk9J_t;S zDi^E$sUk0weaRQAu)f6fxaH>iQ(9iJ`4UMz5k1)VUT!#T_Lb?}&$~J&?=V~VVb%^U z68i{!a5^6LtJm%948uaza<~w^6~hso(R+eV?@?CAwMthW?ab20wia1nsgN|t?}rw+ zr30_>9Z6OC$PzHeW-uROrrYxQDAv}Tn0Rd75TFSea9}u|mv^FD*sF3%jFYf4N2;ws z0}%UJeHiQj!%!)TxhXvmpZ&x-FQ`hHo~gpNp;8IE%p1Xc^V=aJ z)LQoDdz*BnD?qMn&l@1H1$(*nZw6&X0^zp(Z}uIYH}CsWK#_pm4N?HlFrejcA&XHD z&#)#zM58l?&kbFj;|X7jWS~S*@O~*wY7T$1dH1*5^ymGPXYoXRv)^eymK%0 zfQ{w*4<#EYI^=KfF9+$D(yw^BB@4a-{~j!UzbU^}8&51Pmwx24YaUi;gy4efYaT5w z;$jW>V)|S`Vy()9^H-7=$vc}d(A=)D{T!{G_I%x{y}M(NW1lDsE-3_u3AjiY5`+j? z&D2!ZiWv%XyWuUxRQbj)d&kV#Z?&EV^m_Rg=n!t>?}Dpd5GRPc;djH^_Rm}?hSlq0 z8MK(TvB1o5vJEs3Y})HZe_R!1QqIcuzugCv>#0?Xuq(AQ{P<8vKJ|6)m;rW=iFT&B zyF)jRV`1s85fI*}{B>ye{qM``J&Vl}ynHr$Erqa(pswo8Bg4mEPyY}Gs2sbv=A^+6 zj0upih}NIrEdPN~?Cr<5pCxW4@M_|#(NF9*j02va-8v5x$L-5Z z(u2uQ)*Upf0gIA7iIJ<;n$*yCYVZu*K-uQn`6y^BdO zrV_mT^qE)oWH?63@8j?qTdGIygTPUzU!k z{{r{p=LiXRsB%5QbFgGha@Lz0-)plk)2g&h_{;mE!LLC&{hM^Y1Dc+`BefgH&b(6m zm_z{FH?P@qCBbL3zsIS(-=V1AA%%Y9Hg+SrRUVSI(R-rin#PF9Ioq@@tJ>@s_WFEb z^x#N4Ws8cNe1Uny>Gq)VAw*4`$h}R*8!X>9xB9@Dp=l&DWdgv&C?Yi$gwR=ObHV$g zIX~0oba&=)Pd^!*GjUIk+vMm*pYixIpqSEhfIRi}8UXs3TYm60=WRxk4^)i{wu2wj z5|$nL8x$L6OOWf}{2Iq_0K*Y<^quxZZZ^yS_V3^Sp`}HSj{*WKjep4_Jw?%&&M26S z;$;7rMm!TbPr+r8s~&Y?rkeHSw}V3XSuy6s49At%v+oYxlYkAfqvOlZ$ByMZ-oHB& zZ;X1y-M!Fi54_^R3nJ?Y(9V-4tml4OXQtd9mKRFR*bYBY+=H$1HLw%4l(fmeynU5G zv(z_Df5k@@)^hJ|ev6cK9r)gvxzk5+f_}9JFl|q4laMqxW%F%Jjkbu{H-v()?^y|v zf=t+pQ|y!L5xV2Esw#Bz4iGU6ky1IF$By9%i&FH*oy*(DoG*TPy@)!8Ujz8iuDj|h zN{%YirSJK(o%tL6GBK3}N&gstWmi|sN>QW~)jX2n77|Ko(iaMxSi$5PaM2YIO3X!` zb{Ha9wB)F-*DI@Q8x70Oi@tXf1l%BlyU?C-!o#T#S?VDN;w&MXQnS1=!gq zXX%o$`YKxxFK$6V&MYRHM$Co_7N*EKy_uIJ&9ZoSXgg~ab6!h|cWkO|EYOS_++x|N z%46r+l*el)&U>Fz$M{>)`OeRB7?6#$<@HcPlvKFbPO?IIH`O-BD64eKqU6+$!Zv@H zh+xq2N56xK?t_0pHvL`WSZ0819+fp2NCHFj@vNh86w*zQ81NQX>g$-ZdT&4Cmk34^ zyy@zO&_5Exm45{|B|zi+Zvak9Sc}fnCaL>ZUXK(95b(@lEHyPjAq}|MM*oss+apiu z&buDPLSnrq`WH*`S>*YYmkwdQcsg;7NhBgKu2+n4J<&@rw@f^+40xXWp|e&#hc%*d zMJ_a(815{hEO&UK>%V?mwTW?UvEOAZ$(nDx{|5}q`~7}=ik}13TP>@8YLjgGK5eX-VV!AIF#+48(hds13v&z z$lgt>Fk$KWNxqt}f(+-_%oJYMTEY_tF(R~_x(Iv`0{$dUz0QtvWc!-3uW>W@UkDox z7%p6Ay@Q1N%RNbnn2zB>5dwChHCytR2b>HijL^Vc1PKP-DOYPL6-|$hM7)FnADW1u zTJ^>Yu>FvINgGk~{MwFhzZw>8WAYLII{U!8LgJQJ!iTjQkFlRtVlfhB*}tD~uK{5b zJ}!)znMo{4vSNxMA)UgE{_QYgBxsXBx6K(3=9Xomxo!5xtXKJ%CLQHJiWtv>|iLh#4TK@A2s87UE&jf~MEdB}F7AO-pVeh1ce4Wsr8E-M z%ty>CGg>bd!VN^7l5BCF+)H;>dY)=ou`j&=VV3F62mHCh?Vl0e0)VTGqfY74RYL)T ziIxxnKH3iMe%Tx~W>+vfd4XsRzOG{wWj*twSv&2xvXr$o*F2||$cuLR*3;VBs?88t z!Frxru}^QW@$erf%a*gmjT|laYY)CF784;yE7DFp>fQ=9{K6b&a$}M~`tk1Vq_Bi~ zy(#rgrR!hpviL!FXs1GcCn%~clc9iPc-7Fc&0iC=J_SZ^XpMoaOi*f*QDr^F+c=*3}-Pl>e!tEl? z8_enOE|@r1!*Q;bbv4;+LQU$<#Zo#}oAlLb-(l2!Q?g=fAz{FK#nL2PN3b>R+P=oP z{{T0GnfT6rJ+r^^FpKB@BOa#3&0jpsXWqk<)8s?Ek8I^=bBSEuBRM9r{AHKTJ^hZV z*7uv*JlmrOkL%~G>o*S9PKC)|q+)M;}0_R7Y`dO8*ltIQ`mBuU%B)%kM4-q#ve z6?X^26;L<#y+g6Sc(=~(z}GnMz;{`oZc0oSNj%QlLlg^Zh+^RYki;nZwbxL^p3QWi z@rbBjB}$sc;|1d4PW9bRZ0RgMN#2l&Lo~Ih zeDqTwrvnD0dr4<;v^S|=-5-q&nV=>jiiMir8laNVCmW7wqxuae7%c#}Q*Zv1w4t-s z&;Gy*)Eb2S5)?O>9f0V1L|6hfx_L9z)^%iGG(j|R?K=KwGYR0Ly>((d7#_;wNAAtm z`hIm#V1z?E;76;23i{DkOrvcjFY4&x^Maq$ac(2o!;El|{*BlfE3gLow|~RxEZV|6n#S=P3G~isb2_zxqsvaI zbY5+Dc+jzKMS^?G(t=Z|xwJN^_76HvuM=b=mGG<}u*DY25wo0K=9B9|lWt&Oin!$7 z21HVIDo9a@RO)Xvtwaq^=Ybsd>6Ix5(rcC&*ToQr!IK5#%PkTUf2{9d5eJk)^%8Bg zXe-zW1dVhR=0Znvb?VWBAmh0m{KL?3mi`O~3yh46@Kc!G!@xMGNN)2Xksa$SnqI5| zyJjQkh)V&4bw|6b1J*>kdS?0??PahXuZ&%M=5HDyk41pgm{%@FT(xh$%lJ2d z#f0l6j)#Np(e+?&g2(BIytBpk1+1lUSQ98eq`a6lB3wA>jXLI*AZ6lxOjJ1*+yEp> za3yW;5)vV#Dc?39He&S~eB(hw*J~_)+RU9@BlR^1p4ah0Fi90pYN9}^OVRADx)@tA zhm==6$NLd?S6Ls9)+qOWYcYEOEyov^wkgJfnSN_SSc@mW+ZPY(>@TpzK5AqX-!l}PC{&UH)7 zWlz)HyeV+kr zY8Me}Zo2bTXV_`u!O0~L<~wf3v&4#-@p{Od5NdToENLA@DC)%yhO75y5xaX~@}SM7 zFYoyGF+h^BiMq;>E{&wQB9Z&aW!ZYgDfY&dA^rI^8~d70sW3Xocm!g7jZa}N`NCTr ztq@_7nslpj`>I6P(R7SAa()g)Qq=XMml&RIA*{_35~L4vKiNy~oBXW~>MrED)bR(Q zp#TTFTqz4NIyW5zZ}M@4w9W>&?GAIe{_PYiUvJ9=qkBg#x;kG3#VoKrY3+(7Q{Ke} z7?*_4#c-@gr}>bY8`Ak&c6-Lq&1wxb>_m_q)Yq~#y)vtGaD6{4l;KfJvxnQx zwQQOkw&zd2$AuBq52C~R>xqupeZ=_kPCh%4(2sHS5BJt~Un`tus=X^tvzIUt&5sekuWvG))Hi(d+l$NC;J|u? zax*P0&4)%V1)MyCXLqf*Zd?}S9OP|YMo$i2A~82TlAL}2+mIj>VDCKIL2EO&+REzp zBsceTrX@d4GGb6;9(2~rc9U8BUkIIz!WAY7Gr*&u4^9rN-1>J$XJAe3(Q3sKmf~@g zq{*Fk_eJXGuy)$I%9oYo^@8RF)+iA!J9c26k=a>hk;B@3adIr({RzE_h>*~M?uU9K zB9-b?zWbaGPd8OtEa;rBj^6AA29Z2wchOa&gT5La2tgQqBj2_j;9;^QLHR!i*54}4 zs}%1Z=29h9v1_+eb;z-dRVzvRE)aT`r?Mh0!j&w%ErI)q-Wu#k<1QCrjuVxRajw7P zZ=rh^ejFQX@~XxROL`_VZa{&4So-|da@qxispbttMLRccWrW%1{8x>u(W`%AT!G5} zi?qv-^%!&!#-1Q^W81?3^FwQET8oc8?AKoRJ3tDOH{R-kLGE@2hFUAsG9g>>t~=2Z z0~CLdovodFXAOReZ#*M4X+ZJXsXl%DhYm5vbcb@Gd~V^h1_PRyn4)ugn4B!1dv6VB zEihjJ%mq{v0Hu_acEkYXtv<`h2lKO~ZGxx1IlR8;Vy+CW0hDo>n!&G4dMXuO3O|k8 zSJ(R;NLo**nF6ETgV8^x!#rI>g|NLIVSb?JgU8uleDeKAJyFU!W)gY;!^@Yoa#vLX zx+9UmMnh%(SWj9)yjOOKLB=*ft{FRU-SdyHk1ob!o3 zFYdp0%G*A2(yN=dH>9(od&Eq03FRH8sguX+%B8gvLRPCc02Yp9!oCWap#%X~yY9p~ zs4}-aeSV>VlfWDNUe*jIqFh0q`!TCe;6@XJhlfv#PeMHZ>Ddwy$AOSqq^I=EhGFTD z4U`g>RXtnu%IwT4d8WJ?_;vY(CcTS~mtZ?uDs8mVaL3&7-1D<3$;m;3-(Y^$Z?0FC zo4~+taq7waYDDiII&_F2V)e7#Td_KVP%8pi$Hlfq$^kl6?DhAOm87-BkRS5N+hnA^c52A&+iT8&<)@jIxD+1- zB=Tz@suX;qf(UhiR+-UtZmMksdF_65;^efd2Nqkv7JuI) z7*7yW0E3V)jySd;-Q(uwUKGa=vyHrA7)@oOuYR>M?$?tYIt-6Lg^Jrha;zO|2zn)a z$hX&e@5LG4(J6POwc2){*M+z>=(Y#)Q5=U=r*7p}dOUl^P2IX?ISefCTozpy#}I9N z-_Zc|D(Zb)Y3}JIp{?0tQ5dBLSj7<$Mu)bC)IJ0^ z6OZoQyLXQ`Iy%9Da2%;GN9`vmf zeY};QmuQQQemHg{=VA&_RxoJ6+?3YTq{WtRe&1vFj&(~Yt&hO4CAjOd9t3fsRE%;f zj{S9}{gPmEru&KA)Z;x@yu6}2z)3ZIaIYYMnmrN$_;6q#%*9lewoWRp9(1X~!NI|X z{Ks(k#NV6K+jX7qV8=gE#WhJl6*=usXITWcF-f@TW^&%g_FOx1*C}5=nwu?QR3lQ;&-3Y@Zf|;++d17e_OS6d_4m(DdK5lsM zF9(JVa5{O+IoOtsMUWGbF==|aYQZ6h5KwX4?$gj)3?ti)Im|L}3=$mJ@H1{gk2W$l zcl0!#7pce8z@j;8E$Z6gAumeFK_-Z>(2gYchi>+AM^pt}iR1Ld5FOWILc0l*4xtLg zDRNg7UxYP?0Op0nK#LL|en?zAZRM34JWnK-vlJcPtmG9jY@j(C0c+O9aM}d56BtCl z`7xga1S4V%mBHRuq@os^id{1krUlre2_FgwciI=jNh8mzCYBHrCSfKjCNU5}9$ZVvl zf?8pbV;bR=7!+!ksP4I*Ng68-7l23**0a3Lw`x*xvzH?@YOsvB;}jdmzg zO9hT@=oqEKZ74Iv#OSczJckGm`X66zep`sL2J}fcl$xF{V{JX?)4BI_erAR)Qq#)p zO8EHr9@i6S%xdJbi7q$#=>N`d3LAOZ}Qh z+R9fs9kk163B2Vc^1Lj!vQeCd1m^nwIazB=xw1kf zC%6xsXqd#mk4f?c*p~bi?reWkr(1EYg*u%sn>R&~2s4H#TKARBB6R&k`gI3Q8q&4@ zTV?vkd{qvFH2qzZ&U?Xsu~92VxyVD2KdEVy9te4Gi~f1(^5wR-e<|sVBjlEdEi_dx z#`MYzjn+T05Q_>K!b$KOpOeU4yTiC&zRY%~e+dz4Mj)wKSsI7ehv{|dpdK0@2T{4M zTV8AL)o%$Ek`2`{x3VULGMOE1;{Pu1Ejr<598+PBbEdmt8Y0#N{yjEs40Q;p`*1 zg}S{@!fHmAp^U^mTwLniv~O}pxe1IuiNc{_Hhr33kb{rBJ|9&*rcc2Ad{#X+nS97{ zXZPZ3S6x}ud!)Hqj@He7fvx{oBYd4uluZozyRV5GR2)FLvh^JsRhjW#eDu3J-?+1lWDb~e>q9i*wpRImTZm$p=T!9UY(zZ zGPdT>p76_EI{IBf4ZR+Oc0HsdTp{NZ@o6xy%Q(rJ=3}xz?A%d}%`Mw85D)XD5O|%L zqR)PO`n3JO1Xp)={wLt7wrrd0h_6H7lloNcN0aUlybezh*3tEi2N?YgZEaN!Td2?v z+_zjjDp`LTC(J&;u*)gu95h12Lwd$ujLHbmM}b=eqD z`Mcbmc}8~9BE*+e#*%Iczvs*-b>e`_Z6Hp#@ox@-Gkh@?RWSnk4?@IVjXLj^JNEcN zqlW*-r}1cC!7zN@+Q`qgaH+_U|M{W5b(TAp7hst4>6h33T_eehodxlwS3?m;$x&F1 z52b0Hp|`aA9w$05@K~0SjkJ2MVU&LFk{eITYHe}56SQ|LR)`?pde8PX-(b)>>@oZH z?M*B3PZwP`WpwlMQVdK)X*a>;vjYH0c-xsg(;t3ku5EeVidOyrSb7@+x*W*yT8=Zm z6}v1H?$vJgW$$ujyD0MIX=1)XO@K2o3i%r=v`N)~pX@5!luHJLNgNpf9*DEk+ucl3 zRRr7fzm>(8QTaz%8NUAFgZ9O`PKNPSR6N!t7jvfP4X+LupbPTliEi4(`XS_!2D(}`+*t-dp9#|_I5ZTMl zHRtI*v7*i)M8bV`B<9*;MO#qU&XfD4l(L9cVyQjiIo{LT3f zRI64UAN27BV-807?;cUKjJa4yugGG)Db*|&X9VQ-N{|v%AE|(5I(}8>>OZjF6Hotv z^_Il^qLR|bvs5lnNu3rC09A{Hx4=SNQuW>K_3O%VdyU(zJ#5{TBxYWGe*JKq%%Gye z>hQL9VAz1g?6wZt@%pbu&V919B9Wp_o91Qb{cJ}zSHmHlAWXh63!ZNE6Pa;{QvgFb z`Hwu9z{7|r*|JaV_OO{u&K4cWymNALh}0L5enCiLj$(q=4;_e50dqk7pp2flh5}ZqJjDtYUtj04R*tgX;X$%q*l>f$UnJAN(?^IMe<9 z^0c_taCD0zbC@xKic;z*6Hil17*L~1PQHRz(vz9KQr6o8m{-DOWf7kveSF$9M8V-K zA)()ASfjlHAFcvD-ZniPZk|OFT2E94l&H7A8yGr@R))}e1y0x2HZ}@fFgz|bbpAMR ziHS*$mYBHd{w)oUc1S*s+@Td~kxIUwQj)4$TZ}ka6e+SwqX#>cKmeyWTOThdGfATu ztk^RYS&9j6r{M%-F6la&Yf4OR?i^C9KVZ~uDxUYV>B8_7I>9<&kps2I2fywhXTu%1 zLNNWS?lTqZKr6*yH}-Tr3C(%+ioR(sQ6_rT{=wq;z6tbAuSBR=viQuug2<6 zch1O7S(ut0ftsH5rI_~2TLPD~Aup}?k5v6yJ^y>EepkBxOw}LxAF2BDlP}6q04=`) z#C=LzJ4smFs9>({%uJ|mz5bOkb)nRSNw1OHfpTTccEG&Fs}69fcA4xB4DeUVjR9<{ zVwHMlL0`}5zP#&!E(ZBbGv8$Z>K=ii52hO+89fD08ChnF5oHK0XSnzxKX37gBuw7T zf0ZuA>?S0_4}rQJR9xaFKK^DtHgR^?b?>u#oY`+TQ-sHU6stNAJF=2ZQB z9ZnI9urJgplC5&G%o- z15RyZ1&o{-`gs3h&1}LWKts$o&Hh4<*Cmoorx(MBV5!Cu1^1B2ijKtBXJ!SWEG+{5 z1DN@?=+I+ZILiB9 zHPpU$!2|QE*2*0@0!+Y-{{*KwPs3O1%2!(W`?%17P~{ZEJM&=*gsdU_lhS1ckd1(e zY5j=pT8RFEU=~=NbyFw+6X=p z_W*5_b8l!xg10SWB9Zi$yekvEzb#|a;lf(oP^Ap)-0VXY(hKwu*=mnRJBJU`mY5t* z8|p*O{rB&W*-}Sm(B406;@1R2>~?%i*QZaPZl675YNqvofKX7})y?=3Q4`WSTkq{0 z)0hWi->*G6bUAYENhzuGw<*ylUoBQVqqaiMZM7}~&nUN4PS^`Up6EEu=kFFB^yn9h z>I#k+kOi$$*}*w~tKqZluon_ETtd@=a`qXhSx!BRK7Dopxow;Hh!`AjFxy*uimcGA zb>w_l`_Bs7&`Yw+=s9W*0$+!a_z)l8UV(sDEmpuzq;n4#J+!ddRGTmA{cs$#%SxvI zie5VJDBuFK5S*v$&bU$IIh3Mw*I>kn4*eRTI;K;4yKnbyo;Nm+HUlpI({#YJfM>zD zfZ*QUHr@8W2bZg*{(F-G@85}?hbwtu^a2T}CWqmp&7G3wg5=d6k{SHW6psacWU>(e z^x`-6k2W)q9-kW4>q77VXb}ZtPpsiNL^^^~;dlA}%{yR;O6uuFIdGF82Q#DPko}uu z!PheHdtlMxhoh`<;!AMgav|`?Ex^)r~5xWsn4FQ1<*Vnl9(CN=_wV6a?CF z?c75>h4X~~=#Fk6>=+F|B6y7aLg+v`FJbmd{Q1n>*cqhle8t#N&7-mo(cB<4dJw7# z$OY$;^z`_;6hs3DzK-H3T0-&8)zo)d2DTItI7xJ&vHpd6P`W_ZlBVYaDjcD(AdG_$ z7J8#?_UFr{6FdZwC^dD>{81MPf3V!rJhjKRGeC2ekRT#C^}>v$xaKj2xI2N%e&FPM zEZG_J76y5Rvkh~riEyZ{}w++S#TPyH+k{2tfps6mfUZ4hDHA>wu5XtY?I+Oe7X+44m+x_;t!UB=Y+;x9o6e0PRz zaRCk9BsSEiC7)Y{id4rBFuLZ_gTOam)b;!kcr$}S5dBNMwS4I1d z&r9fWgEpWx#XzU%%a;cZBgc=jRihIMl$9!wO@k{2>lU(AG!gVFEAWjYjxSQFQ@HKl zq3s{2<1>TqOtLYj*Xq)r- z0fS51GwmS1iW4IP0~Ci+V8H@?46wR9JT=qA%=S+@ZLR$^STz7>S0B8CN&5NJGZvj7 zrzIx?d!x9DF>V!M{>F&8+z;Dy0H76 zs)c>*aWfm6L%q7}pyMwk=Zijmg(vx>oAr6wHwOF076&_vnz0-Qi5Bvi_k;dS5@|Y# zGRcOGT^|E)i2_PC(;S+*ZJTiL5uWh}<~y-(>b6V4Cao*B0#65W$=^`y4NOg0yB{8J z-0xVmzd5df@LbKgl;Zlw?gXY#sQznGm5hP^{YIUI3Qj&G*sp1$67N)oH&5$B?fqx# z#IMOaPpW}fh38^lV*diQHQvX|C7vb6lF;U_hc>?^idpL(EB}sl?GVS_s_s69E#muk z*8MCjH7zY`k8Z#a@JDUTEZ+rj`glc}{OM~3Z^r&)@$y@SJiik5R=z+XSj$s#7offr zvIO=7i1KnS5%MI$^llEc1YBM`@n|Q(}59<#b!((z5-VD+SH7mREX#Z*W zxqgAlS}8nN0<^u;dkaE3@W>Oeb8~Ks6_}yslAR^9=!Mn$ocTy=Mh`?|Ps zCBkgyQ?1t>ja_BB=&fB7TPgOtv8!TJitiNh&ETG5y_96Kn(P<=iQ2z9d>Sh>?_gz^vo+H8p^fT|HCr{R5Qb>Bxqy3=56QUXHtn`E@ zXL@=#%li6OV8z04!1xp49K7`l;UqYy0R!p*y-{X+#G-s!l{J4xm;Tg#zpE{N*uy?M zjNAqjw+NHyKnYa_nWG!jilVW*v$3-)Lj;POfHhlRU!N*QQxSH-NEp-x)x?uu(Mz#n zoP%UwmT)fuR{rts#@YB8S^qoAe<&? z)_kOX^?cUZ;y4zw!QS3ltHoQsPqBuNsH9O%LK|IQ+7@W=0Gq8=Fz#tp^of26Kxs7x zm+wh(mNZy^^k-kK@!qG)Z(J6sUdEKTNqGxKItEkgP3}%}r{tZgY1VIypCg{3>wJbFu5HikoQ!*@%=I$b_t z?MlZ3k@}M~cXl0@meACuh3jZWy`E)jl@y&nM&ttn13uo;NiTyO+{tdE^kL8AEjh<_ zZY#?St9c%%K$x?ZT9lk+={)p8cRaqru2V=dWkZ{cbN7x%-SN63)oi>}t1;In=Z)QY zxt;ZSbjFQ?`#(#<-Kjw^&u?2M;?OYQ!TZ8~MmS(&;(_%_=szlHOuQ0R zJC@Bsrez~Ru6qNxG$v4|JvWNyR;4K6c)h)zp`837 zW-frt2j-0iLuRE;|kOcC)ig-5kxAf?{&DcpW?E>r8x)*JuOo$ti(jzk>!{l?2WZL>SZ zG5$hFO~+;SxM*JIeyz1t9|2`Bs=$T9#0dlSFR*L|6(Xym`;prBna1Mxn+HRv4|d8g zCSeN=Zli5prd80%-X<|SVR)ol?#y3+RKm9kCPC3_9bgE`<3F-5d@{`_PWN=^ypn!c z?w)t)YOs#rv9N@qZ!Iy^SpDMuceZzfdn$sV7ah5@t6G6K8}su;$J`k)t&q{CMX^W@1Y-Wo31i^jyzQtmaW!_m9+I0Nt78O|y?ub3Sy(N5~qD!Ge6@?cTw=r6*4_kbdBZ#dx9keSj}9_rKUuCZm0N z-#kTvaPM+zMA)cWvnidI3BSfqO`i}WqEP;a3#Wr^w*gKXsIFPYn@)9kwN!LW{Z;Ay zDSck)9vl}%TVk<(!=6qZ?Ku!cN8EN|oz1>$XU7&eMS-aAsJ$3%}6@_G|{ATM)iS<(=2DjY6sHg&+XPy{TOg z?bI)?Ed3kTGTVF(GVZN}O{eu4ZEY`bCJ7h{w91vJ{fNv47{>@zKrqeQWkdN*f!>^> z%PBhf{Skx_{{RKm`jrg$qq7I9*+4Y~i*yM|ABt`t`wxcv5SG3s%6u4u`iv6UEb9JW zX5l!=>Dde?ctBb<)M2AMSn!js61jn-A8%;5&bPc{>%4dcFHRN(XVncB>uclMsZcj+ zZPu{_FbNI$@+}wZ3=g-(+Y_Ghh_4r){Jw|Kvx8NIihT!uF^NzOhYIAs>KcWU*SAar5;AtI)UXuH)^g9c}YQU6mnO8oeS1LoRUeetq(;*4!!ZZe^jaE0DH7Jv~7`wWyz@oRCOT zWXr|IG|_ME?hTcT>ZMc+VbN42U*0x6B|8{tIGPu70gGUN+mq<@(2BllzY!Gxh~-Vt z(%(Xo*EtxG+BOkxpLCbxdf^5x3H~hTiqTG8bPKCvp6R*fM4IaDl7`I(I#Vj3)q%IL zRIXNW)vk^5WlrLwpxn*-V=4}EH_1Eud-%mMQAJ@je*NsJ!pcSqQIv7&Zkzq@MRWM^ zdVn|p>mzN?rthJ&`6Ye|7k^X^o7%F6m<3j%ss%MxNL|$TrfSf)bxX|d9Her3cuPM5 z6ko$((p2&s=2ws#9Lw}2+EoMxCF}2U_7@R2op$cQ?9uL_4)PGC_*_=Wz3MCpOYI1b z^~P)s^2R-z4!+&I{M(&_@Y>xsXIrx%+%G~ZV^tFY4BSUw9@2a*M`L9&ClqQQhYCsdsd-39d)k6rPIy3%+0m-pj5`H1@W`! zdHUtf5I=9M-1l!mDUbN3Cjf+2BQ2+|F=GXca)y`XH}{7zDJiL$LTQsUuE9Ne3u1?| zcQ#6ibzZufE`0GIa!CbcneC*U38>xJN5CViRtKjL3}G(UfnCjUjhjDz9V*Le|sVSJeR zufYCazlZo)7J!~Vf6(an|NjsFr&p|DV!>0|1CU#8lP*yy>&mO9yU5 zkHb&qg|{Vg8c8GoOz@lfn3h$iyMu}2AXYxw9gx%(!Jh_xqT7gECgheZAtAAI-r2S0 zx{>qAM8eP*cI|a3+v%5@y5&v=SJ)n&@th842b~pMqYxWsRqo@^+52TnPx&ob|C zu31BDefV{F*oZc8etL)H_x`Sx{cauBf5@wuS6-!f%qX6(lx1}2?2z>r!SE*%9;JvqR`<{F;fv&o2aB6Dm5|OLlFrcwa*bM(w_4sa=!F*3(ItHMVU}e%U;( z0`cR{hwQpNyp-KymE8XmzN01}F&qV_rWl9*YKt5#p_opcs#HJ?MWA%4QGb5(=ECZ6 zc$e4^ai9U^476yAwse~qwIc9~mLM}45tDsaALDX5tP*8TAPU>If-EF`-(O-@D_7Wh z>Cq$5Kf=#nJ(vR{M=*p5|3X2oGI$0Onc8*y3z^F~!EWhV&m_kR5pmnxFO8E1vIg`l zDLN8ua9>~09CPmx>+R_wVscvyN2g{8bp-kjtT$;8n+a}L?&B}1j4^LvQh|eiD~!72 z$CGSvUjrM!r>hf=8`p+Ro-`3NH{x!t*{#@=&R+m{2BpV$C%%1$#zk1Urc5RxBV~nuV>QeMfHy56JJu`3i2FijjEA_KmWCX4c^}2k zegUQLj3(hq&;3|G$L!gUG`g0EZA%vs#GyIIY+V50QpmA|CnTJr5DD%CcP%OF02f!a zb9kGD=o8p4lnukZ)t5GZ>VLpH5!+xXAT#(#A%>=dVoaF_RBi`kShm4U%= zWOZr?FMCM-GXS{&D!9eP*Fnz(GXkNs4@nFfWtkt3DJN%u#ts!ix_N{eQ>NLy3qpio zk!R1p15}iG4<}sCZ}k!BA?8T~$A2ouJVH1RoPThqw7$1+`18s0!Jm^wEI^H#jAWfSpJatZXZ=UPKC&~&L`Km3lO zXDZ*RI>?)u$}dd0lEwd||FMkoRB0He%A;W0GvsdBW{M>*EtRDvLAxNL@eC&=$Tx5# zqS1DLXOgmtKI@3g>DdQnKQy{x>TI9vm_1O;K!V}EFRw|}7u-bKD<9pg(&kss`!%q) zmM4-6pm9=5M^mDk8(~%^GD-v@KUs3MHc_d3Fk|gzC-^L8dXq@#z8FbFYtf|6GcR0T zk8=dr_xtjB@WZO$R6?SD^-UP&Vpl$r0rW-tH}s|MX{bR!`H)=PE<5=Jid-KM-K$j_ zM2>R57y$4-Y@3TuJMU5$%)rw3SO&`bJK66GsjvO)kNS1I5L zgu8z3%J&a-VL0W!ob=gsr1D74GYWdw5|fxKw{iz^XhLI}xxw>@a}4fv93V5VHTJH;%Rl`L5;&Z_nr<7?o2|B93K) zj=;_f8*-Vmyq^y--*>IwBT{*MwsJ*woVOnFLHj^XIRdAZX>wWp>tNqHTaHRO*24~n z-~s?OJ2hxhp9QR|mzR?-aC`X4hwcpAY@DieZk3^!fa$n#ja{9@AZv&qiupQ{+t8Ib5iG}8 z$#R89o0YjmTz(!5D0ooCzoz6Q=v%*QZa=OxxXHY7__)XLz$B5%vfxzkms}RQk+BMy zvQgJbN@C|N$$ZVMUA!1)f=>!va}dMBIGRt;%n0=loi7>ECN`->cXm0HJ%miP=3ct< z`Ip&cg$5>v^}3p=iak!}LvU3+PQ#XMT8uzY%F6Qr$hA{2O$aF>ijuAL3{j>vUU4kS z%N{t7X493Ob0({LK3BF%*^#ri5n7Mw*7qIRMDS@T?bbqSrFBPVie~z9vfXV{m&EOs z=Xvd19mu`e@rU{O_m6A9aRxaH5M^maH>|uP<7Z;HH(I0dimNoz>dI*|34xAhJ%({D zLYG%_oEdv>Wf$r$Pdymp?9*3mp7ZIVIQvqw#~IE2^&~@Tz9#?0u*YgucQucWS)8BwSO;4<3&eLXu@_jahKRRrH&&|!w z0A?dyMaOQ`Ut`!c(Z@U%SCin6ybBW(6KT=-Ro6nPES6jzHQt8MmG5xxOhI-E<_*IR z?$hl|>R&snSCy*wB|IL*>(?YCax5z@00>PMn3Q8$twl zF#_pa&W#QG6l=)Xx%C?5nYN~Fgz0Ydy|P<|BqzG-vUsx3-y*_1xcRbAiZh;3;tO^S z&xX440vBWdql8dGy}fp;e3Q&|)Chn^>sn0Wf1%Xpj+W8TiJ{*7e)!)n3eTcDp}(% zInKs-lnW3RZ*ii*Z$hHe_~b$a|z}xtCRMJgkh_d zdQ?Lf^QjIgVQ@lF|1{4u3FBVjH?NYB7TlrH;n#M|1qBrH$* zGHvc1;f!WLUHA>Vb+icEus^@(jNG7> z{Pk6H@IPvli#B_O|KHD~$c9-F#8$;fE+axk7b@<4V<{(rLx%`35c_?r%iyj}UT2~3 z&qW!`v6yHTUk@M8ChBNF2*mu|2#y>`B`GHSOissoD9%5sUvI1ze@z5J9OgLnKl3|Y z{D`o!H&{a2Dqm2J@S5Mk9f*(!LG0W5PMuppZn|*eR3PXfC^Tz1mynEzM#vd(Y19ENnW6!GNJB zYCvs<1AB=!p$ zhY?@0ksUQ(=**TJ#;wloWwypIYUJdc>~8yhg$oBwjJ}L_722B)CUNZdpg1gavN$t` z9daE9HEurlMAQf%qrnZdDIgJ(Z1;H2KdA2z^^2At*&`YOOy7z-Fk=mBL*C%|{wHT& zQdRUpJFz=Vghnu|_xX6OIEa&`Za(ed$~sZ|?*R^(Wt){mcL@j{D4fG(hr=C-1X{rN zzCJaN`{jd`)YuPWkAG1k3WIUl&%0$V0u00fG}3cl#4q)z{XwsNrw*+tDh0y*4KJFXo}4h<_|Fb{+m$G-@0e|gT0lQteXore z2W^1mp0Bien>+p{LfT$r-Sk3kt6$sw^PV<>YvNq0pu-~|UY!Sc^1R9L^{?N>vcQ8_ z`ilqQj_`=OoMMb~{f}+Cy)E!}8br^=dzLF(=Z_N?5wjteVB7eA@*p^Log!+@5?BH$ z*@(k@Z9}(B;S~@`pbYwfo4S-M{f7b}@@xpMZuzJEo%RFPRHL-IoJd zyJs=7G5%>n%WJ1-+AR_k-SZKmf5N2H?9WI9L;X#=W~83tyiOkeS`opUvw2W{^HT_g zh*}jRNIX{{)!?Ds3&TpxU=ZHqHdz~ETv?*a+8)qvP_tU3jHXL2JF84l^xT7k3QkGm zd_0zo8f20Ex#VK`*@$>Ce%`VAf6s_eHra<`O}6`(lT)_21bJ^uG5M7XvY((a>ZvL^ z?-9}ppatz)Q(aEqY%A13N*QC-t3`SOBL+it$4xTDWsOB-=nmIr6FE5hfi=n_P@a&f zqB%riz#$Q7ho%P${T2q1bx|hpMTQd_Ou{G*%HzLx&+pb-k%$?+L!gmIYs^kUwg8L! znwp35&F}&v;s6gT-I})=Jq~e@v^W7gy?iOqn@3MzC+^cCQ77R-5{s6*wRIt6V6*wT z`&<~L;^|3A?TXn6NgtS2?*{|Z-2Xvs_?~*vK3kV=Dk(Xcpf}vRe;=n?s8M+soV}Kc z$d0_B>wgw9ICz4+yPB#pXG_(8w!2~SY&d*ucE}FK;3IqHjoPy{t`rPo7#y;l@%`A% z9ob~CD%>}M$3*?_jE2h@fdda1`ki83n-mcD3qM^CwBML3L2mb&VlTO+*hfytpU|=0 z2BmI6$96crDIqUUw711MW2oYiyL3c;ZHl9S29@rY`Id_@Ys}5WJj3+A@fkYnS~Eij zy7vFZXW;pr&ronW#$Tj>+Czym%EIPmYZ)4PxvJj#wc;SjXpq^`dW9GcXY*MMK zJ$iV$0+0rHY9LIZna8TcfmHeZ>#lgukDELGY1 zmsOH(YiLMpS<3oh+?caliG8ekUG~R4kJ{MfjsLNbjDAj|OdC0)?<{;GM(Hr?_AA_x zvlpJ-yszNccI9Qy$;Zw8@7m^O+GvaQERq@^%w}7H%q&k&Zhrn-(A`W=lt_fVtZjW5 z;$|cBNEs)JP~@;t_xb_J!D`O&Hg^d~%|!W*Ifd)z6s1;pE|_OgCm`PFU=he}u>!kB zC=ZjGc>z|R3_bs|?JSegN(+mK-L_OBcYD%H+4ZU_2~KCuFyWW&kO8{`q{8>j7p_3| zLhM@pgJ_?JJqn(i$a7DiV|d}Mh({v0EruZLo=gg8yRT6dGxBh@b28uHc-j~;=H4gg z@d-6S}8b13xl6hYFW-{k2n8-lP5sButo&f_Mg% zZjV8Ql}A$>G--f!r*Y;E=ODODr+b8Yh`&7Q6R0L;w58|bMM}zE)a7mvxia};TKo)c zBTcC*TZ2Yr_+_0yh1tep+59?yi@$gZp6e{;rDEiHA(@5Q=L6CZUk_Z_ z;E9UhNr8qoc8wl{6$Glo2W#;mAt5iA=MmOpj-d20WZvHzQr;QV=I`SJ--GjcaUwbM z8KX;4tM9tA``>tGH92&vsr*Jeh+Xt2?ZD#%_q~6;4QI5N#9g;z_gb-T#}UNZg3ad{ zz_FLCtEjOMAj%{=esHp}+AX0)N|W2o+93L4VfKXnfv`zJkh8r$xmR#I_`T^h1}{s-yVmu92`rsxB0dJ} zuJnZ;JZ>J8>tE%bcQ3f=uM6f;pmfZ)svx#mCUHNwTOgo4$Fo)S3cPgJlFsjBgxea8 z(vUuN^VUWEwfd{&oCFBsO_-H`r zPwDHY>8v2ZeJ7~xbhg%Mi!Q_LEbuIA*N6;Gl>WnC-$hxGWEBoQ5XkT?n>YQp$CeGWQ`(U14^WtlRnN1(bx7f!C-i z75QzSfu;X)Ci!K^)Df)o4J4oCWTg-%*bZkNwR`@zAkMU=In6UyJP1$vX3YifroUP; ztw9jQx(w&B9v`8Tm}?LwRfK2Ja27yQIEb(UXg{T)v7B^ysF+rLDtMmOmQmJedk#}u z(p8Xu#rEtRy}Oo3$zGu4^6U{KOqc2&Jt5S1mFcqYuym@T)Dj(JO_lK(Vq&*jJSIORckm+c&T&m_!JRUiX*j@f=b{w z%X!&diWu)Vw~mQJ?@hn9i~8{9{Q!}(Sqq^YWKzk`y#?Ai$wSj z5S-~(kdUbx)!~pzO;rr#^q6k_C?Y^F=B|_E?f{ScRJU|i0tOvB8ic^l=kU#Xd9E&5 zop9Wb%z`$kH+5|I?{os42lEP^hny@%67~&g7mT0! zc9=hiin0qu1Uw0yI34JvtdFuWf zy;$!|07vVjQI6~WAO!d2^IbZru~b;2SMr!p{31#g9=Iqc&Av znJ1%Ax95fmP@MYGZy6qetmPDoX7pQcg2KOEk}Aey2nJ_T7(gmGDZ=V|B?z;nlgGGd z|FmPWRmI!R$yb_}yqu~()a11BPtX8v;)aS?-3P^S2wzHU-K~K#|QzGGUZAcfzyK=ID+n0jN9w14oOuyd_<1R(r{6rwqG-E`PMM=mjnpG zp}d9gVFfr+N;?YUd^b@s<%b6tBI8<57k^+tXy)qb|B6C@%7PI?oZk8;MT99xZ*BGM z;Xi&iMt>`czkc2r{ZCNi|2>2QWuFuc&7VG0s44nkW6t(Yp9Q(yTIAR^h5giJ0n_f+ zD03`cz19^w2Jc66AQSB46w)xy%$n)sdNskaY0tWlS37M|6*+ibNCba178V!NjFv4H zIsK|5F)6&Y;Evq?h*}^wocIbnO6X8kPvxFc`U}ad|1%p>PU848E-oYOC?}QlACY^q z#BcRkXUW{-cMO+2?QqW?qVqF)J$ioAgk)w!#j{0PlSP|CQte(pQ+%fTtZ)~`Vm5Yg`Oz^Rwvd}7U&CW}M6a;^s z=q;SR0742utxA5HO^+}P*umIfFZ(JnHzM&1%~<#dTho^bad}o`eKfA{@wBSY-`z`Z}4nq*_wO;EudX2Qgz_*#O~JHcT$O*_Ufj|ja&m^7sH@FOi`*YASQ%WE-v0e_#^AWRoY1R;tPG0{gp6t`$Z~6WuqalRNaH! zmnO8GT(4wT-MS%o`u`&Bz2m9u|3B~}p;Bp>iBv{*$jHteSyA>@gp`QvaFP~M_KuQ~ z9kO=_$I2#q9I`p)G0yqDF5UNMeSeSNKffOC`_5g?xz2UH->>&;JcnFm(DbjS#*AY( z8qUW)k|rf|xO^7Kx|i1X%dFqC!^-#xf*X5#zFkX@g!@G63=gr(@iw&CS6zUXE!TeIuD9;A5!IJgDDDEe> zr-1j4C}TT{3tYMK0@00UPXrv35&e+^`&-GVOQ7&IdyV`IB) zkE0OJoBtzme*3B|kl&jLe=hU>nBiHe4=9$sP%$aC&0%^2j-X+Qo<@JIWX+3T5xZ+3 zt{$aQ&pX3sI%#ea9X;tG@ShdFV(T$AXVCWlo6Imc%F3Jlhs!VuVo!GiZLf>}dq7Tl z;?ID*Ay;nc{hzn@8%da%Vcq=QKC}L$Gmq`@;Bg}~uP_BQiU%)2p-g07pZr$28=^+_ zvAgQ#ht$WxfK?IL`xWrXqlD*$CiBQTTKz)QnZ2aoUGXP+aY1ov3LUmt)CtWnP;Zoa z10@F1EWtOKnjvk`R9+}aQ7m8r!Ze&h4f0+}ZJ*mO!O8!>%mNS)P@)0R5p#!&+W*5X z09`5_-amO5GWvy))k~|}NDIK#yft|yGI_;pJ-{t47+sX(x-D0d(uv`bde-se0w<%j za!<;c$NO$G3f(=TddDJkwnbAPwHK1fIMyxY zN>(^d90Dg=dH|%uRG|hDo_3RXWXF+8a%o?%8Z#EkFTwZ>@Xq@ebb-r@GON8YhrRph z4R=I^DuY+}{V9}c<)`6_HDZiyMsdQT^%YNMFg6`d7E$qOwMrenhQ>!YUJSm%xw+jX z4U9hhV>b#Y4<3N#6XJseqXs|%f@&!uywcN?JXH1(Q68`CWt0*8=rBG*rjHeQ)X5bKKMs3%iG-K7!N*XP4cG7 zP1RN@6|wZcM#ainqyMJe{L_QFDj;N&2@4+PM8E1vzQs?kyF)$op?w)ly^#j^z9PRk z$yL=Kr$y%fUSYIxWWAkA+<3Cfp0ueZ^3e)xqd&e%)Gsv9=j!B2chg_o`_OGwg+w9t z1uN^c+g3U{HT_jV(q1e(u^Rq2#m};ULS`9g?mG;%-bqhGJ1E^;80aLCdFp7IyL(FXD;m#6W?*; zQ=QpJssP~k&Hu<05JYf0De+!sq~rPLxDD+R0Wb+$ABbs<0dgIIWq@OWhQ*`6Sn5B3 zCScQ)(HeaYVPM-#W;Ci$JSNlmRB2V5^Xb96PT`GpY}|Hb+G=XHSB<1I$T88j%}#YD zYN81CV+oS@o8982gZr8!3=k}VWsE|Bf;7Wlz0Ifgr6v%Y*fP)%bV_^l?QN|PBlphS z{QPw*s}yH}KY_I6um3qQQ^y_vky4@%QR!7u-;k0bVU_8y{gnOfyr*g0 zvh>L9sj79&)JMt>&t4iZeBQCc^mvd&jO*;B;u`Ni`VL?3m6GZON5{;}R3y~K9?gf%YX*GTC@hY@ zhbJKZO9p@hP$M`6Jta{TqWSgz9}})!o`3?JDFs5}%q$${vYT@#UIy2cg#VBYzMU`Q z+wVG6B9kaH@t-QYxH64D%jSvY)~R9maLM?>%`>*FgJ1}rPP};7BiUGJ6Q?}|2{kYw5{xqdN&`ED zx9#RVxv;NC9DR1@Y-j zyIcv%#Veo*ai=kut>i9HZ9TO=zX!cSs9j^#QU+@j;f8=Ehc9tOhCo5sK@*qKoRlrc zt?LP>QJCr1n(lR*jseEsm4KgK8GC*1f#~@^VcxCQQJuqxB$+?WhZRf^FJH)g@SkJH z)G@U4zmT>GyFxNSTmRP+>8O-G=)LU!@(Uh=8#&@s^~tp)S_51lZ}DD8*Q@yZPibju z{tdRht`PNKfZqSa>Jo_0J}WtzKe;l3f?rDVYS**m?mWMxLVA!IzCVO|tu`)em#*BI zw~Ioh!ap?de_iYx?(sjexMzKr-}=#AD~l_9k8Q0`sAoj9?=7_($Bvvi0)Oe4lHzp@ z6zX6$3PqB12^Ptk$zL@XbN1Yd1lzH0M3#?p-qY{_tZ({U<2Y z@fzeGl6t!@_fV)^X#^AQYBIlvc09fLfuA7rNENjbbNMFAr5CmD5^8e_&mZnN7V`$1 z9Mu1HVHl&fF;6GayE!=b%f88~Wkd0Cgp_!M*R@z$yZcOe6`NfF3s{jHPmxEzIUHOf z{N+L(vZ>_G$v*Cu?YPeS?E~tV_4Pw{JPCorc9z3u8ihWOY|PoWo^9U!xg)jY)~2`` zy}2W0K}pN=#`}caq4O{H{}p&ai?5aC&+kKD!RQ|v$Mz!dhk}{6mi_+y_o#V~n4A>+ zE7PcwF`p^^W5L8}o-(J;MUnK^u}X?#Kk*U9T5+uBpf3X21z?bKw{W1Y4qo+*a=(_j zNsA}#rR-DF2jYoLkA+!UTR!B{$ks)W7#+x3^H<3a%S2fc$xisAbYImay}ZqXjpB}) zkH^N36HS=Nq?b-b1lyEOSC+l@b2~*?PS<_BedE2qze;OtlFRrMMZrYg2=PFrr}_IL zv1te6xR=9~W(5v=wmjc#YX=#ZrdG2|f>)qG(DlFpEQ1d>!YIvsX8lR?-xUkD|kE<&E16>7ZvQ^c+$cQJ06yZyCkvq~V%V zjKZ26y9Nd*V_L{lIyw2c9>CNtko7RkF`-p#q>8JWSosL{9w_B3!U;LcxZuMkEqk=L z2zPduxQU01@K5an?qSMI@utO;ceJ}mX^1B;Rf-BsoVx6Gc5C?C{>{aj)Ao<7zt_wYZaVWwU?lN~yk)#%}oF z3Jt2tYFaJLc1^ken7`cwy3N=j<$-<{_Om2qF}m&nc6&(j@j9O4oE%|uX{@=eY$av@ zO^holfl2-T;6| zfB&1D1rD6-&ZSIydwVeLk$n8}x@QwJ(B=*#l0vJ`^ zNJ~qZE&jy`joIX*CN%yJt6lw*``kwTT3;*a5(Ef#^SxCxo|TnHQ@>DH5>vKDbZ>a8 zw5)t=!4glZpI#jwqHSE$GNFmb*iuJO7bdyvk)fkK36swrFqE!Xb>c(XRxGB~r6a!W zj=v^m{Hh3S{jen+c*tyb$~44KbW*tjlXq)Jl%{+qf>Py?`T<|Q=S$^l(_*W$#~UWv z%cY9d3{1Ye_09X z#1N%GJ6hdjH7py)(?z%`zeKPY_--hgU}mzbFgCtJ%pMbLba9(iQqQ28!8}RZ4z_dO z;HT^OfUf&^3u_iWjH8}2nrQnP9?D{BXZXGm)D2@Z_ryWdI zK|viR*GV?*TK7!}xCrm-qV;A=!-|K_C%T5JE~oW{j>OOp%yg{l7>ha{SdF8Y+CyEc zB#o2|(_nK=ShKM^9np|s^1Kkm2Xud!wVTNno@xcw z@Bx&qUXg*3s>3KG!Q(urDj-Gri#)a{9B^CaJrQYkSqp;q^}867fiSMwi3y2o^{7F3 zLri#GX`8XKfWqvcr*BRIU!9>gXHIo#e&U&*JFnC}erIb9@b^-ReKM4>UKLxZ-CT4~ zm2jlU`E1d)`GSsN9^2K@hV6X5Q6fJ5SPW6ULN&~FwhO=2LW(EId=du6M8aGX7>-V=&e4+{Dg zSdY8coSc&Aa<1;%@7+?ht9N$7e(Uq(OEntKdqVh>_C2eXwyGH>!KfG;mKWUba5q`m zunDtXnzQweKOM938NK9$0&f2cH|Y#&$UpReJF-8U}_bT_*Zpi*8raa4GN|5 zM?b-a4CDmP1ekRu-%PXMi)i|3NTIo42XlUyOHrf?OONt0HdKHPHK1lj_ZW-+rGOhK-1_I=B#5mA-voZEal*p0}=1(Aqx> zB972>fJZMH!kq38qukZ0mWg_I7<|L4!_4Xhs}i)M5Cd8bW-7Tu?ZJ8By1m11h=$Jo zmm}Te`u*lsvn%-cTCxRKMElBI)6@Eiha)#Pe+?1(P|G2e?}&y$uKn5&Kf`=hbaeBs z43|Xs=F(IR4y~{-Q2s=%u-tvgU6En)(!qaj%8Q%XhPzUi=_99Mxcg%_A#Ct5Y}2@# z_QC}3id!CI=tK7KD4qzv?JX}|vD2BqUsTb2^tvr8B88s}`KreQUx)`-Z5`K>@ZrOA z@g7v^m20GfYopHg0!Sk8Wnt?cMl`HN-<<=U=Cx8DQCFw$R5L#C6?S(KzM5&bUJQkt zCF!wkcUn8S_>d^<25(e1C0u z)gPxHXZd@mQe9f|ZCmSygVg4V)^{gHucM!;8ik%L$MbyCXyo|4+vy1J$FkSn>o$7C zSDY7qV_&c9m0(a9n&IYgsNLe4oF+P#S-dS3OO;^&M&5Dxn)Lj z+^tT#@pvGa_@TSRQba0m+DB{WitWH!WzO#c_vZ4I#l$L_Q`Y{?Q>zPwBjl&ddDk~K z%=!hLD*VG(m0Xvrx6*4&0p@0J{{u1u0AvB-h^pf&9@Oo@aN^wAPWh}eeHe~5VN(3g>aYqZNT~tbR~KRMOhELLWmK9IR=@_?&a2-O*ZbR zTgJvMK6l~NfsPP}S$Y^(!Ce%Fn+J)c{3cryoU7@Z`O7>F^>JT_gthmV-S7!%Vc)0YJ>K{dp1$Di z#@&unE*_M@=CosaZ&KOTw^03f-}{<~8_}rnZf+njw4~tj)+4$0jKWiB%V#(lIMsjR z;pz{~$fH-Y%Oq)sOHPVr?=T`OAGHiQ=_;+MX4sBarhn*KIHi#GFJ+95Ts#_g-fMNc z@Gju5)uiH!L5fFfLZZ$~%T-RL zQ-E#Jsy+lRjeGJ^ue2cc4K8lsfByXWvEShaC;ia_zKbsbaY94Ku>$%OD^*B06lrC{ ztN|gB^UzY>(^vS*XItqvEOY)QAjrXBe|%hieZ}zG{gcoc)LNzgjK881%$i|b8cwVP zd=SW;%K%|?pG_c~(g3hcVR1%xWL`shu9wo4X@@Zl2!hMt&axD0KQ9 zu}c%ib$n=vobQq!RjuUx#;e)Ff@m@O0#)+L3Y(vQqkE$EY8%TDx*w! zs3X(nWr=p56`E$GGTnP?BYy9uB+3PgP9@d7Y{;j7LgJkIYqGj5@@MJ36pom1?S_v= zAGb0ZMaOqq3p5v=_*H2qgZ8aIkUKy$Bk?F~Z@R@DZm(4&cNw2$V}Nv;r*wyNaMg6U zW|n^s}y-UA?b~v*{jtHc@3b&hIituSY6!D4-_W8jU9i@eu@>?`lhvs6#?)9U8PJ z0g0|rId!&-e^0GXnlh!1^g-5=+{7bBQ5LN6FSWSv_kQnc?m$+MhM5SVqM8^QJJvh5 zH+x0B$ge9HBQuRA&Nhf{72_LUzqW_^a$+{=J@vyBt8bD?zf!bA*3~OsYqwf$;Z|jc z6okTD?cOD{!1GQS_t7 zccdgRM@o8nO3$%Js__tVBj%(}6XHoMD8D>BW8uu}xVNOg$w?r5o*Np=s>m2pp2aM8 zU5i%5)O~sQz0Ytal9!J4`8~yNr^@c$C{sz$+5B!av$&`L9CtCpmrg=kHQ6TB*NIZD z(>sNKL8W#V_kbGi9Zzv*i;rRa_2BxtJ33h-Ei!X1c6%R+#5njWh(y0UsJl-9#tfI(Tamcmj8AA1F{fC;K4(A?PlU1Rx|bt)I6H7-%~YxMX!_Wn~;~&aF5H_ z)Qo+d+R^|z+6gP`ju__iFD5l?eRTb<^;*@P8ze<>QB#R!mpkt*$wk5At!u}ruaIEu zw$it*@b8RdCWmpF3|@48MEdNZU&N!Zp)Xs8IUgLDq>`T$gDl~YFRG_(t4KGG_|Rhn zIUt))89ty1&eiUY(sdc7J# z+L?DqvuJSp9Fe1vp=6FfdxVENMlRGUi_=iqW6XbL(L(X;IcM{o9Zh~5KC!4BpVFqq ztD{m4rxQ!7AZrX34+jc|CtXYV%681PrR^qBJbeJhH{XaWTkm6EN33dx&{OGHw}r~p z@OFRObU3>ftBlcj_W72jJ1rA`>A5~EdmAR$n9>0tiG=p8&Q7T0z!S&6?z`y`O`e`i zf0r8ibLV=GCfrNQ%?&0>0e%Z`5m$0(bpU#$YjYTU46lV-=<(2kt~%iRz|DyMkHp>A zfrYen%DS<~cYyE%!a<0GG|0NZ*Q?hlt_O~d&g%!H^lCP+O358+d>KogZDuuLoXg_K zbkCTa<2^^^7~hCxCdHJ;s{6+a&eFqsHn^d=A?9_iY>mP}PX62#p{A}0zT~hL2TmO* zj`^e*mISZjf2@b=Srpd#=s(%4tEc2n9N8NX5?d9@=KA{VdhaWt?8U0m8pacZv693X zGPcwW>tTl}$SO|K(RFo{5)MFGx@>RN#~3ax;Swxk&nHr66c@EMSyKZ$b3F5|9^HjY zCG6OQCKgT}abi{>S8Qr?cuH>A#8ZCHmu9Evr{o=dcWBAAT`W8k5$G%;`t0^%3ZrRYv- ze~=HNdx%;cOl!w2-Rg!ljDaA`YTyt0t)N4w-BfDR=;Hq3M8XX5Y8LExyiE&U!aYOEQhr_qWLQufLXM@aJUC z2)CAe7UQ%>!ffWZIEPulEEN$pwVK0KDx!Gl*hDsZlQ^Iq<9ltZ-}O<}_kyYt=(IuG zMC5bI80K1F-cT5qX4BH19poRQtLoCVa7W0NJc!OO&_&0`&Sk|oR87foPT`un>NY3n z@}N_h_-l*t_|!1H%<@+?n~n+d?E> z^D21C5M#)gj|7x|gwe&MjjWr6ZUKPNVAPQut8pw%`pT7qa3FvmCv&rzcQn9yo)=V^ z*`>a_egTYW;J_gT4l_dk2e!bQbdI}26ziBP$bu#Nt2+H5(Q9uB$Ilg!=xnj&?jNpC z#aVLHvM1>WL8-wF}{@)h@+YF`(Y3nCPTmEF0#Y#-M7AU`=$a$vv`qb zy-9X_b=+L*04l58X>Tdr+f|>mYMz)VbHcpDi2Ch(yuM`hoHx${~V?ZafYem>R@NoPQH|N(UPLNV^ zkiA7!%|NsfXms!K+PgcfE2379`&|gWZ~a4qxF&)*h{L z5)jXZJ#)*;r6vT@p^5tQXCPNwx^4;;^;~0$ep7fIIucOQ7g7f-N>*Ax;}pgEL#~r6 z1eBQUX#=mp76?{(^;|b6ub1>T&D^to(^Ba-o5wz^p!2`d>d?$}8I${yFy8a*Deonf z$iQ~R+WjbPJl{@si(xBD)u`TzZOYhdW4i?OwOKA-t!ujSHZ&!d($Y=4Ol4sH)t2~^ zP2;wK$Hcfc%^LEsB3aV8-$0{lhoD#h{Q$kCNnVeqn#_(`6PpT(Q zLp93XyYz!=TFB!v_K1JtRZGPRnx}4-q>R6xLS_23jGb^~&e51W%p1XtbIX zSE5p< z>CE1cxz)FQ>Nef>o;5P#Wg{8gka-$Jp@&o~NRe?VTd^#Y?WIi^#}xxpU#tUE^w8NW zL5lPqN$JEA>92dLb&ADR_uQjm_gevRRM>CZDl%08-NcvKoBd?#dXAR3V&??|kRwAO z=K^rmY;0cCouc}_?IB$>w9?}Uy%W2ND6b4Yp|=DtYg=3LQ`Ib)-d&w{t3{!#1xYxt zKMJBcGu+-DL|yw78|KRN%^(vIU2L14n9F8{ef{a%)&V&F|AY$6W?>RB-~?&@_y+xoSl_238bKQJ;Iq`smhNwYc{11r8_ zDs0^aqEbanqB37RJJ+BZx!}HWY&et5S3n=iN@4~kf*JBi{0L9>d|oKh&5@MCiZncN zqctPaW4#3F&>bozJ9wo~7MTA6#N8z*<HCG%V361tY{V|GMWvK|!1&UwOjXJrEn4a;(Cu_!o%z|3jDhdqRd4a$6v%f zn7<&Dbu_(ROzr|Y#IBZndXA&`~nB9=jdv8}VaeR9`?sr&fP%Sgf5 zkbE{WRwXfI{&=KP@g;_SnY`vu6@0mGkGU^jm~xJ3{pRXwr3zD#T!IODL-d!$8Hwz@ z{$@}c0RFune1Q>zno%r`sLWR;s5e!idE!Fckg~VGy|uvjRLlH1i~W-=qd&|f4ur+Jy;T3WUhcAV@Hx!7@%H65-}GNt?k_qsukpV zg=AKfGaf3e;z9WiVGaRs*yD~pf=Zx1@+mWum&2Qzes`|}*w3&C;65OJ%|?<*kW_)y z-A&;);uo6)?U)iL*WE#>NYnOWg;Set;l;DZA9~Spt5=T13qB?b!C!fB|GuIyIo+vv z;3@y?k0LC{LG?*WN}BfQV$dPL;%k{Fa#{o<#f0BjxbDjYo5S~#yLI?NGf`7}o(b3% zw$a(Ss>w>uaP%j!AL|!{I;DtMDXr!Z4e158AI;xz-(ZLu`E+7PNqz4y=Zh$30`^SU z^xAiW+)IY7)II>^Q49_(=|6OZmrOcUEIQ`Z++~VRzLnG z{Y0>?(9CWV1($j-6)4_%JbNsZnS~6zZ{IE`OZ)P^(15G-wg8K(nbG;ol|aAhdGe1L zM=t5UEp=Qt_P|a4i1~~$+S~7H)8lmAmD9`|lC1d56!xppSSE zXnn-!q@1Glr5~d-a8bdJ7m6H=-rP&jviO--Z z%#)@3_t2i~c4#&Czb5!6;{DtIJI4>QZnQ0|S=vN2 z$N`ZJ3V-8VzZVw3&a!rS`7$Wi8}UUH9pO9x_YUeB_<~Yre4N1_!&dr5GS43q68Qbo z>U(OTw0PEYU*c}6QCg zCu-$6u{+}DJGk03M24F3e(vZtH+y7HmP)REFsYz%C+2z*Ear9!0_w0#1~ux1XOk}cPlvx!1nt-vj-1A&xM9AA_FG5eKsN}S)gG+ zZ?9JS4GEq{9b8yJBNhePv543a;EE3Dhu3L)*HWb(V-&dqT1ArYKi#@y@u7_!a8=+` zYA=FX_KckQpQ-qk;0EaxKr%v;#)F_5m}V3@9UVNf)HWwe<|$$Wsq;mp1ba8*x&#R`CUtqp<-DFZo2h6YHHA= ziF!U8^~*8MiXp!&SC8jQX6DP@IpMVz<$aunbBI4g{#*KpY#b0kcPW%WupWpoU>^|* z;Q^J)V-Cy(urvikGwK=iP-A6iY^<%vL=nCj1c^U9IeL2f6T0y^BDRBvP%zvPn)1K# zUJKjt>(>#uU+@K{!+jSjfir_4ngF%+fj$?$MD4zkc3hIp{s+s}v%r-HFX5V9-0?aM zd!0`RFlR^rR_jvb5T!m};^TT_h#s-;j^KAP=--tx_cwEN=gkg0K~d8 z0SAx+tV2YJpAE*+vg7rp&_)4BN@zB27dmxMSWX810u$^aS7npz2#OLQ6xG)0*ToSU zbKio1?ArP|qOT9=?W??>HO3!{ZFTy9!ToI`N%Jtd;3R^k##!3 z5u3XHp&r_vth{L8(Pasxb1vn%Vd`mCFM~2CL}~u2=H9sY2I=sD6d9c< z0cLVvdib@0^b0tIzFZ5HUpf-k<2^!CaB?(<4yvY{K6LQebr-qUHRt(SkCYCzs?USh zLY<>YN+~}Y4|SJwGZN9 z-z4MlahQL9m;#G!FrL@Cm42n`Ot}bN}crQqFM1#2RdJ!OETl1){NXMyiS? z`~7fFkO}J+ycjwly}MkKX3?Ju{%(XqT)kLvt8L-xB0tYZ#V|&DwMA)bPs(% zCq~}hW5vs^+mP#hy>pFcXwtb9gvXftfn~~L%@6~=gY**4^Lq2ZGL0Sp&Z_aiSEXWO z6&00vFr%7W1qec&9(>m!5@k~ew$BxDV!m^h7t6^gX=^K10_^T*Wv7${sAj~1wh+xu zt4qb{{Z|9bc#fUN1Y-_RvaZk`I%rmZs(#Fyg89~?9Y8UrVGx(T93Kj*A7v(XHZ9S@ zO4d24C$M<1U5L--;1d#0N{{J}Ocoan4bcndQ8&XQX2$QOKowVMAX`6=y#TJHyTn_T z1^G>jd8CqO<5Z%mq0tZa0+~|h1!b=HfW$bD0gEyAq+USuI(TL(sTY53RFpEutGf`s zK2ha<6#lN~+r!?h#C5v+U5~J=S?RIOCK2D*FJgeunO_(4w{9YRA zGKfQ6c3CuHSs`y$H3nN##d*6N7&^kyri9frH1r1zjkIkBmEadZlmK9GdQTYZVZ>8l zc2>EczhBj}kya^qdDMprrbhgl`X!Fm8g7Z{{CGad$-({ZP~pIbP!l-gKqWe5&PdRJ zPXV#1z177Z#HzIWYYij}-}tX`8TKZ@46UKTEhckM2-EDO%NdWgel@%o1Ja0qwnj$2 zK*v9T0{sn%@r1X(TdY2XDNA3QHq~L+t7sv=SkK?R^bkb(+=CPglEl4WavE(uXz@9Y zfs^iYt|wEMp^PR|BzUeJCMV1lIQyukJ;q}1xk{<0pF0}6d}tF z&G24-{)&e9caqChq&c93s~u{f+SXi)!6Tc5g4tCzj35eotuI>mp;o3s=s*_ifGOuv zJY=NWZj$b6_kZ0Nh;e{ki`fnyq?I|WquQ11yw=qeE_NDB`{NMHj3mEj=n2M=(WX%K z29SNg?}BT?PMKtXNUr&rddZOO;LOIkb=;NCZvKK+V2GGQxb!eC9B7&QqT|`C9 zFTN(thaE9-4uTqSy-4(?TqG>pd~~54nw~AyfA$1>=t%sbNzsP@mUp*$~+L# zsv=Dbr!@0yNsMFQrOVu%y}H<6a-8p{zKqaSFH5i9s3E=(k6b0Uqxl$}*iihu$cjYp zqx9(_tKhiYR`j&Q$k+g9N;q}q6}rfV6tUYsew3KH_|nOg^a`Um#+ms z*i-cc9WX>Y2T1jRdd>lmF@{vj^~A zV5+E_+4R+Wv9AlvNwRyJC}v>i^?kkTQ>6a`nn}N*>oA^SJq^NZuYr^dt@c2%cSrpP z^DbRImF)}2d2aqTFl#|HmleNJcRa8*Pp-Jvbn(690Tg5zCFG7$E{n0VH-q3X-u3m@ zGsPM6Vb*{o2^pihI@Cf+^^^zRhWnocJc-Brcc-}?Uw`~*X8mv{s;W5d{t`ZHu4Ml2 zt9C(kdqP|xb9_f`#VJ5d>8wUN%F7!DOl4`78`C~P9<&;%lOdm0`6b?Zj97-mP3V3Z z=~e7vv>>pvvx7xW<5SW%y+JnJLQ+^W9g%Dw_t}*G39Aj45B&?xQ=J($gm+yRL)Q9) zp%$y2UR_xT?SwAggBY6xcS{rYdZ`h}O+eP=fkWv~nbIg!*Oji8lyjJbL=LgL&JuZV z6YI(Isb1GaW1YY}!CvP!h?Ps;Pd$6o8P1fn_1tPjxA5oCC4@PHG-pg8n4xqFg2F;O z=kVK4-PGZaKpMwHmcELxJXYf;Jb@_56I?#=%PB*azDuEtMpqW;g#z7eE6!?AEv9#ZJ|xS^#9zDP7S-IDCAx~1*($KIgLL|Rr!yPo2~k0fPK766jM~0u zKG)mb%{Fv!7@U%xy~`4`ga}knsr$Agrp*kZkTX5LqJ>?mK0B~7JO5ogPm?PMqLZKy zpTDBWro^}z&Ir|e6XIVAy`rVOEYYx-TWzFUFg;6(<$_A@$=-rfFPOpp?CXovvU7Mj zt8XcIbrElQ$u&$FbkL^W1$VW^NpZJwiG!Ua7`)todL&Njsq0S%kl-@Pzdz`;voOw6 z1M2k@m-xQdDYV5Tg3Sni1Y~fucuAj4)W2V?TwSkApm?}01g7n7Ijve1P4ZF0P}6{F zc1LZTR!h8$X-<+sRukFx7!gt1!3N@RnVS&NaL!+JB-h>;bHmMVEw-?sgMc+Wj}ht( zTN+>&Zi{d@I+a+g>a&{U{`G+plVa~r%c}v;2$TdOh{im; z&3%lIE(9s|lonlv6V$>Ts3YpLn9s2q!YgeziZ3HHhmXI-FTuDW*9-qLG+<6cx~cPv z&Ae#^%@a3Eu1NYPiovX)9aAN%%b?-@dpCyJZ)Q{YX-HC?)t zj=sJIL%R=>ZW=~Lg2O;90<-`n{p!!Zs8%XFrG%U4AWQdQwV6*cYH%qAVOJc9;lRLv za2wuHkdqEuk$a6kMexd%+Db`eb(S7g_}rfG`%K)76K4pn`L*gM|A=3?q!4IaVfO1% zc(HS~s@>YJLgS)b2^QvGUYgx*80iJImr60pVAfASN;_eIqx(xr$YHR>gd(6vJ+HoP zen~uUAgU!LIB3KwittXWzd7R zTd->D-}xgI{J7W%$nw>|fZY=GuM1v3WFug%e)*Z}$ej&&K&e9G$$lb0l&gpDtd4hj zdOCh3i1jc8DOk)L-^VTox~6Fzt+%tWd5bQ7P|kak-}$bu!sn}=7ZtJ^{rK*{h#cAa z(6)q`;l|QP8~<7=`GMv_AO*4nEg6w39l6bi5fxb9+^m5RT&-{HTUd~r5|yBekq@`< zQj^>o^^s(PBL97Th~Ui)Rwp=n$|Q%qb{^f${NAuu5~}1j^;hyvgdI$d`m0(;pk$Yx z@{dhUe~TN`d3XLcn7bM%__e8u6GQEaG`y&CmlDnTWIiWvd%_82RQIc27)|g18mfsc zbk@NIa`)~Rhg(x}Jwkc)L%*F)u+L6wL5{W;)IZ{632Cr8+vmzQK|Isp)Ad6(?#%CB zPn%wKlVk>FaK$jiIggLO-hsBuEh&}szPTuCgqe{P18U0iK)=j^!iNpiDehdGhf-v; z2@xM`LSP{goGBppvV)inNADLoA9^G++o&U=`qhfj?#Zg1$j@YymP*V#=}$Cbx3#hq zcU``-|B_I{;M~TiA7*xTbOeWrij;4S`zzIlhh0053As74^+R+^+$3An8m|@N@HA8M z-b0P>pn(N7gRIzE+^H1D{bB|Yq-K9qlMIEjKmk}(c8eG|d1uDkeo##Y;n`Z)2WKQK zmy2>dteMgs*ON|QgBN4f^8;Xrqy{g|$tz+VHt?K`(hLMX?st^T%<89c8jD+IpN1oe zGFL0kz0Fv_ZanmXVcw2afn=Clx44?-hZ}gVHo5t8F`+w+$x9y75=wKvGdDpR3eJ}G z@T8)wIHh9V7%SHJsKkibCmjo)x7^?r2BI7Z`!(3Q!ByXQYxa1{Ciz%?wbXYb&p9g| zhE=ZAE;OsnuR?yQ8J^;5cdsDMcg|0<#Nlp8`$Q}I=iY01>q0{-DlX`IQ!d8r$bNAU z+}`0aj;`0(eRnS;YQmOX%AP5c)3_X-V4Br0yp|f0h^a!q<@wL;w5%*O8tnlfkb=go zYZsO8m%$Oj!U#^l&+-C{V?k6!``i}fSP0ll_3KXrOXeB+=?$}svvXPxFD)cpyS`yC zOlzC&?4G|J%nIWlpgxlLn&19SDC=iw5)_w^0I|Q|cjlms14P8xL7q?uT1YsCwiYOy zjE`qQJ_6x^l^*kOkx-HL;Ea?oh1aWuA2oQYPNMs$_{t`?4y@XbHYB|PJ0I*qHC0F?UH zqDy5=s4dHhXT5XUnT8GSmK#Nk<;SBl<7eKPmh4>YpZq80w2xR>dfgDTd{^4OHt#I) z9ndRQ@)u4*`wuP@kTSp^xDv0`maH?s-uyNfDcozL5+6fL5?YBT0u*wrJK608qMoxV zL0zVCY!E~->^J876DgBHjTfk+cQoHt()wQ4FuAU13#aT~!K?>;suRtxgoHU1BNA!l z1-rg8b1Yv@o%ob zuhNwFRvUG-X%x$Fg?Xq^Xtnvt#1{wJH*p6`9e?dZwYUXYrk}s>ap<0br80EKMoBKk zv?SvT$Sp?w^`*mg)FbQaEZ+x!8zRN#od>$x-ig4Zb$BDdw^?P~YDzzFf- zxl~ZVg%~a@P1u@9^k!o^PC?6W@)psodFb=xw0WPvPjP7>ea0}+*7lFam6-NC{ zr+$koC^M8=o3G=+n7T%Vzx+D9X7Ifdifo)G6Av&703zM4!&RTHg^RMIF1zsWHn_Y$ zxedpEe*6E`BhO@@DT7Cah>TsPe=%fBM;B2xQ-S{1k4L##YAvTB3@@O?Bx4Wlb$SvN zV_B8{On<5APX5!3?`(a0*aYN_<$_sJRS0|C-o7zrapy+XPETRQOCT{rgF_SL|2NI9 z;hSz(^?x#o?)8C}AqnDb0R;H*@!*K2y{0Mqh9r$ftK`fHPTUPe( zzy8mMzh}vfoJFd!QOeN42i`$_{a}y<19-+MpwVbd;Xwa_v*b$cX~cgVJ`E48{{y#6 z)X#h8$WV~l5ek{rQZ!@3YX6hUFmBeWC`|(Ny5+*1^8!6u;5`bh_8btOGEsn08Nyr@ zuK)DF#-{c!DybLX85tKBC#c;~ibnDeSkP^{plJYRlaE)%5@R73ba8uU@^#6a)*ln4x$7Jm0nCbS$9g5h>7t1)XEGyh^9w-hH(j zkrq39yjw)^ARV-ikby%?OE~o#)Uza&2*12sgrL;uAdq~5nk&3g(H*nkT*GXHidjoh9W-%BD z{M|IG@3+kRR9vi|-|~O4`8**uV0NII7BRnNwe#quR~}avOFuk9o}pJXTv%63JmX;aZI2i11ka$+BJrz`2_{IM{G*pL2DRK&VL;HM55Mw|cF<8Hoev8)`-3I`D>=ko)Y z(FS4RA&1=D|VB1?`mIJbZAa5eXsaI`K zlX#@#virt+C+>sK!22{YOBeI(cAD&)BC45C2hVmNvg}ZA&Tu|%nM|jS+u=&;>TRL* zL-&`J(@nVM?d<8=0$3T%e>uGx}lM{$s1al32*=5l38op8s3ZM%Ysl;URhW=@*vHgb8+sMpb%fgF) z2Qa}Wstjdm85xskDsU5|KgxbH1KSF|VKjyZAk&5}6fi=dD1~JYN+gIDFyLe7ol76= z*%%bsG6m9KHs-$nIW*IQ%jZPurKVM|5=gU9pzoJKfi!c57&nJ^{m+b+rX~Gqwu*rFnx!w)$seBFZj2#T5qg2&!3xAf2Mq3Co{GR))$!u zHyn2s3jOmmat9X5ocslkeQuuAq_S`;8M3Vrombdgf-Db(f`rCxe!l?boVj{DNyiii zV;u`wv$p~{F)1};xArRg5bJIbqK(Zo`=2YAd&OuQi5pr)C=wvpgJdO$#~&y#M@qQx zA%3y?`iEhufLb0~0Df}C^{dyOJC9LOV74K|m29u|bcB>)*%o&UCFZ)!f>``@7oe8w z_`Obo6r)f$zv(-EuN8kipk}~fqP_~ghf2Q|vx(c;$R>uc0YjuO;YqU*lyOt#OWZ66+llh0qm*}_mGh(P}z3+<&kAt$qe^KHrkwAz57{kZ_CfX#Rh1A)!!Sy{|0ntzF}wJ*cb8(K-bsH$XhKS38D&3qm9Y z;iTQS0jFz+71+-(FkAz!0kIOYX@Hy6nJF2Bzl-uv%MTb?QXfdNOT%3szxfgrYWa-~ zKHYl5f4bBBEi;8`5W8PuMI;azfc~D0k!cb|0?(g;ch*+o90U}#Sh4C-p9=!v{XktE zb0|jSkmgc8OTOYU1>_ar1l0Mv?J1rC^ldfV%b;HMO1J$DzqMvtfT!(;f_vLBxdSY~ zmVj1*HmS8xE)Pd}3k(gBZNzs?8OG4SwD+o%v0ypiOHx({6^Y=@l{`ZOgF88TP$VG7 zGV-JYJfefLBthl~D9RcxfPhl?~cd^%0bs@nBuGvOpG(#DK}7& zMKr-P4OJ1(KB%X_Jm4Gew@Al&DeZCv@_~PnyC{MqU{=WXV>sudxmb;G|2=bF6F4kH z9{pnFi!0EZyHx(Ao9getdqRW0#L$GM6auTXSW%FUQql#w9bmtL`7S96>7(xJCH#Z% z5Pf9uf50Sc{2_ZV#z`ZBoTeEVssI^}0>5=jGoW4mpL6+y#-UDjS9uK;(G%{#Eox{u z1c={S`F}FRL+8nERHrkH0$o(6+?yXzMP~xWAx?s^#bs!LYt8)|MM6ypdJq4EsuzI? z!jJwdf9|9r%l_A!$|<%Bc0Kx9cIHxyDLX(bYNZ42;$DbFoY?y|+dH{r_NF{L`d^nA z$WVK0`N4*0(=D$U=j8{u2mvE}!Q%T1tcSb#|Mh-vz6`LO7XseszcOkKY9tjH81TPy zxgiA*L_npo*Op5bE-~4^<}XnI+{wJ7STUK_3WE@c>VU*RKHZSZh&;;)a900PRCEs@ z2Hz2`IzXHN?ZBF);Q&o0@WujP0W#>$?!!Fn#}C3iV`<7QI?Sm7`5X`%$8Av0I%lY@ zZaxfVy$jq{JveS;tU!Gkcu?F~%E_rSuI22=J#MVcV6-i&=4e&5==saylA*?{z9sKQ zA~zo!>yCr=Gz2*$@W7Sbfugkt{C<&d!5()4X%5tCD{DM`DzP8owqWqQ{Oe)1Z+hra zrQok{R6)xK(M0#CMBP~#uS4uz;Ut4m+?QW`j36qEq`*kzx-?sIn+tzEp{_fgFcW{k zcXR@&IzTxIaaDDN0OKc3>_>vDdHr$FIY*X}2O(86#lgKbT-lQ|%)=7%d)+-bloDCB zmVC1!ik6Z9<%oth2J9>n6iggDlbbqVVFeyK4hRZge-#WO4*@e-)H9wYdN6ten-hT0 zK-fW5J;+By$@>JKz(EOL!K}Xie#)tnr%`avy=!ykK}87!>R>VjViE>8r|vJHulj)~ zB`qCS*yEF=VG0Frl%RWg948i88G%v$&vozpFc)pKnO$7`0pfpKIF2}1xqPowcs-z- z)lttvREox zJsx=9Q{Ha?q;v2ikeF)l#`r>?#oFWIpY?q3D&yA=nd!aQ6b_PKe2uF8d^8uGS`T5L zEu@nBSdvbG+&viS9r*4HALAVTu48SVnoFyLwjr({S`U!bGU&O@bqH0W3%tCgQoKiqzM z%^Gq`b2BqJBPPXf%I@R79LPt3yhi$x)afHHU59gn)Gy9hG-*wT2J5BtyMadNm$a0> z>ne~LPwo?!) zC;YK9?rR1z{Trpt5)3Lg$&f$d{SSk+uYT73r2$C1f>$}j0QCioZU4{!xQR+@7Cg7} z)EUUeinbiWYb-SJsQ{Y);X}4LGWmO-I^Ru8>=f%Q!h*k#rM`so#3pNJLp>3&|1n#A z_q;C|Q`!&ctsXOveYg|>}keZ-cuTLlpu4FI03+dDP@i0kU3%+}JQQ#h3 z%_i-GcvO`91GMUner!zWgYm%uNp*L5oSPWaos|9X&LZGP5(=iL zPBfKwBaQt|lab3R;;Xy4Nzo;F_wwzH#GCf7t;$(iK;ROZ4j#qE-pDGTubH-ft69P0 zSNl_qd;9~h@H85<_eD)QS26Kz`1A5vjU1E|cy!_MPImiyk0|fUV*EPhZ%s=nVx6WRepZPTI?6hHLPk4(?1!s>PbZ*4y&{6oC%6?eq zoO=pjKymTac$RGg=HlyUGKQ-ivYyaWHMhmkl|NYVF4mqRTef zjpwI}7d6$206EBXyzeS2w$(RwR}7F>sQciD^D_1eFuku-mQExb#);!OE0tdDU zPSF0nuL+uEWm>=0b_RKlXV4WmfNL1it@+MLJYYI3OwCAd%gK;(Mt3EHS&P+Dk>dn$ zEY|i+H8HS1LpR!ARaj_`J9$93wjZieHSEr{vnN&6&_jl$$rdB8t>6Z)gFgn-YP=IZ z8{%eKaDfNzRo66iO*kW$HK zuyPsko;Aai2TOHJOW4?#7iz+&KY+anj|gb_;W@vT&eR;wXL z^WZTRma{uZw~UJ(N&>RJ@er%iRlSo;#W{3e)PYjry{ryTu35rEtYrz5(O?qp_JA0} zKmkPKN4RBUbS!x-lO1_rB+cpj8X$y51&PSm>+#s2+{R@dt&>~@hu*RfWxRjq4!dC*g9%zbusF(N5Bs>wq5DJT2}^6~iLYXmt7)(a3yG zd=sR(*II|D>8W^nh0i*W*ED}nhe-FJB;jz}UGZj_ZZHv&y8Y0dA5Lgk(@NZ47f_>7 z8Kvz*rXVMQB5LBBd~}1g%Y%o;EYfT2eTZ6elR0yl#iIhYu|Q=}-{bhrZ3mau>9Y56ndXe2k4$w1fE2JM-g5rYR z#%d3}7)Z)}j;-sXeFfjefyAsIwTIX!NPwhG0K!v}5FRODQN8Ko`TD~|M|$o1_g4W9 zQF&diB7)mThZgrGde!_Os)JNTKgY@Q*{RQ^gO1zs2kt2*KYn~A1nXvzt@|j;Y`=El zl{TBw;|t@*O#p|6O1U`JIhsR=O|(y=bb*G1MyBrL^F7C(gV5E`(u$91NwY|}b0DSw zjqqJ)sd$5pY?#<10o`3`u#&?qj0uaYDJG)&;szNl#9d0s9ju*d_dQEY|fQb zYg$gE7X&Kdb*dsQJng#@B?1!=! zMnhUUcn?gUF$s9`KU~twR5X_k-eS$tQSYzgGRI9eIPvhc10hx)b3{!T*){@=tRkB0( zxGRB0)?VsfpQuc%DgJTy9az|-0=Y^H@J3g^eL59l8t8bR+Sr$tmJVlm%w&nfN*O6u zj|UwQ^go7A++DT|XUY$oYxM+_h!#K3;;mLWuIRtXij4jG)@_P-8rKX8tw%-DE-`+bdNa zn;oqoYUVfI?zh@3PyTk7tMetFWe$~0Xy={KXBc9Vj}p-k33@chF`hTF-mGYoX@1Z8 zvRxBbWfl{Dx{MbmP51_Ooa0OWLh8vXq0+Uc_gsCINz+l?KL6DwJ3ciloaIp9pry26 zCh0}!FHm}!Vg<+~UEiQb)?VUb{1YynA5 zWas?X;-}&POCm0=QVzu(y!^2vylZOO$^V;mse25A13QnA`?Og@coK#kj6690?;zQh zm_jq8iCdERVvlM5W1AE^vG=e455K`l_D{QmS~{%*3vV)Y`Iitc>~blpCZUpBt9Klf zN&LBcI+w}#dxH7z#nqHrkRk8hT$NB@N6hBDdy6;w%#&q``^WzxU6yU#w!+Bo@v#-- z31k-F@cmVg@?&9u^%P<`63BKUfeS%Dnm6aR*WY4SB*YIh-wS>5TCfxR^=&elTU92M z9e;Cnx=*n|gZA=LwGTDVRd(!i)?;rURa2w?_`= z=U)=~4In$Z9zRy&p5|ppXn_7c&HT0%b6I|$kxVSu9P$-E19D6_7>-R;%Mn%x{G=nHzt&S6XK$TzQ@5kzJ&Y%V8L zbTldsJX%wukL0XiXz9jF@S)c}mTLwa*da>q)%otx0YzI1(_U!hUt(3&s0bz8pD2e1 zE{h7Au2CsgI@=OXB~8XIQE+~Qp32%g8s=!+54vDJ=^KYuZ4~iZ=_fSA zPcs%l`!d|)m}{6p>m`CfGYX=y@cIjJ6JA3>-0~S@SzNV+GjSzia&qp`$RP^_IaC;i zoFqiU4>#Of=OpcZ?Zkv+dAfm;Tzq#(zw=@8p;h-JSUc)z&*SRD;~Iv$o#{oB!?RSN5TE+cRPZKisITu>##`ikLW$qAkPPY_ zC94E{w`j$KLa9oNTOh?FOf6FY(vRd9;$EqexFP;MSLlGJRW4HP-48I+QrnGuRs@-J zETbCZq`%%L0r^qO;FdY{#t<+|I4!$znPK74ygeFYeG(EYyUrqtErOlSKyQyzlkm9X zQ=ueTcN9p)?Fb|SIy4eb1Um3yFYl|1&U5Ff-vs(BQ8U$!wU2K@<0}rlHCi*z=%ku1 zx2+QYy{XZtJsuAds&ZwOiFR#JP3!eTaNP*`wi!#$+i$!O*fanP{fAKfPXqE9;;&bI zMcdjipI=65`8!8N4C`@Bap#<)a|Iz?ROAaL=Dn)$@$-?}v7+NimGx#h-MC{V2@*$* zHuL7HyN!+lwM*4n9vr2;$i>`JXf0r`9?z?qI36^^Nv*FV(U&6iwl8akrl$q!ZshW_N9?M))NT_a6ICXyc zdD+8ShofrenO%E=w5MjR>wj94O;lx`3@zfm&^6L+6B{~}o~pB=a&v3qBQP`lft zA*qNj>*F?lGV-|95=*87--g-E7CvGCnaF34TZX4eodA1h zzsK2- zJMZ&WU?s<|JhRs-P+9!32Qsq%Q^6WOfh;dh21?R1{j57R77nLeT@6NuE}EKiUmL}z zLW|uwXD9itR?D+z%-a?fnm;$uD%z>|#tz-E- zHi_xkorqHhHHi_Bs1Z7KN*1aFP%Z&)gt_I)M?NALmrmU3aNL^`=8YSnZjeLhM&WXc z%lSB0tAe*r``?}u?goWU$zVtmWJry9^jgN~F74i9nd|$f)?e`(J;f2E%ssfuM|f^4 zC2?ZkJVv~{)s7C!bN29jzkSqE#?-b(NiI7nRhe;7X_`UJb;Jx$`0V;|nYljI=~<9! z6eQ3xe(&|e^F*v01Lu_O8RXju+#g#n&4i zPxg3_WrNbYsh4!_50!4LV)E#J&n3Mj0}CEO#JPJy*D*}D@Pp2k1>__)G!s1dQ`0pb zcOYS0=Fd(rum|<^SpWb?71$Qek0%df*&RN^ynQBQ)$!S`4R$aJ?hI1>W2uzZPSm>w zrmb>?9+usdv!C`C@VCZ7N^uf!1nCm7O9tPnb-icixRBnzG7X%8 zhNdQk*YKruJWo;t5%?Vc4PiVcm{4NnpRFjIz4aoVL&{QsXwkufO6nwb&<`7qW!7xx z(2NkXZNGnh)#7nUvsBW3IzH08W_w!CtIsA2|HGbM+@avoSGU96&8&GCH0o<$=@rSz zz$H6=HfBA?wIvnF%y-Llq0kOK6)0U@Q9$wr9L()jo7}_W&#w`Ko@op>bU~NDUA|%N zkw)8-6?F0!2~@RAkgv(1`un76OT5B>!&epDI3I1Y#97{549X?Lvr2o8+CsO40xq=+ zv2ivASBUk|fi@>4Wi6qGaZ=Y&>_=Mt})Hll{ znuuvSZIcTVLOy~P$4sn)D=0~sZaX>IzOK z=H{}%*BXCjk;1HF+Ly}v1PvfiTJ3K4!r8Zk@}Ag71wm0Is23qsL+9$3s!xO^WssSZ z`tvt}#0VuH09xMk8iR_2J+jkhhGiMUiyu;Ll@DqQG)NXWWbpU?ChYa7v*B}VQN|O&D$9{Gc4zkc zJo@>YZ(QT|$i|brXC7n$20;8%*{TD4f3TO!%65W0{A;OQv)`LBMsMPe4EN^v)?-qjGUq21928o z=Xr?|U11`%6}$>*QKfY=;VTUDcw*=n6m^ySuLA4W(AG|v_>O1zWm=82eqiqzTiabQ zSCUIU5E<3QtsC@x%)ac@yZ2adAQQa5I9w|+;kpU?LpoaMAp@5M@GWH2lJG|n%WXqL z^al=oP?b;)+`=DXckCGG#%;A--b)E+0MtU~>j+Nz&WF@3J=Udt9x0gYgv3 z7jSbRWmG@Nz(F%V?@pG>wcPESepop`3|$rWTRZyJW~dC1dCO(To^-Ac^NVo*X$g*# z(ulYI-q(7JC&Omeyho`^;T-#l`4$(#3==U`Se8ym09NuoBhe*c-e0 zg(+7O6^2H!FlBH)({x7V`pW@RPKK_1cgOPtTO01b%_OUJ zdQ}%sa-LeC;hPnos`q|hLb@;QwvkYakRHazE9oSp16Cbjl!I!HFuHT)8||XA(n)N5 ze0sR9sR?>QFR0T#N8kCfH{ZaxH1zP_uJ63}lOg0oh@E`JbXIOgI0FBo|Mb$k@#_q+ z4GG$#g+XE&!iESWBz1l+3lMi6<0SoPydK?OF!POy01v7f58`ZF?E&V9Iv zVm3WD3}d9f9F2txMzii~8;lgV*O?yW{|5pwg{LpFrc_4N>09c)LS*tV^65oMwBt~O z(CXOIVj(HE;+z=vqzVLOhN{%Om5%vIyS-_Lps#$5%$v7+YM9D)ntXOPkK*-YiqmqSNV^i{Dw@l+|K=%EpEC>w&Q9F zIL&i0TRI^eFwCe%C~?=~n-3cnPiB%)NMyv4Q@QM*pfr5mXImd@9|M{aJ*Z|J>=(Y# zY|G)(Zf5WHIXzh|Ct;r3&s?@M^=^bw%W5Obwin(rNs_De_IVPTEmCl5_45-~wMf^O zGNgl{5K1n0({5a!Vw9HTs8_M&A0i!qUL8Ie` zOLyrhB)qsv6S)?Y3QNAE_s?RuEY`gTARP=5QgW<*%)<8cw6Wqnyc8w6IlxC$?l091 zcFHQ(x5<_>kbEM(=}!7(9@C44^nH64D$Cg;r*k3}K?gVNnK_I%KlNXl|HXBd6d_Es zxB7wX`MgN?;2<7SwQM}e{O!-RyDsk}I=iu%qImKK~rgrvNt z*R#z=J25iOH8zCLx&7!<$<@Y9TS?;Xv*HW6@5El9QEDft69kjw-QpO8n^F-(+||S` zvg!7o|Gz0?yLN1-3rZ1szFV_X&Fl7)=uI0eW7Z~L##ysYn?fj$-)rnXq5=JD)bba9 zC>3pxorfb3^~hQ;wa5`By=y)kj&1_-*dA-;6QPEK`;Sfi-x%9d32|{044#QRJK<)C zT5iN#Qq9t6rWR}lG5*!ppYG=kUTTOtPi5ADzUVIA$DZtp_1`vzF8_698A6b5yo_r- zUhgJgbng55wR2i^eO?aq4<)*H*}Z6rb(+dGko@Yc30N41K4=^`5rfY2 z%C!{m_b6qKT7GdB-Mlz^DQrdkPo=^jpZYVF29yS3Lu*Vj3>P_aG&Ay9TDaBUk%zGj zNIVQ=WqH>OnHcam*r9_85&2oAC4%N&R^OeL9@xw1nkkC@a!S^y)bp4_P|dJ{Vq?!E z*ZLFj6g8vQ+BhSn-SiBK^O2cBy0rJ(3(uOB9PK{W4{q^X8Ud9q1TkN#A5?_!q!h`5 z5=f>4{D-3+LCgn&I9kfbPPNhCv6ej!LLz23pSlI%}_~Az+cRjf$>n-kiDImgn{7 zT_!I^&a{QEHP%b&zpp$TKOSc~quElyJDAT6df&X6m2`6GU99? zp|$dj=b66#W&{#=P;IUdyIMmJNzr|AyDNqH>%7k?o{k(3uFx3w&tRqZ6c`)@U=5N` zBujPNQR=ILwv#kdcEJg6w?=h-^LpXb2aVZ&N!7C5Ie}%Yh2gH8$bIV_x>5@=Cv#>1lV1LDmp z?^|@IY=Blwm7&sOfj%js$r5LH~9DTsaZ0SuZZh@w@X8hOt)uV zd~Vrj!bNGo%&AXV>C8S8sq7sNf$8PV?3B_3k#&R%#tLHpB=m*zdmbDiTt1)9KFus6 zI`{r+5R1AcluWo}_$scs#vXc)@huG0$)cq3_>t-xD(mCjQ|*WGC{4R|84F$8-*lS( zP$|*o7bXq!o`b`dn0rZc=V)JSI<7TrVa8eU^83_?p55xOZ_TSt;{{9|pHaZD#+ z42EzjqR$N)Ssak~g?}Iw8K*qV&o^x5UKH%!jy>~!ai1mD;I3y_C$fw02!=NoY)y&m z8Ufg-p5eCDRPVSCeZ@W}Y02^T>RtUyLy~`T1uMgsj8_+Z;>oA2j|HcbnhvsTrXjGu zpPc=j4Nyn6jWwSiKO|_-m)o9b+sI~77^IQSC$>*eE$>_2=n+y8cvhm-mV!K^WGE>k z5WqGCvpT47h=mDK863F#0qYFCTi?9SLq&{aBQ*2+huGZywRzV=);yI8=g|(nUE_1R z5Af0;wE0Zu}GjuhG%9DXQOd|tj~qIIgnOw3DJ zSa{lTwnLZBT!vHLTl*hN@Waf^^zcq>Ms6fT%E!0pJ;}`t?^qV{vw+^8tMRAzQygK0 zS#3?uE_(8p_qUakI^hz3xhZ4hkwtDCwRNHW3Hr3w@&Ia^Sc=w_hX!;k0)ELvOJq|B znE*!R4=?>-ZFqk2HMRQz2=RU|q!+dRV%V={lV_GVY-&Cq$ee*S*z}IOpAu#T za}qQbV*aXcY_hulg)+b4mwNEth=?(o;Crt$R&1=ipXK9cf3nylIVzSH-jZPiXgrFK z_l^zeXF8(E-z8B#Ruy>bg{^#lM%b88_7MT+nj6*)r}O)^dXA_#s0694YPK1h*~i7! z5q?d4HF08=`TC!B3Q~Tixnb2u>2s3AaMX23W)!Y%O47H285?QUvc-I{26{ppJTPty z>NMUV2tK7}KM4UabP%mW5%xdEuq)4RZ^cA7JGE0xaAXxDpaT`xNeWf;rx8+u0Tk0cFzB!h+t{eAZ2-F7lCQDE`? zDDaVTTLpZ zNx9~9eO2%+MybANBwAdFiQ(8H=Y-s(_YDmcUL&p$9uhuZNdAZxG@x|%99F#&T(N{w zC+9qu3~Tv${P`mKd%s%hz7DP0x%b9h@ri%>RC(U04BTSv(=!1^hk1#T2-PybwL-X; zFZC$DPy5d}1JxtvC-*w^GXUqg3ayDP;m}3*jX7t z=TqzaK+##gLYRdw`9$Z;){c&FrMQXhK`M91E3SRQQ+=Z_)5K5fI>-(ZVC~qMFZnqSn<3w z8ccPyR^9waj_bOG>Ou;yo#g7bLps&#;dS zj5|-Sk{U2yrU-|)PkuSOl(_^iyPSVS{~O$v7$M>W*V zdHOwMx;^3;I?lMy_B5Pod15X2ut5lJbHWy~rm^7_b+wkg!~^J$bMzRZ0u-fNveh6D ziLAK+>CUrub_`6t0e7k)Qprm_=N3k8(QCAdy z3zUwe!7A#iLOXxL=2Q@^f_mxoHfyt7TCSzNDtSk`>wBKc0~rL}nx1)!&(F#k&r-Xo55 zoRZ+OvCPN9+XGx3|GVlG#l#J_s^AiVf2zV$7}=5;9|xVnYwVA)76M#N2=0UUXSx6h zelXm!zSzjbq>v*I$keah`9b2?*xS#p0^7s;`uJ)L6es774t9jMNG_i71&>!78M-_B zt44bC{uI|T6HCylkl6Ub&HO~$$!A{sOo9Tc%&F>@B>Nb@yaN^8mf@=si{{*&*)7F- zMo}!wCL8r_*ITUJj75r14F?>yk=^<^l?=p20cIb@Cd^5@Rwgtc=GKdfW;LP+ZicVl z+{jmr7edfi>CiGK{ipW2sit1y6%4w;Ymu+r*ckkN#f|`b!R;M=eqFMq4~S1n37OAo z-uZMY1)CRK8gqnBguS@FKgWz@T)4KE-GnMFq|`&$&+^Tuvp=+!_Ho*p2I+_`BSR0Y zga6XoVc{b+XHFr(2OBEgL&4xG4INo)j&?~rXGk8?p{H7k;NiVzI+a>h7~?~W|7O({ zSj8_Tk0b$W>u91IB>kjA6iAN*zS|=^SbEP$m&-ZLR=P(tKS)Kcmr+TmyiyD2hV78N ziy{DV$uAHlW7k04C1487T%{W3fBj#p7|vubADyf@o6ZgX`# z-OjAw_^I-7r<8N*7xrkZ{&*xN@&=nblY(T~dXK<8@TZ$rI#57U%pi}%3e5h=aF zTkVbr>h0rXnsw*A{=Le0@oEUhM3KX@dRj>)->I-x(L|d|-DB!}kRN?_)x@1Ci2&y} z)S9%o6l@a*CdT^eyS9A;?M6ZRi2OZQqoc$3e&7R~r~0rM*A3-?Yc~DNJ_AIb+VTQR zg%!*3+efdn;zlh-+>JNGZVzlnd{4+0^ZD6n%pa06j=2xABq%Sz zxpTOA+nuGa0oOx@>Wlp?!Vykj805PyLC~}BTQXFhHw{JH` zAZkjtMT!m~1(a6EvzN)BY%w=gEo%Ji(Xfc$_%Odm70ua^}*5BBnI?wLd;&k?5w z!XtA>-{OKwlb>+G80(i;ma2;#PzoiBdNHLybKBFJBPM)xY4}~HQ6-H`>KC7qy9Kk# zvM%%FH#Ko3Q=4Z6j*8K9WYZe-g=;KLu8Y8i!E1l|NOXG(Z86+1yK+R z%z2j9*3csEB|S_`ye%_5j=`-_M+ugl*;VI{lq9?girfK6U)-+iYJAV3MQ!k+7y9}l zEhTHWy-51_>CK#PRw_0~5vYpok>=hT`?q2|I%N~tKji$*?;Ex%I0Pw3V8nWQPbp^A zIG2}LJIiN|&de!#>c=JsD|}?<_I)s7u6j#rqh8>2W7_Rn0%-Bm*{ON9Ou=m}m*){Z zCzCN&nc@2aF72;Ov%gmDe3JJ@wshF@`R&@DFGKl4;>y`ISy5B#u3T5mz0+_Vz?1*s~PdDnP0_vrwv01p^33SBnrr?c%)fu} zLX9=(tG<+-1A3|{;P5I9#^%Ul%6`G75v$cl-Ss8!mT(AumA!e)Ol(I}LZG4SUwJxf zgtq^I59w%2@2`CP4;=#*=g2o_$CnQEeNc^4-cUySr(+u7qvQ>C@h@dN5B%8)u*P5Y zA3zcV-#^{mW=~sBIPE?vTBve=73rqi#@C;h+wpJxY1xom)={#?<@oMD)D}0PwivVx zel#mO1lh$RxXC7`K);A&g_Qv<#Yb#c!IuU9@3u{=pJZfc8l2HvtQYZwECxUe&`m6l zB@OsyD+M5cxXTM4&0^O;*S5z2fBU|B4i?mJFJ2ZOX`1uod({8{;nkbO5lSxsK*(;` z2hs=x=l~Od)|}k7V9rFAMW`&-qt=kryKb)!afx@0_Z({|ZRy$Zor0Zt&f~fR)7W z1MEGyjnqOj#m$z{8)n2AHmv(tkT&hHXCIn8kJBE7k41$Yh%X?te4{48z_L%`A#8qO zx4n@1`u<0OO$4S_&ju)xqP&qLX3bOj>_#$866v9}qNKv^Ufc3xMu{KyN2cnR?1@GR zL_)+_C1Q_A(u-w48WS;t;|*fD0Q3M@7&78CT1^i?{Xxh!>gu-f*U5!75rkK8&whL^ zR$+}HDk(c>FjG{NP2Y{%-=JzWd^e0hrL?(Ap^6f@{mvv)km1=uVpCJU}8rd#oZ~vL9(D^+E*3=Z7j(r6DnJ* zML~c>WF8~;1zaQ>*wgrd3exX=gGB1<#qQn}9njA;OkBHb=_aVeOVcy`LUaC@~Ok*hvf$hK8F-TX5^g#?lZs z1{)g<{XSmb((TroIS|IseGWUrtP1>g%xq1{mgp0W+ney|A!|De}bas&YYj)+S#2APJB zkMPBMEGumc<40=q-BauJd=gD@u6<@*njDiol1on!S5ee(@S##)zpwGlIqCTe*_UTB zb$t!!&L56Yn*XRq>`5eXrxA7}NUk6O3|WsK3!xZnM2Le`W+SmB%G_0UJHEV(;k29F zdw9{Hh@o88wqg0!mNYC+^OJ}q(x^y>GN|3(XX*Jsh_`9k^#TB;AahMExau{X%riZ> zt4z%wQnbcqv&=6H^Lqw-f+S0OCKPA*Nlf9LLT9surNw77s4{D8_zpQ8c6~Qlv*0o^ zVqNC&08UII|LY+W=fq;&y5e5()W$7wG<2*7A^a`BY!PI+b*&CD9Ds2(y7*~nRcxa5 z^DqCKQDf>-{eOr^E*YO~hW!8n(BbK4>%BX^mqM-j90?B%?yP(453XOMEcP{xF~uOz zbKJYwftf%V`j}fvH6m*f|Lri)7)M>Ly8Gyh&_fgPV;JrlhH(Ry5-GeG*4ag7c& z?GexQa}|QHN)?rLTF-PSu+iKqd90VT$+Ss9ciUm7je?qco}b&-cv06gY&2L$q|`gF|5jVfJe$t6=O8Fyd%hF8_V{ZlZIdn3UrlH$U1SEsoG6Di8OZwH+nIyFJ1G+i9g zo?<>h`teUt&vLbY|G0AsG#(8(O0e2%&2?Z`Gf#Ut^ElK$xdHJG3emV&Vd~8PFAg3PlFgb4Isy031Xz&J_5?=_;n&PwGN0?Tn8N zFn+_ta!g-8LCH4WB($2WDv+f{eOiXHa7EUO-H!7X}37j zZlc*>ZD_p-M*Ue^MouJcupQS8p0=n87xPUH=MsjRJlT^Vfa_*$FvbYwAFD{Pw5d)T zp8A~OapzONZ@O<-YEVNERY0huUrbMfQ0MhK_JM7IGg{hj4?IAQ+f@j%q#l^6?vAy( zw)Q^?t)IliTwi!fANf_uKdPVgC^Xud30lKvFlXTa(Ny(sQGbKm8ROK+GXEWtbI{jp zk~qH%2jg9ORjHN$g^Ba7i}Kvr(ZE2}r&+cg)sf|CXZ$X1D~}Lq9fIg3#dG1IYJ=ML zhIKCaV|k)&*4d|O1B@hoIbYNQv%uU^4P5^>IZ#y&^(UsKloT)<1+vkZ z{n^RNF98n@C~TTpZ1rYb>x;yVW93>tVT~K4>N4J(NDlVsiw^PEqb>UEbc;7|<&!LT z?xuisZF7W19a^GK(7$z;iZaoTWYP4;v6VYNQt7ZpSKzP5Ag!0mB%S!wYDX&ND|ncu z+}}=4#;C>5DeMU7KEap}TYgS>bE6uA*9Z`#&%2JFEENyu3l!Spo9eLHbj*kj$EOOW zHmmD5AFS^^#SS8N#Gy!!sVkC>r~-Biu;dz;)e&?7eX%!TCgSf5sAQ`LVD2A-#tby=93Ot^DHNT(#iH9QFD*B;n_ zk@r63Sg!c}w>ub;-dCa8gl+yqOX(uW5A+hMCg!>=#V%css)*I4Xi~&x0i_BHV;P zJ_IR(niL?h+m=5r5isUsv=b79%D3ju&K1Jy@&#-w#`7&TM14J(L--BpEiCm?jIZ@C zlxJ;E*3UYU!5i)qap>)+!`(Ie-;N>kC~RA;+dCg!-(vlfRny@EmlJ?)gAU!Tywb%* z3rR^&AD0j|Uk~qu*<;H}eBYcEuk=Xgc_98M`6$Fb-!r3V1rmt>0;3o;hw0kI=!Unc zmARVju(+ce3pKz`DYcDm2TPSRQt9zM0pN#>0@6uCKI=~F9b`98T!P#_iLlQB4|sl? zfhzkC+h?ZOT(O2!_5c_&9E*gYF3Wb~9@|k}e!^$Qr;Sqh;(D!(N_n?=*Qe}_M$j#h zEZ0jdLVexbC-Bx=6#^lZ`gLv0%j;>kchE(Ts%`bI_E8x@Qzax*uutNEBs{Q?JJGIQ{i=6#M}`rvD1sWA zF^bStKfipJIStrU1+Xc&h?DeULRyEu%zVM{k6^-1s}m=-^ldzuX>ibaz#+4)etW>^ zOCRf)*euC&JK81>q<9b@X{a4JK0(CmWdhj0SKlYmf;Ji)1p7t30V)98Hk+DUv|Rd? zc=SAA9R6Zl=)0hw$?kALw(%Z!bj5J6miBS)Pov)rzH{rO>>0NRrfl5&<{p!X)*eij zTMqvy=VTnj@Pd;2Jcv2)%fmnX@Zb%@oXba%X|H9+Sc`zi_H8@J;g09z7w)p6ISGCs z=op~+zo?7B_>YXiXJ4P&*fEw2^a$^whGMR8Lua3IdFhsBC9)RlT7w@{?5|auZ|xJUZgma3HG%+t$&|fjxIlT{ zg0}-xkMYYB$t?Hj@HeUG^NHsuIspJ~u!s)&HuI>tP;8~crn(HR`p1>A2TTSNG5XAB z)pm!VAY^r_kWjrha(Wc{o^&>dh%q0T6{LB3aoe7E8pADr<1|~hQt3H`!J~`bmUwx4j3F23RvJle-Tb&gG15?*~ zHL6zu9*bDec$oli`0@Ko75y*DTF38?wNH+g$4o1*3d|eO8^faeo4djqmXE}?Bm@ezvD^K3{Vw_46(xfX&yo2lb6(PEB#KA{cuOn$GQslO2UU}sbE5N; z&u&x*Tp`%fsClJqpZ5C3FV&?ZM)ntxagh+}5rn zEoyPf1%7!(%%h6R6E3&#^~6&&Y4THP|K!lLr89lWM|v?kHg1>n!mQ|ip_hmGU{+D@ z!U2;9yl>?mU_PLmkPr~KD|E5Gx%npEuq0Xd)I3H85UQA%5=dk@v3$Mnt$mG0MwtOX z14?N$D}1r75V{C*6vC=S6Z)kt`AcpEjS@$FHu%T8u%DJS%hVI&aBvI9Vlx}Dp zf4IGEG2VlUWWcXoUQ_FD6FDAut7Xi`x_5FWfI91lw#D5SiW;nYd#WNB6GhTFM-wer zo=ji8leaD9NPMTi-_LDo)DO}}-h7pE@7i;6BJWz6eQmg^ywmN=ce-g#vOF306x8Qe z4^oVm3_Ywmu<>E9#7h;;2)$MT^Ztz5vC80`=Rd1TZeU95pzE99YXz~YK4c+!=YS|| zH9v&jl?O2PjH@z~bzSt)>%G}NdMaLG#qEKIjAqlPHe2Aj3sVt*35O~vq;R(v)JU$R z3(p(1l1tA{>-QH0@%JWY6>#S+4;|O(OD|9pP21oR%EJ|&&Xdczm=^LhM;#0?z>^;- zJO^={#|yGPWLlG*mZqLzlS$O7Ao~EOt#K-?U^4_QV7rKITUfb)zcaUE&Zf?tn(}fK z%evnkuHroCaP_z+9Y79wm=82l%R_Is>OxkXai&3K`2}Z{oRRkuY=M>U2UO~W zxi#l?vTa&3`4T$luKjFR$%y3i?=Au|vXwXedU$+m7Ze`IbobYY0rsJvX{}i9Qef6C z!AUkH(HTHWNVJ*0r8WO|)ts{P=*eksp%bf9Ka6-sl|}^C)gLDKud%tid)2zGZ7ILp z3fTxG$l;jyx2#&!6Mo>7&TuS3-Rzs+B2jt*oSR~QFS0HU~3 zK%j_9L^QnWhyKP`Dzi?d;GV~phq3BMJ*UOzI^H}kR`2CQJ9YRbWBl=w%Ms6z7+Jf& zgKiqmniP~0y3P)lTIQZTatO6U*bm-xjl*HzAAnmjfRI=UCy9`%KUX03^lv z@|RgG>Mp>$>B&HQ`)q@*c)1%%P51-eRzl>c$*=#iQcwH3H^)$;*o#fJmxZS#&JH;^ z{45`Wr9V#ZO{J44YeKH0$n}I>#aY@m8)l?_Ona!d2cNm%uq8Nn_W@)fm zN?NpC*sV9j9}h`2?8dR`2KO&49`oSx;^$m?A6y_}#P$c9*#%r##Dl3wcx+A}devY@ z0^Vi(%&YGGLCg%jw?*agfNIfFJXK2?y@O(%Niu2-`Zpm z_#8Tb6laibjZ_bmb3OXN!SaVk&5yr44-Izm8N3@t94i;H{G}n9v%1XX_CIo?^R(_C zh}r8nx)Em2)i)?euvjxRa!bMBafw}#bcxJp&_UaSfB4&9cAuQZ(rwAITS$nK{mvmG zzF&NRxNIpVl^mJ23WKpPW?}l*ICFd%mRMoP`=>e}iHx;sQPKSM&DJo4HKzT_wY(_^ z7CL9bIWL?wJZYZuI*(gZ?s(PD?!hd7?yMdT=-oEtI!OVpC(O;VFS7aM2%vGf!+!x9 z;nvzcw?tAKf)dQ|DR7(Toj;9cSF&0V+j2<=&_-@cr?~IQ$M8<^Q#k10-T{j9u2~q( zeg(VmJ58q_cbs30axD(UHM0zpq$qZ0S;!sau(LGh=(w}6ci`n9O^L#AJe&a}BDe66 zFY8j$`^I3$bwrIX9SQNIyCaWWz!u3iA6<}qm5Io^gdGTgAU&#@Srz%T~H_SKh;EhOkvf zAlwkJZI{GMkJ%4u?-Rzxln8}Fh6pZ0jpQgTd~YPuf53l>9x_{KmRzM-##(=e$fQ}x z_-@+sc*V`dfZO?nI2a68$0Yv4PliWlH8Kv4?0yG2nTV=ME@u-NVOo8o_QMD7Kf%Bn z8nz8wqlp&;%(qXQ0zHVp+(`Ibkek4GOD;naVS27iT*^^gs@rv(5K)?kvLHajFda{g z)vE&VszosRZ5FW+i5;t(Bl=5dpv^jf0^YjpaK$-OAJ!Xj)vZW?Lsvjv zY)huiD@q2(H+nTbrIQ+4gZuO1-2Hf!lV@{XLoh7_!Vc$G`#nXapIId7no?KJwkN%O zD)$c&A6HrAf6?!46E7k@deWub?*-vC>LbY4Kse2+O{X*T0Du3FfZrpV-+*7~suZ%> z?aM&gv_~a z`*HJmqXQ1egaqZ$65=32j-rz8yg5-jSQI5;3&8>*rqs(i3R}5~QL^8%gFqk=7^I~s z6U(iR7B90*gAuNP?K*O8irr9I2hZV>HC&kFK)w4|pJ`=Dk8)#Xbsl$6ZNOH`b@q`6}O4HiV$UpjA2gjY~= z!Zi;`7@LRVAVD9uK9@}0@~Fi4W#n8-WZWGZ6LoP9zVDZLBLAs*hw>jAzTDb)gE68eZYp zbG!UoTdvcEL_|wr@$jLAeVso9`>4t>x4dR%;;9d_grH5cmVwRJ z`*0d7AJv!Q5JNXEj7eRLx@TEKRmKz^UT%#Qn-~*8q>+YTH8Co9l=Br_aVzm&FWW_v z(#XWsXLB{^w_%UFDBb+98Ep3c^g%VPqu)DwLeKU2#K=atM=S<|25`P~%M`T~N7%s$@{96*qpw#qA{{5$oLHmu~z=E0zEf0t-5K&Q7NiiNG`w@&XB z&`@B9i;1vtEkY(SOpBNr+sd0k*BY#@Ar~A;mYu^Fi;*wFUOzDIzeBlLHz%~sMxuOj zl8G4PZ>r(@2{eX8%eHhOA|r`bha>6HT^r_0&9=pR%1(7o^i=_mw%OW;c|uwsn|?t0 zX%qS-$~-sYJe*qiklWfUnk1q+=27lAC}6pFB8p>(-MZB>d(R+e%l8m<=?TC zx3-5BY!{?p^xydNxgO8Q^;D zn+77&GAv_;OcARH8B@C?8cb0rl6khwqQSV7O6IAs63Z;f{CqC$eGk9qcfarR9Pj(b zbG+|59QWbY+N`y%>paiz_cML%$?2!kZ;J_(&a-ckBiEfP3KzdsF{aOSzux!wKK?Ki0y>%n74%9Bey-?rvWZQ7k|8(sf6QBl#a zj4GkPOVNA+TNsy*DCriVna-QJaZL-f$3y$IobvYlV}YZRq8_QWQ4HQSg! zf6!r~$qeP9$=Lebx)pb5qmYx?1-BTtjYdEArF|cvs#vw%rLJr*RbM5aab@?}ObrEP z8>LQ1%GEdQJ=iEVoIH&MzJx^Rp@Lp>)wrJp;9XoeABx4XoTs1qwWDu0RvRicdTaG`rhN$n{=vT!U zagxfYTetf3MOxNb>PDDMx2(N4#OQ*OFWwhCgr;rFHcl&O>Z_$QmGx+CGB;>nF zzchNEksXUQ zi>-w5$Fev1pzdl(#o<(1yJ*nE$b^J_!xOLh=Tz#&JOmed1&9h2nT5?Ylj9$4p_jaqump->4F)clZiCCFg1w4_iLoEIIsJPOPQn zl0)BV6+1b;<}GsFpZAQ_fa{T=B5&H4_X9uM-^M5PE1#mN`W=ZCynRMg^58-89w#Uu zfH;sy;+MbOo4L>HcY?M3X+&7qgR@V@CS9qyKQixp>^;EE!viw@_nW0%_QbSbl>L~U z8n3Zxc6QcthvyENjtd(tMguteY`0R_H+b*q91E!2hK<{V^5T8%xXLS6Bpe}de>n-d z&1JIX$h8nPD`*Ul45EIpI3TvtDgB6dt9+niho$$k^VMI^ohWFH2~5v_EO<`m{-ftA zc9jZld-oaK+8Ck)s^A+GAxHe;#5x;)lhgWeoLz7a{pxwm35EE`;}X*^b7$Ts$*%6% zE|NHXY&w*ZkkARvnOgj$dQ2y;$2u@gTGgnaahF!&fIwqtq$=ASQ; zv7>R^&ke5+Mw9@3qO8O(IGY}jtq3n}*{4r|qCM(I!pnaC%&k6X#_3ynS;F&4o}?61 zgpab-zV`GG*)dL1Dbrpl2|^q`OVO$Orp#j&sTVP|&Y8L4Zn>|4N_9<57s|i9nO}cO^ zDLAFi*GyL5HdM~EVQMoA-$d_C+J!45-jIT;nt;@}wEExe$7f4?yva@0&a`zFO?iA3 z9AYN#y()yt-6qEqjCfQ8em$&K1z*!rf3MDw&m{^B7hgdYhd@pEY^A0s<&(Ri%I87e)+(gk^PPZPZNYd2V9pE@XE`R=rWa)`(%n&9d`}Ndt7r4 zpX>IVL8nwX(PY3!(G`~vH$HRgmS!z7_-7(Mz5oPc|1m$azNP^vY3hq9Arv_*A#^jo zw#%IIFCoJK-U0!`ooJt1ag~i$&ZU}r)>!c3gksCe-DKCABPBS!JuR@R^J|BhSIu}p zY74VuDuSV@ayY{9B&6R!gctqY`J@U#?ec0M-Vo!bSXcDxMMQYuMj z&F!*dt(SI|v|C2nPI3^d2^YOSF~@b?Dk8umLO5}q+_#ka)I z;@}VOP1fVC`ij^HVH3g^R%Y1;GTByIH?haMxI=7-R;4KiDwkWO#_~(FAhv@}B;Tf! zn)e>!rruu5wk*F>Wbzy1y|`zvdKZ&$eRIz1wyj(Dp=y1h)neX!-Ost^le5_0-^TB@ zRW?iie$MuqU&+*b^qmB|w^C=x(QA29doORqZcG|RyTbe2N(wVF1vupwQDy%`)*24Z znRVYTA3+k8ksw(H+N7Koksh16{{G$fTKEG30?IycX)!0s0gdII+w0fljUVtw>onoO z(E1~9ywty~;0k@BW4e9fa#vWLRX>YlecxohPM~g0{l`fPlb8o~kF}9c*1c8y$XQId z#~n!`;kCdpNyxOYq1rrj_dyP z>$*c`_RONz>0-md+daAL+7owZTLMp|M9gNnv`tza(CyL%g&J!sN>+Z|E@iM#TR40kH3`YL-2`KadY z_{Ov*3k4lN5?;TlqhogP3f!2hudBG}`u120wNZ{3vKpjnIr{pRcWqB@Ad*zOuj%6w z<09e<5OHA7zP)cL>+Y6&=~*jgKin>0rM%i8*A*nn|jU?eMO!z*_``CkpDuIWZ^=>1DLGW8h(tL`I(wUq|h^E-v_@pHa?v3YW>Dk zFfGN%OhAyhEx(L7JrP`mjl}fvoxV>LDMR&3wT>n-4t;^Pk2fIYN;k#F(;r<#(Xwue z*5TtEb8413Z&B38^PZHGd+W8br6E^xZipf3tr(BpD`rwgx1*~hzliPgayt_X35jWi zl`V(A=R7g1p?;i@opYG`sSxiNKIT#+?%2Qn$?3GYM8nxt%CVUi`?NE+#itg}9f7cm zb;pHdpF2RZSzC_x-l7*pWx z1-ghtI)DB=-V--B_a1R^iy7^s0epvWSOFHN=AN1&a<-rm%6U_|KgBvn~7@UChBM?`H9FN4 z^Vwc0Xp}bG+OTn)n7s!)#B5g2^3f~fJjz9$g0Wgk@BTYU?stRKYmBI(pY|zRn~vw+ z@au}>0H01MM=l@n8yvLl`X&V*8(>xvF@lCsr!TL%XaD{s8s8RiIx=I87}Mvr$9%FB z5TFz^j#~NshTPQ1jpY`4^w~Cx^#-0MJnu7H$9Qwv<6V!&2ft~PvOq zKTUn8xs7A5(u0j`H`5)Mnr&GWP+}iuhj7aQ5p8P9Gamhp%6#&VHbnE_6M))mbTARa zgDzf7eQdD0n98`#cmDfy$Nc6Bw}?=rm|@sJcd|L$5>W@0M|E%24P3}@PvJ3H{{Y{B#~!ON{|t5<(giZYRGS$3$V zp<t=|zspB#niv1^-ZtdM zjb(Z6&2K7Xr<-b4#{@UUM>E~kIDYNd{J}V3?y>Fw|lENX%(M%%m^)I-+&8A~{>$(f~SAM=Y zS6s&w)n>gN_07F?Rp-hJw=1_c(s+b0KkasZiUaIBP~f}%Xfh9rAJ-t)ZKo5$CY1Da zvEtR;?kfg6iaWGdSIn+0viou^>B2TU9GUyZrGG>ho_Z9OasY`Ist z1SdRZd|FNsxPNPRtQ$H1ky4&q$!p9Pu($spPWfmXq9Jqx?v6Zn#KMghVzG|B<4RMN zr8lv^8fP|`E$%_BiK?ESqB2f>Y3@)^@^zA;${CtF3W`5dtZ96J@@iz8(Z!EY8juzEN@?aFdQgxu*VTkLLrIgT@&Heteu%DTs^~;INdxz9SA$c=G20dnK5l#PKmE-Vz&+t zAEP;5wVJ3?=nuADo(K?=oP4)M!TH^yPp!}G521T(n&2E{6QrnfT{g*!se3ygSjs=&Hjr|S$<94jePYX~dLFg1 zo+gG;^P%4ArtQ}@b>1ILNDv58_&rFWGMF=Hn|tGiv9lI#J(3-J*`hcEZ(oZ(dG{3% zJlvfY7IE<&r)p|yqQN75UChz)3aQ0CV_%T>3`fmA?yVWmTF1tv5n$u3{;-W|t(sh1 z9#+afH_TFUoP%m3s<>`uo&NCBb%DAApUE-B{Bq2R<87Qiw=8T-hOPYQ?)j|`63ixZ zisHE^sB5iu1$FeTBH6AM`SQr{ygjo)$5d>IBEKm6Mf)C#>A@|j4Z0uMWC>5sh^EEg*(LQI+!>;2HE=~J&-%2DCFw7fK5DRkYU^z((x-IdD_HDPTluU zc266RKhX$kWVgQGMqD&XyoVz_*?~5KwMEY4lAW zyRcO$X+K}nTXHM+ZUJC%Rb__pv;G`HN~hX7hHQZ?|fROD9voLlCkEb6jh&G zSLl0ughSR!zEjI$&=?bvYX_HiT56w&S~=g@X>5?PJCi?>!LevpY^|k>*8cUYs~BRO zg)W#(cgOpQ;O*77k}vmUeiB{lXZoK2O3wdnfU@lW3ZP^)Ens9>rR!()zdNg2PHTC0 zW_xjYQvH5mE4S8QCj7?the~S~`+lfDo7w%-IDCKT8U3$jnPan_dMe6G(`zDSFY+nL z$tiihOguPPpPE_Wo7QlAl+r^jpMRbwp^%LT0gD2^Wl5wZk1`AxBbhJ$h4)<%C zQkb!Ofe{DxftN9ak3>Xo=}YXsf3zCHLN+)uGa#k)@sLqh$8L}FKh!_poe!xzz@_s4C7@H^rFt|Xw5~|4FIMH9 z`yNr*15EbU&Zp^p?(f~72PY)tq84uz6C88Z9Gu*^{lZz!Mp4JAI;(;ej$h3Y`tRVi zRK#XOL5B$imm_FQboKPBclN%k_)a>4sITl`D>F%6J1|)j_kODB9MII{NL-H<%>yRc z7lHXpq%p|9X;YTk&RkdtFs^Fp;M(Nmc6w)7&3CWF2d?e~<3U|AMs553YNr%hs@vD+ ze)xFEvcb`Z=+48!J~CVtVrx;_h2#m}F;0IdTE67Km76#J{PS|HX4TA$6ktzu(hjml zz@7x|AH8_-;)n71PG7NMy5Zcui_%>qP9IorqnO~6an%laOa}4YUUYE>6ek}nvOXV* z51HAd8B&YY7o-q6Sc{&?bYJ|{#lfM}J~}9VFBBvA!h(4#o-d$5E=5uT0>axzc<+`A zJ9@>LIml{UtDD8-(K}5|I&E33tgKG3$*hWC84#Rs_2bGm-w|+3D5y$!$s>jpP5-(= z^+n{#5AA<5WmyoU)p@nrN;=c}pnnVXjIy#okagY;%7c_oHcprpg!2A*#|J<^i7(uP ze!8o0E_>)eY+A;_Wvg^TPR_5=fS%|_&-2gvR~S|6B+$#KgP2Ya zFN%qkmBMlh$UGRays68eTelF=Vo$ETb<4s*$I6*D7}|37`TmRt!;dOfGqv1|=)(9< z>%goKgOy#!>$Kzxs#D~VGK%)_B?g43@thvVDI=o|-}Q&)^3PnqAYJ&G+LnVP`t`8# z6iNO2`#9Tl#$Qibx2{fP`M>;wPT!%%kITgesq0ymkTM!FwJ8VKwhM*p%u=TGxtr+u z$Ns`}KgsQ0mU^i0^HI9;*yp;BekeY329-6Msf?{N=0tQC)^G(a9Igw*?T3$7}f>0fkXn1(Pbgz z6(tBn@HJ*;fN8rEEu2z67CrSuT65X*)D>Z*K-B{q_4lqd(>!$l6h?}D`xBoCIUxBn z*OHYQ!gVQ^5JQHJwGp{SDBMU}Q0Um(ufsv0Lax)k-lqZ24v@1u-d;P{a?hMS`{>EZs%Cp^(0CmZ@r-} zItD43+TH_%s|y-29_h`X)Y3fW-JSV-hKj+VcRl;`xq7imnq494uD`+5$bd6DpsMu? z;K0jX_FJ~Px*F#)#J6JwLo^iPB}_nTpk7KUDsH8miNOX=XuqIuFH?1hQ4Rhrk9E(w zT&!{|_v45tz}o@)VL$pDNNe}9T~vqmu90ugE#)BWM9YcaQ=u!193fcMkQ&rirExs3 z-21=ctVswt-e(tdW(U@gpZ0Fe19n_~C!o@bP89B(zUbmYIc$>|KR%aOIzxE7ff!#B}dVX<%Ad2yA1XMl37 zS-AUDZ|oZNgArhERO>5E_wDiy>@)@;ZER#zqLSds*vyW-;Ha&~rzqum7x%htKhm9i z9R9GC_Ur2Kn0M2ktW&XdOYd8NQYBe7{|%)EE}BP^2Y?#zy2-h7iz?ja7jV?Cs?1s7 zwL!vsZ1j@2Htr&<56!(!@=CqPd;)$BOaa2HSCEb%i=ta!ek8gd9QR)Cm?PLpyV>V3 z2CTqB(fj;p(U2QE$qw^RTmfj4JOT6qJKhb%EStk;0a@L5Y6Bo!72J$u+A{A=xLW~m zLvWLFdQn0>2c0`td8S=wYhzN z=k~<_HI?NBr3d8BcaDhPvmU4T)u-Tcoh1cN$5VG&ul^MsUF;OPO^?xmvBkon8S{Mmk?Cki53|FIq7YBe%1Cj zAdg|vN)fSm``~c#+!Pb*QS4!wzNA?9VnyKo z<~0$ek1oCl%~4V*a|{g)#Wtm_tzGi@^OF&iMM@v7y3&OoxSxzxz0h8$lT%D=43RR) z(2`u0TF(p%wx_%tbo6>VsiRYVJn|f)&{Xe4VtJ$y;~`JdZeWY9=goAZDt=^PECwgS z&2V?&@*nbl_uJ~_fT30rq_Il-l<@6q0AjZAv4XN9B_ssUNpZ0 zF7Bx54oDo4hnSgpskFOSGskzmJEMwAqWarJ@x1iCX{r5~W#~NJ!k|LWSFXv?WtEn% zuj$=3_8>T@t46ed-gK%ba{*o?o%mOHu|=0mp41@}%#XA69Qz!i`)^WT@b?S$6zZP- zg)_Q>_18Fk!CBtH=3w9Bs63{Wq@*uw?p@o=)`=WdD2fIuZf4vD_zzi4q=ppzKcLk{ zga`?iOfq@w!E@!aXX*SOeB>7Q)I^rktQ#)Z;=1P<3G*e=bp~e?%*Op`P5Nd7=5M;} z@qv=FOK_z90l1cAgaRSE?8D$qV0VNJHP>-qBid3^o`YR&=Jr}g0L+7g&P7giciU)u z{4O9T1jnO7O=c5v=%Mtp4!rm+S){t}_773_1XEK+%@J=| zFp(TS71U;}?H}zHpt5?9U7S9P|J!5JO8(Y=R%Xw|KPEwn7 z46vYb2?&ul@Y{*e1qe|jBubEKc#E-kOJvD&L522xGGo;UjY2wvK*$(q6dcGuVLCXV z&Wuk;Xeq}Ih@WKG0@s6i44|Z-NkL#(OLgOsMR|<6!HXYkPG=p6zsb6%pe05vYtb{) zeW5(LLOq418dW3aS`zf8TRoY-u)a?q9v$YbBai1}mT+6HLWX)O?>X7P=~<(b5&0P%r47w5y;;2U zcmxLWfdJ9no)u}|_-cFzm3{qWX{mhI7IoC1D5YSj<1vEvFvK>WN(ep6j%}C3cq;p~ z8swlhA$}0?UEiMLl{$C{XKMO09*5jfkeKTiGMBJ+kkMTel^C)m;>0SFR;odrpRSEx z8o%cB^i$1+cUh-N`&^go#Cs{CJPD+9fRi(b?+i z)f_%@?TW3T^=QVnJ7xB zAN@L$?liHa7ZXK@1eTfZ-|u{Et3i~*YfwaMfS2dljk^@BhmuL)eo4AXIYIH`AZ}=} z63q6ac_jxe4xtF#4L)S3l({!z$z6U~+s8`<$Nu%2?!TXCzrE~p9VXX9WCaGe-|=ri zZ{3gAR<5#1WzG$EDtvZayMr^hP6O}xqktW==t8Af@P3bjig|TAM4M5lqFI5ebK_ij zBl@)b{Cpy07xhg;`M+awnaTBITQ4DManXUla6itK20GjQdGFWClgSW7JX4ELkm^7B zDc*QxSn*i+(0Uftm7MZAmwHO52R=^rvwQqpV)VP^4?x$9tHAHtYL>1u)d7CDb>Jo? ze%v`{{RgWYm7KG<3l$P5!?)vz0l%1on{T zAN?D;ck;@W9Q)A>;Cli`&A$@kV^&YTQ22eZG3~|AL>7g?)0P4s#)xS~S(~*w3pH4S zwBMZ&kJPa_7}q9xzO8Ywm#3@8L^&X1n2oE+=%gI491 zkpD|zD+R1()OrDJaiD1Lo!T~x@j71_D-*tN(jN%82^JK|YTbp`UK+K}$Q?I<`v46C zHrj8&V(V9jWk;>NaU6m$XXbCKZkU#^k%$rZ`VueusKJuOF3%H$9#n2K%8Z_><43w@iBY`5270WH zHHx5tBDX@BA??BA)R?|ZR#LF50BJu zcH7{0!goinf#n9@ARhh1RSs|WGO0(9zX79mi>&bmR~f^Xmi;Q6@;jK}VPQh+3s){( zyng+95(zOVrwq<;C7P(KtB?MqbG*yk0ZWZ>cZ}w z3fh`Ub~2f2hC)vG-FdonM``pyI{3cS}m9Q1=&V3hxw*S_xBn5WD9!q zqmA~&v68H~gtl5RTNG-E4tEub&^lsIBWB_%$_U3L2n{7z-FSUYF8aeYvpbVJP+zLW z2lkqSzTFmZx-B54T3hFxLUn385CQ}SqkPgg*apolq0oEy>00*m-Mx0vF#}f71I8R| zH}@Vq7^9nmcaH8YRJ~k~+qaimbF_BVIXD9k+r;3!jD{n5#7Qrw=s#m#NBx7kp6X~z zizRcTc=H+4NoGRV^6Y|rEePOK+cJy~SAqeAx*}{dD^Mn9y1q5Y0!N)T53A4)cFnI^ z@neH%0?(V9JB(pvLV2;QKp&;Lir}^-TD_OUynyi}Y|s@+wOgW{pCA@I%b6=d9zD5^ zq0`aj*(EcZ7CgXav{k4b;<+$ii#H@$9MX~VCF&2SDz6nNy@Pz})^7z*i2?k5VqUt%qLb0LM$_EasnXIITn7Q!d1EBe=fu<( zLd`+J7Tf;?klNELg}#oa13(JDN#HMlX=6x{tvCMkugAfX|0Px6`oh0te*#hY0EvD< zTmKccLc{&{uXXT!iI4KvBSCjH@d6ees%^%@|3P^8|I{cNJ;~-hhS)T58XuTZ{;b2@lgyAb=kz5NM)?OGj3$;BA4MlHtnm zy=_HQGt}!}{-GSq|6*NgmC}J6X+W0m8yd{{$XXlCB(!|#j0Zyt62y~rTFfaO<|J`< zsb0d(MZARv`iJ+0q7q%YQ0NI_x!Vcc^lE)CKx_zkwx#D@|oiJr}ivZX+e>y z(XLJ^9l56*3otU7cPA))Vj2-TSQa+Ed#`C{RrU*IiQF+mmxn_YRm)|1L0X#U;dZAb zV`F1rKsKG!%=UGfI{$wQ{+yXONt-alg^BqJ*+!#8WRPx6KMd%v=VqrJrU%9w3{PG~ zdS&byN_+#*R6-H_L_*ej;w|z5iL~jqw5=!oD;%;YHN?lR$qN>tR+UqJxkrj#o-v>P z!3F))Qq;xy&+2bSJ&BH90@Or)tgIDX6b8C{Q0iK+!kUEXxgkayv zA-(D(OqJMLeicew@$Ropz+p*0Q|HHvH=Ybps&2?F1j+;%L|n?dkgI$;O`^w9xvoA0 z!|`QDN5H8^-<1zIGAWm$%BEBALmm`_QX6&&6k_n*br0m&iR*nYG^QCmTK4nJ-c=;{ z&q>q{-fKE`w#oJTu|rd?LJQtUIoPKcL*dk!eT)SSL+heE$=RV7)1oQ*1%JYC?EK+( z%5n=I$!=U;e)peGFN6Zc9wuI7uo;GPI$WvZl7C6)N9Hfu*pzy+%iIFMf!THNA7SQg zLsseH3rU7~St8Z2m-$7HS>%|b&Or);CjzjZ(r8?`Wjt$puO}n`ru`U1$gX*;{wKsH zaY-WGGNf(1wmt-PL{7U4E6|==+SH!xw2#@H55vPtp%Uf0si*h=ro)o=F0b!o(|mQ9 z93nQGpjvIrcSua?Xm(asQmU-K5hU)f&?XwiAOq_B^7^KIjr2rCy)4dQLTU{+>Cno-Z}1i7C07K=d$C6W&E2L?+5u!X1#y6OKJ0P zWYV&A6SZwg#8}LCk0rdpnK(;rl+9^%0oNv{Vcmb|)U%P=$T`k0cG?qDy=OhF9#`+P z@%6$*Qbd2*?lz%6`9ftP1dw!v_=*LDomBE%>Cv6&Z40T-L=XZYMgFoNe`*lSYN<_% zd4pmU-L}y~?mXXvBqEs;#TB~86iH=iLG#(AAGlPQ%+zAYo`B!3d3bq~#sU519q!XM z5``bDWiTfa8kx#1C%5}=O>8OkuWR)1d&1GO%q*ft0qesCD%Xl-Pws%m(bm&Dz_z$5 zuEFEyYkm?5VC2qwtQ0pe06#HwmKaR4$aCiEcpq6u*;?e+nt7Cf$&Oq#onJM2|3U{S zE)a+_uG;wDv$C?W2^#X#|3vzX4S2Mu?r0TQkaK)XyG5z!yl@QL!IZk;&Tm(K@`gHI zeOFz*<^HMeB01KZ1%vsD>Ku@8dL<-4010LRJwypW73?iIFV9?(siR0R%^NWGsCycQ zY|0B+r{O2A7ah;(?`qy6_j{d`zmVm@ABI~6%?mX8Ts0M>uMW@}(84LYZ{S+R$z_~l z;QQE5z~Z^!DzG;v`P~9T=ywkl78?k znT)=HR08Q7!%p=k+S@a_0c**+$u;I!N$Bo{Cux)v!(!16Uv3?(6up;(YHzK|jJ~$^ zJ)%QdcQ7hpS?aC6Cd?FA(Jfld>L8Y|ZJ=!q0hr9PMI~GBN(prM8Y%38Xq9JYbHOZ@ zG~X}02Kjo#n1o-RNm;Q-)vX_GYlt(!GrB2JMeB+7ZX($o&)87S!Sf0SL1OdbL=L8T zId&+LMLwHV!Rq?gr59=b(F(fBvsy#_wjn1ZBYlEZVls47B?%h`LqUyQ(Sj10BVE(V*9wT;2`N)vA%X(zVZgb-VUeb?io9 z#$$+rW)--6OS?-)YR}4w6s$fE5{OTprkUIo_fgY&-jbZ@9=6NO-4RNZcjmM$`C##E zYaOhut@pDn7E(#Yu8kf3UZp9fOwUNIdw<-EN^Dnt*NM^xWn+eKAd*et0x=u4y}!aK z#&<87%(yW}G~NJ7bPjQ@!f-$4bFMKEgaGG)#y-lNPEd(5+^VX)Gzcy9V5gkkmTce# z+;TWs;aveSAuklPn4m6bMyy+fnOZBO%j@%O|3rpPIRH7DfqTm40Yj(?F;=Hg2q94n8Z|vM=D& zHkgzzzXst&9PY2L|7!H%rl~krX2z9-BT&wO*CguPIY&osz|{HYga>3Z-Euzukn^$; zYSd9KsM3+moCS8%?LIN-nr~0!?E~z*{2Fkjr7b@;Njvcie0ujD_9#q0c|W7#)m2gD z82a#`XUjmnjrp$){ra!ZAx%OSyWZer(qotNTImjtKthAlFv^&(N^T(CJF|l^BG|Y6 zO!iJ8m4&Lv$`7;V;}^PpRsBxe-xqvy+-|R$|7w@b_J^)ki`Ovl-}1L(E~Vf%SoAta za(i#AT-AE;Y>e8eZ5)JS((>MRqPZ78cIgV@2hiN_)vQm|!x9&F{h8MLLgnD;AGF&8 zvfySSrXZ()m+p+LV*ilE0jqqaWs*4I>&6qWPfku2x~TWE&QKer{WV`(4HQ%A&Ne=d ztQ$~^AJY+m6#0yjQg86Vg_BrMX4CsSZ7w2|@R!5gIs?-%hN8C z4R=rPvb&VfeOI6=^F{fha<{2-A}yOrPG}_mLxWLw_^L?ip~WJSAxm^R%`o3lhmk8* zY;Z2xUg%dND`2%Fm|~E8wQNzjblFpShT-BIcYOjKI5+#(69l#@{a;%kQT5!gWKscC~m|eH<*pC=J{8{J$yo0Q4VOBW|Gt;uERc zpO%e+87FGSfR7tviW>R&`OQg*1d-*OqWG`2xTIgUxSMpw;sYHXQd&vU=NAbEt}i@3 zc-0IuN}QlZVD#Yh!F|*93>zHzC6q2St2zmJ)#1Y)V~0T?ANGHMVgvptKX3XWAbTXI zp`wXB5)zht`iIeT-Y+0AdwUmArw5R_17X=J(&xG9q10(c>bA^ap15Pf395aH(Ywv_ z+{Y6QD?MFzO~25|rc)4zZ?^X7C(q;Qo*uLp^Bd)p@7|=;oYgf{SYu#m$z=2HLir@@ z+PvjpOi!eT!SWOw%y6NhaYYe&Um=Es0V1q$WvgVk!d{H8+fnOJ)&-r=Q6}xvj!qB) z6@l02g9D=a?c2KU<4+=_;Ef-hef8@22Th)!PlDr+qGak3Vhhm~s#Sa6?`|s$K1qeILiPbPOIwt6Ze;C#^tLpp@;*G{CBA67oD zWJINMfYAn8r{r-a$${{gb=`)?Y+xuh!ev#(vI3%c^ex4&h9gSw;^$kIx2gd(ZfSVX z#vI>BU=(CNqRp0L4L`0gjHC#b?m-!KH-Z;%^}uYnw%qfWG*TwYC#Tf?;Us_1(An@K zF}&tw%<(GE;uS|NiZETCL~zB$9T34TUEfDA1`7@`jWuMtGr7Og{=BVJ@zONCi1WL& zZUr6DIxTQf`rOkW z`+M!n@{`T7m4li^9e+fU%N#PPt?L&0_%+`N2?#|Ose|7E4 z#)7g?ZTPZdb{AeMtXCDiZMKJj!K{g?&pr{~;CQf#iUMMud@ZQU5#9W#G_^bu<}Gvc zoyrkXd7b+JwS0XGF7F&HA<+FC9(Ic1Tg< ze5>CB=n2`88+PvU&5btf+hjAK?goNJ=I7@ndIgSaz^IDbeI}uV)gZrB3_Bt%E#e`! zlVjy$S&z*`ea1^g(s ziOl>?@WW7=-K(7UyAoNd-wU

znLd5myKAU!li7e{Kh{5TsRqRML*bm4!2{XodSS zD25wja5tGtXj!c}7Zc>2O?!}D_vZ$pxR?{Z8Q!I-%Bi1|51*UV&(GdOlZF=z^bH^R z7$xD>!0(i-LM$;tI{|wn!%>75qfYa2K^KHl=}z<*_HrGj+USf}GpD&}@5T9uTQYI=VerXcc z-(No~>>EnbKc|?f ze%@!$f$C4p6J>kH5R96KQy%m`X5sxJE;KRy(x1vai1)d5f!SRL37Ip`UNnh}_PB!s z;pEKnHbM&Um%f0N1eW<21~P3~8)uQTESmE6qOC0(X05+K)yRivP=_vjzF#$Sqi3)a z^RiTCbh70-wuCJ^(6P{#Q+Sxxr?=Kw7vPi|5Hz1BE9@#yzK;Qc9YZ%eh8m<>&*5&% z@6AHrZ^EZRy%%TTlVb1+-Q%_n!y*?lTowmk*=IH5_U~JG%>ZEq%Q2%8YxL+K+Xx1` z-3j=lhYV>~*}uIzSDw?H&ye6+T$+#oj|*T^V!7?#zyD!G#EESj@r|2|p4N{g2C7A< z#Qlpt;6=M=QlIL7d8s1Qo^i|y2KD>4%7WG}n0G)-65M3V7mOu6 z6PrvsA?~l)HT~3}k5=KM8Fy6xAM|#zh!YLU`a=;GyCzGiUsKQT%4wGSOeE<1^51;% zxbt?wf5VC+5^_SS+rPKBdt0oeWv!I`)N1f4Hte%E;E}kgK}M@{Tqr2P_olr@)h|2F z$9@=R=@SA!x8_H#6sX8(ekr{9smJR&cwfRU=IDJRmiav$A)ZhAb zlL`x^&s%*Vj5Af`pPkbs1S0M9A^_9nA^`Rd+v6-6M9~UE=@&g{*$yh_qbRW zkv_d5hr`Gepv7CgOw=8L=RRoemlQ~M4G1S97=UnWtUPDEAy+8Ix4gCWv8!p`kNcgD z;0DoMLv%|&2jua+l8HtYmD||def%3@y+ej*m3}uC`rw8gTWwFod{+Tf_8^NtAE zo+vKw@Fo&688`iQK8f!nLx|xN*FEvauCYY9qI%KHQ->~+k%f~yi%7Kv(~E{Z6Is^& zL6M1%GY9Pv&LMtpmP31rrAxc0f5@|6Hu$eMN&t45Q08{Rvz9y2e$}frI@x3FQ3p{4 zp2W>PNJDDd%-lrrVfX7cvOiz44-Z`Q;KdWg+=spSi4Z%eg%PHXRaGuKTPxf9bpnbn zV+?Y@*dA8*nJ#`4;`=Pi06iHTguoOS3{SB6PMtnGhl%{U92t*sEsqThF`LbWtSIE9-cojA^3fM zn19ZD_Z65}TQOgV&`k|r@tC+m@{FAO9*f_KSfI}b7zm}wADD;qz!+yF@uW2OSr>^J zu#69Q(t?a=FAgXaWz*kL-JyfesG~!plWyO2q7`kl6T;((+(;4KY*OC!FY~j-^Al0@ zNlZ{Q!u#`lp2B4U48P5TVL@|hMG!3UDu{R8YH&O1Gw#sWC@zLks8yN_@z0O&J1}?p z*8&;r)4@Mq8bOk@|1=vROh6^`7s zn25v{hLj;p1-j!go;t_1+5IObNL_RvdsOI<^SYl;tMAt#qv_95(7{dt015MIOZ;&Ug!g&(g5>hnupX`>WWmC=?jw z%tzkVD2kpi)ur`4?ifO#^C7lFTrgHNkIO$Yq*tT3$K(j2qqq1_5Dj@N$gcczF?xc~ ztA{>ms34>ovISYw`;b}Vmj79SuATG?ZP?X#usn(03oZ5;sW~c_2#qKIsULO&aJ=V4J@2lVDRaQ-|4$F@f_qNfs9MoHmHjb`sQuJvV4F>Cfepf8 zUB5k&3*r!)HNW9_Myyis-mi}Up%YO(kwMC_5T`qw4c=#BCLz4H*d=zMyIBhx%xQa2 zT~eFHT+V%5o%D(4X*-jq&)D4qB^SZu)3ZS!F0@k}aiubBQ$V-OuTM*ITJR zqMda={t^1aY4eo467(7V9?28sAryOD1((>^*o=|%ljDZ=58Wm@z_A~e$w8O+g6MrC zPl6{G6ekG{z-yd)!cZ|mfet}vTNa`lAhm%UUpaO|E@W6?>2;&44tvHVP8m0yP4APq z6PKPmWM5(*>8?$8WJz1cbNn-z-HoqRM(*M`}<%1GsT|~m{t_C#;%pIhiy5@G}Syk%Ysiq<4EQd?R&xQj1UL! z1@;q=h!4CnHcl_Sg2TVQOpLjk`SWYfb`MU~&Glk`;VJu!k!bYSBS9BfPMwMHLx-!I zv^z}Nt#(dtzc(9M@^L<-G0cV z(_@+1DUDO1c_TBUR34#_*~^n=hf-bC1_)IUF;)v=e~6+LTN)->wPA-xby)k`1We~GC@ z_lh{q;Jae|l|eP?ovmW|Q|KwhL0OfPC~nX*{rr(r?6-#ZP=!5VJHoz;9F&n9xFKz^X&Zhvm+gIEglHC;`PgT{OgKkWhYnKdgsi^_&!q4F@$Ap@!{a|WMm)Z z$BueSklv3LF}^gN4Y{;?QOS{jlDlbhku*e9b%?p$Y8jiJ-kp($3>jq>%3uRmroQr? zl*lIf9ot8RSFS$F>R)u|dunBZ%6nn}88L;xGa&&@D&crR2rKws5@A1Dzj~uWa1E;I z0mna7IlJQWKMK-`?hp^_mase8rm0{bblKjwx`_Ecx_I8c%Um&Mi8iAD@=U*ASG_+Q zNq^1P@!@QAU55GO&_cGGVouKlf9B6FL$NOli8*7o$`iV!k`f0f=}YSPEWVA|dCODaP!|OH3<&qaFCy#kR6gTAq5w?FC=qqTm90 zsHMHw{9E_MyTj=Y1(#C!-dDO?Zr+_gc|O~Sw8|_n@nMaZtbZF@AFcnOavD+aKnz9T zOoF76yh}Zvdd$C-FcfL)edh}LxwT9;-hZ!Wfmk{|ia*20p7&&M!9?2~Wc>`x)cn?! zY|AdmP|%UxUs|uz_`V~fbz?sQX)0X9>M|$}q6o}ItNV*(-{YVCW@v(E^@GrnN!PMt zBu_NxMOP=PJj399yxhxyj@AdMGV6D;kg)8f{z31DoKX-17?yV-NLyKWZ0oNDCL}+W zg*?Yr_1WqyDc1t}lP&F%jupp_DAMEazt5=kS|cW|Fz9HWqerW7XwQ9oCc^XPW|tKe z($01?AcJt*1EueyT*LREkG~+%1n&L(eHF{1G0BH5vW6w6C7kEf;(zw^zcv-sOBM;i zS{XO)pB8>5X<+5mCG)vnk}i=!GMY6!LU@kP=1+GSKFT`PjF<{N(Ve>?MfoV1Qy>_Y zp{iWqPTbD@7QA5kYdJf#nmoL!|w0kYqtCt7Qv2)$#&}NKh%>1x?-iys6N6KfIc@4~+;_ zApfs7a6k09rE}d!N6FYawW_sCn;a$CHGg!qc!!8$u0fmn6L~dJ^T0!mwb;+sW)a@q z=F93G=X3Pl<@1Z2wRTNrYPWIgS<55n-E|&WMZINpVHWx~^w#cz76uL>baFU&oF*9| zjLT*9S_1b1VM_CnGnE)feBkwrQgip$EgSYaY_)+zwph*%j?{5!7`Jub?O#Sx9O{il z8QpVD=J0os$Y5@-{*XM&X4a*D${5-ve=qk4VLjo~-6I*&qqAqNSq>jLsDTMZ3Clfe zX$`>;jby2*Z_%T((8bvDk^Z4)r~54XXTRwgC75q16c45I!aWrAHEoBtR$1_A=8q$$ zpZmUgR1I-A;EnGInpNZfPRVKbJ#xjjpZ+PQBKjE&qfdU3B3QS1MxRdXW_f!%O?~jI z1~&-us;hMgx@{E3xQjhfFSmp+`(WZKJ8hMMDYGvkLqm@V+^@Lm!+rKjn?uO+ZF>-u zPbOnN6zpz<&W*`^w#Jw-}nr#qZdMZw+4e zpT5_?b#`g?n+`5FwpQ2uzK@J$cte&L3uox2EauA_XIa(LN@4y>CwMA*o0SI048l)_ z-R(i;Gd=zg`Z6fo!TNClHPx<$A?IH&%Ld&Yvm>b?!*AbkWQ6#si}t+YNY1#OvGGim z!{Mc*a^-#R7GdV0Z!r`2nB@&pHe3awLmxz{&w9}t$LK6S9 zDfu&{C9fnbxeiyn6cr`3EM9t1Bb4PS#N@}WCsLsPfHaa&B6?+9I`yBLIDnpjro|ur zbq{{ZmmJL9YRsoykzbCSy)Ny+KPTAB(i0OI$5<%T({&--=%+Tu?#UTEY8yVslJ@4CMzc$aC=f7;!_8VK${ApGS)&4q4id+e^7jud6 z6o+^JY~2=-;AsB&_M*Et+Dv}HZ21j;uHhXJbKbX}e!uVcob$iVb)7Cx&qbJfKKJK-f8NV$p+WtI7EodV zjxa9p0Q=GB1Deu|{sFHqi+H_XJ7u2qrq}=eP;S;LLfciibHTUBA<+ z*(<)K|FX8^I#!ZMJ?oIbZ~)aOGv2V?uw+AB92R^aXA1fjc^jYGzV8jiIUuX0dT)%( z^RmfxTkg!axMA{xaBd;23`o3!X5gMgXuI#O{yC7^S!iYbcR&f~K_H;@(D$fjXafqM zpPrxR_x6}GuB9*^X*}-qgXRlh_jc~?`KP8!N{^M5ikOA* z9hM80_huGK3{|bH%H_E>>_&Di-f!1SP`2=OI~VMNHNrj0Ek!^C+l7KS3&LC3RSFJk zP%6C>L5L~zKsfx5S^+AEVA{w$0UMr)Jt&u&MOtV(!iSmnocNgSpF_=F{yzqQQ?4hU zd@O+QcyYdm!C_z?C2{_NL!KoP(f>D!BUoz}W^03jU}lYFsOj zzx~fmwVk_|;Pbtx0FBWrnq6-b!}iml{NTT%@YM${u9iYhR+e_=LUvB(Hdf;2?>gI{ zP<~Y3z?&M^>G!kkhi{@+QNE^)LhUL=p=h%A(!y7~r*`?m|7hH_ugjwfx;UoblU+7f z)vuyZMPUah=DXqZeRq|O+)${4jmUpAEiPGroTO%`T)V3K&}??o_bz)&G9RU0MI~a_ z;QCQbjsYdh*7VyK-##pI48G{?WJ|K7pMKRjzxzp2Ti`Mm33KfIjb$18J6Edo_LI(_ z9Sj2>TF1POd2sXtw`QqCtMMg$&c_3JC*FNs<4TJ1QBz|ME&bVa^%UQwn4|6NYXRI# z8+$(G$A3UI(b3j_#OKR^dKU0<+#T;H|g_JqmeU9i7t!r=AF) zB!^a6(|uRfQ4J>e&E2wVqywY8O{g|ac~u+eGlDw0bGZp!jx9XS@bshnV2TN6MsVVx zooD27*`rTzJqgG32Jwd&uk5C>b{{Tc>(| z%qQ&KA|dwk7cvc6mP#tW`+c%3Fwr%m3$NW`U{Bm!%j^D@v@R0TY>~je2AK+c09^lT zl{>eJ`~}u0_({gc6;QjUXplg)p-G*7AMIm`%du!dIkjvEWX?NF?Cr-xwYkS$#w@wB{a`lRJuIET#`&Ax0Tn!4 z(IZd2S1cA?_o03dv0q>F@<_WP8{E70Y`QR}Uohox-q&BvHVXp=OYAe+KW(JKgjg0? ziv^oqXV8>g-Sr#1F+S_}JJ*}rGm=7s;VofC8HtVsJO(~zOEJ$2Z$@$h7wL|pBOTlX z6u@X{jbd_N(Ew2EYNU35;0fwli^W|dAJD~CbaN907D(6AeAP%nIT=n(rJal>h!xiI z%8I_CPy9kLa-R;JI#;1JlXl(jCoUsKb1RY6SU$H9YwBJieX`n^a9{ks$1@MOYiqr} zhuPl5>?~bPXDeg=o@tC$AscbSO83UdL>PxGbLKJSI&U68HC&(69DC31%SUUFp_yOf z?`u|OpT?W_MKuC9ez{K71Mb|&*F?aGVV2$%rc#K*KfcNN-<`+F-`pa*G0I0w5^3bo=+`I?J50Qyv2l8#)p2W(VdBe7W<{^=6D8 zAdEtFgcL|IR>BoOJAP(%S}TAZGsxlsq?*-HHDg?@^kXIkz$K9C+%_^gs#cmFbx1x( z?_H>I^r$LRLqNl}WW>tg1;l1Hwl*!PgO>PG7h2^MYBP_m~qMcjg z?pwVKkKD&TENt~(6|S&VjmC{j*H!-X#xyrP#h7i~d67*x z2H1m`w?c%y1}3-xli+YyW4FP6KX(N`5z-Uh5b?cl#?y!{zu$y))khrOTJ-n*Jd4(t z#MUwB`1G55{UY+H8)Pq)n6^m9V#4tD{XWj@Czd=HW2xmMm(O}&Cm3;tCtSxTO-UKO zOHy_6V&dX7Q?&Uv&or^~0-5R)IClU+6gs<>bBkKeh|o0+3TB%Yr|^7kJYwL+07UIu z)%O>b-mAvAU^Q;su#ExbVJKJp0zi3Kr$pv#D5f2(FZaH~*!Sj$l|D#k%lkb-FuRSL z-Vr0WTY!4_rZ@iuH{M#O$JPe&Z4*^^r?JM;^G&!Tj{~p1z>SwUlAI_dRf_=~E&v8U zWI%(0CK!L&)%Gd(C8bJb?o+gkkN0=tTU%9zdR*qdmRejmTP%;f;K%nmkPKLZ1g#dh z;*1X1;EpzGAwjs!{+^rt)|r#y=G{WGjL z_RLWUhWIm|Gpn>28)sCu;wvU{6jy1~>eGxDxh@ddf3&ImSPCO~dkWgAs9%wDJF;dQ zrt$5>j#xA;b9G5(zcN-t+(AcyjpaK{@3$Amk{g_N$62yJh}XUqQ8V^aRKMbM%ifl| zd!s9bM%Gi>L%`m8NwM?SVv$JwWtt9Nzve-`mNkL9FnN}a8>heD8HWznm@irQE!PZ z^iB`f1>rYOP?M>?@au3jRDNz>>H*3Obt^HiR<`LP;q`g}qrpAL?j<)Wll)xw(S&6m z2tSKM%haX?--zMg*%sq`st+;9u9Kj6PFG(gPu90eI~LZBvpPzC^KUN#GGgv{sh!m!xPj`D==6oWbomyKgV6*(t+_P!vh9G!m zOX35UT6-!-`^Hu`kdd^kuK|#EBTirc^ySOPo=DAaZsVEKP6JzJ1vNBCJJxZa*gsQG ziz!LQn;C+q)Nr>AFZ%W=Ss*z+-cy*u2b~+hg!5^pG{Mj`xuiT9y8AE$K^km7meeWRK@XFqI23))Vx-Kb}1jQQy*#~E0 zl}KnZFM<(-jUa<4j7sNgSIv6+`-w~K`YUe9xo)>kL!^5W{-T)FTeTG2TQ4p-Rloa< z^&Ei|i)L0(%T=Om)?OwYaA@`26+_M6`h8Vmk<#I!Lp`tI(Wl%w?7>Y?bI{r(%;n!K zVK!Vb%vlT(|Axx`(Ab=!Yp5O*@Vvb)(f&zpt-|14of6)S^cCx+=KZA(+I9qvG4+9A z3p)ukP_LhR7hU>3qhb4L$6KK@Z@NJMc2{C2ZDQwBK*Ji+^%+)qou2hwmo!|pk?Z!! z{eo;iUv}g3Y6dPpYlF_$k!n~Sv*AeFjqDOxZ@etFaX+ezeg1vtd`po#i4d61B6O=! zZs(Ob3wGoL&#T-tcl<`yH1sjK$NSo3Wol5Qdo_DTHgJ^N~rV%^B#~u$n`q11UOvi#B@GczF&PD>Ze?TNvfiQm|Q5B35! z=JMxb)k-wEIzE)Uj;nT3og~=dmjEsq08#F%SJ#!cKBXeZFKp#w^pYs|_C`l(#b{%! zH8G>NBc^(QhZ)8m+$OF^U1g3T)G?v-Op2Lm{p| z`M3_;8el6upF2@_%e_#V+o~vsr9FCVI-F^RR)!=JtLgQ25Z$#vbF6^kmcw`0j zPQXFg8{+N)oA<5Ll%3XBmmNE zb7OoX_B~g5PUlwkQ~q>sp;>9_OO3+FS<3eq`KGRp#g^|!P$#(Q`ata|}cc8i?+pxFJi+Ul!;3L@^! z#7aPseHsg{*5<_g+J^;mYEOq`^{uT7tBfgl%t&I^t)%r=Y5Q`T#HG)ITA#8cMi`LwFP z#N1=-dUJ7LDZp1n-94#LkKJeO7sZY;&k{BQnUmN?;k_-s&N8o{c-)!AMW678G^jTZ zD|clt9#&^XAz3pv&-Y@Kk2YF#Nj(!HtV@m4EX8m2_6|RG>b1Tn;`g!P_c$^-Gz|~o z4j&kyTyp<)=!D4Yl<VK<^tUj)5r77y0A_VH24a)>hk=NfNe4sLz}^yhoAc_9;-oFwjX@c545bp&ly zijp||!uoo&=(`%o(qJBMjgyZHT>!V8);N?LbMT_i%EEA=9jjH{N@?LDC0Hr#vMg!o z{ekR-o#G`^Or3gI7@H?}wE_U9(@Qu>I8h^wtZggZDWWqE$-DLF9ofw(O&~z;^j>|< zsr|)K0+|2^3JTU-*Djzvc^Yh>PITv-*$*&5u6B_>AUQvG3TU9be3J>2KTUb&s$y~b z&ZHalG=~~S1uf3JIpfoXuZx94qgiA_Bj?>eg0Wi?;y-7{7BVCa4+=1Q^JEk@mQe3| z5N2IU71j!BJV|b;_I+kfaS5KH1l#*|0?|WZE2g34WoCn-1=jgxECbi6JRX?X)7hJH z>obs8W>9G`OJ4|^wChdG8HvnA;Fd+Ied59u;Em{?SQWm`vbv>VvuHQ&0x5j@v`-X-q&9WodACG!j z?g}usJJ_m#a^D%I(MC%+XNtU6o$CNlI(?ZZ@9s2vYYV4$2tzpz;4bi!AON6eGHERa zWlos&)a>j}1u6|P3m?N7g&j<1=Y#woh$EuBak1R3k?Z=(7*(%-JV?9NQg1$6xKlEK z(8@Yxl~;MMN>ia-5~f)T<6HZcv)sw2(#CDBaZQOu360-={q2AhwWFy88sSjvI=t>j zY+YZSK~Sz@gUs@g+-8N4j6)cNjPzkVpzi;rOH3}zVWRov_8I+Z>&x{C^Y_?k&(exxaZ=tMGVIQA?T*t{Hw=8ZDfOt}t?vZ<^pFDP*7Q|K@;@i% z;Heq}_9@TJ%8c`&o!=koDA%!iD8>!)vy_nkaUVO@Ez>AlDkT24h*bt(JOv^=&=jvQ zeq8%~4hbl)17ySceJg0%Zt@#{GD*NmPh1p^~sjzTMQn=#G`K=`? zv%#X-aTn_2t!v)7E*t04KU_a4F8j~V1@y>FY<}D9S?w%eHOqrOapa$2qvvjof7DD1 z;XE<81;yRRU-3G5sR0+lHLRxhspC)f@$Z`=$M>2;rf-$(+m?svF**xLQ|6idu-3qC zeOM@%?(IA`TrW4KRQe6tf4HW%K74c6?ag&k z!d$p)0O~fh8A0C$rvGj$bVcWqt=sU=Of+pJEdjY%X?t%^ZxYE~RAL)?gBb(QO-`d&=c^U@Zlv|$AZva=NJLWQ8 z1(g$Qgp>)UBe1ygpc)21YtSb9$UgVsgR5NX?9>!!g}oVYgvm^nJH33l&pQwy8SpWzFvNm|ENbL?y}vF?1zsg9I!{Uv)jIAiEnffq!N1- z4h?*2jE{($C_oG{Y9b%x8DJ3%r2R&z?%70W#O7+vq@yP=-5 zO2=-z50yK;b>vLISlZnHp<1tp1)+ZWEv z>t84tH#j%m#&UZ9*$1Mk$8ksX^No25a##s*xF@=i#S->34bi=n*Oa_CB~gx90xBvXBc)^ok22Nd6%knL#7u89wM%?tfrXOKL(^}ys1wOTNabG@d* zQ!(o&MWDAC{rbH1^uDoJA8EaV&8%L^ZMkfCIhkn|mpANBUGEgSYjPinJ7jj$$uRcS{bPbWrT$c zhduBTfZKfO{@mB*@^=4D@m^S9cDFB~9&T@Cl9iS+diM)HNSj2b zE=C7ceN!`%C5p@K%hrvNu)jr~gvF_Ud9Zc9`wM~dLh!sEy$t07DW<_SS7r5e)vF!{ zN38;tPd!AdA}*)A>&A03*<6llb+jF3wO!L9k+^9mmZ=(LD=mBH8>oh8gFNixSYXU=d_^6F*^a>nc3NEHa3|RU$v3CW5IuM zYv4riJFD_9E5}e*mXgtyV&C%N@lK!`{G*D;%b(TJwB-${pLZKvOaCsme0M85hNu~t zo+QZ+wDHr_jv*#n@zebdnLVxS{h$TNOUqtngXfgB>Cp6D*AZqtw1k=xdy^vE{JV|c zARx22wX(!qfsf#luj^oId&t1fFdpZ4o)5~V`r*QTiE(j18jZo7*4cQ{f*TEmgthwg zp0B{I6nVNBA8!Dd3*;DGy8pc@*X>fAI^P{d@_3I<(I@wyi8dA)a(a(I$++_hMU)+# zo!t&(gb7N)#}6M#ZgrD4n#WyJuvvqtj(Tlihnkw544pMZCEmXpu6zIidQWZR53N7Jc;;kWVGM%n^&d^Ozl<^ zQN7n}%Su;&RN*rwvW=m#bgo$t{x*x{fOtZWGIWf3_)tpE3v2vH{Q`Ebli|m@`dkBt z0>ePlXtsI0LABEPjb*2V3)-GJ;}qm1Lc##(CIMj(s2@Wg0sA#VdhMmj8>OP2_K#kO zS>fQ&bmCrNL9xU`4dtqSi zJ{>(hbfAf2OX@DVXt1soXaHffr0tc8V+)>FFo zK<-$_pi6GmyTluOI?CiCCHsKJM?i{zH>934x)`sXIe;+_dEG%~UD~bHR^Oh3gY5x= zGK)24mjSyI6*tKrKQCOC&Le)cB4uFswbwa8eIIJ}SlQ-9!p(Hqn6H=o1|E=xMY39y zv3)SKkcnYy%eGl5Z@9j(JZ84?U00~s)G*~`!}d<8Y+Kd2WiY0b$+-LHfncH7cC8;# zEls6o9fyQh?yG#xsNZvbGrvM2qr5y;t}>)$%+^IyoYT;5YC7}_RFX)TT-A^ZY@)h( zEG=MELc@N1kc)V<%sx>}U+yqVxni0T;WW%XKM;%`N*lho+`6Y;37-1jBUoj;IcY_X zVxwNQBusd4oZ8XdwiWXg$@3w7Wl&ARjbD^i@=5AwMl1eq$E}GC3cE$ioc>^7D^2WF zd%-1lAW&0q_)bzTUGbcW;3OJi%)|$gM4+WNN_LVjxYovS?}HW+oa8`9#u4euL)Mj4 zG{x8mPB#zGKE~L_CcHWW+Nw#`7p(y}|KNcnh#SeL=DIK>2M^}=(0e_1HNPhe-K@Dv zF;KyVj>Hwd^P=QzS=q$}E+3*N5Oz^lAjkh0Y-*>->CvEE*KiFa{TP5SQbG?m7vWK; zC*N$5GLTVtUS%zoe8C87@5p(o@0notMxu4M5&VFqbm+NsXo0DeA-#w{UAF{`HMxDO zZb-tv%_Ah_q09xh%_fd}K!11=&ZOpTPm`Uvtmf-my60dx+mHltf-t1^ieIi@#9&2Dv>$N*a!Z2h9#FSZJ$4duZd$8-;PTw|>*(SA{wLdut>52O z(t05^DJ~i*bm8#uuU}c#1$W3j;JZIHpj}wpJW4N%y@Y2w*fNWL87Slrs-a?igHF#O{RWjmiNK< zsc}6zafe6azKQeMM6SDJtJ@R4x$ot4juD?m_)ihhz6XM1)ug-!!YP7pDef4OmB7aG z9MVB)qH2)Vjpfgwyt{>DJ%!3ads8{`MTx7z^9gBwbg^h2L<f zB#wx@ilH9$sDMf7;b5mDj=0)Hd}irQuZ)Okoj{pVU|Z-Hm%=yxYnTr9w$6AnP7W_z1-_Vc-{huE?a7AyW_tf^!Z~t|`L~r%KK4%oSM_j!B zd`EbW!oM%J{dxNI%`|4CaG*h18hwEB0k$U$?3&aObP|km;Z}VtrVDF1?JR%J4*=rZ z(%{Gt>oJQ!8;gS$);Ji>h^16wn65|;E}bbWrBkRk%jzF)6+i|3CjXj4>!0z!(t_Bd zEQVEGUH#4AoW)&QmB>%Mz2MmUNo!{u##p0?EMr{!IjwA3SxE z!x+X0ymX4(=%xXT!H1 zl|oCyaj+~jQ|Zm>Hr2!!mx^`d-x-BFDDBae^FAO~}K7;(Z4$Jj1 zMrhhiT|H(z(kzYO;SIAx5M&?aJ*MiJe8{R@Gfh%1qifQ1!W$byTyt$*WexTSmD~UW z2^dkgW@OH0~ZaB#Hmb_Z&Ego>R zb#%N2EbkSNSM!ibu2KlU9u|foTM9shN9a?ajnMh^i?&07k#i4G2yYG6eF$PE^y_;%-oS2y-NNgD%jvQ6;@mjGp(4 zzG*@(c@k8vjTmFpvkMr#ipOol6Bo!vU1Pbr{{MU5h-*ty)IS5`n@hv<;~gy8b*={O znb@xD;}jqLed8$%{5>ip$1Q@W8hh!Nu+wFTbt&Sy2_1R2`%ZcetiVgvsrJ@ zozuf^jia}C!kx3Ef$PtDITM*;HW(WlH{Dh%%JhfX5V!z=16N#JTtidSH7#OR0!(wa z-i~Zfb6N82KgP0{J*0tI4)8r%QyfJr@zSk7LS?XE7GrQmqF`iL`@rc8tP#O z48LYIV>ya4fF}-GLqA3?J(%xxMCXJ4Fi3$F0lYJ*7Nd-XIsfzlL$Lol4z(LicjFO| zXJU2S2la;L8fdtDb~m1^7V;&xUsR(_4t821v^h;B+ z{5ge;eoYa%n=*+w0jJ(>Q6P6;J4J&stiGSmdP7+S1Y6^MWkC&!w_`7<4#HtQZ^t<> z1s}r^dBj5 z`|BjUJH`7g1z=F))YUvg>PoqI&te78mdDH;rCfi0oCWTlbr^@#t7_oVbUK_U%UUIF zY?k0J6Y0>a9isS-_06pipZx<1;7^IUbnkF^>8U!0!rJ>QCd2D7^DE?^-$672rTyeAohR9Zu%%4?pWc zWo{8~0NZ^R5lF8ytjro?Zv688`6OKDF|sf$`SrRd@~I(f(CRF9E-e2UCFCfqjW=9( zOO~7E=m}12PAiA$`pG*R4j$8GUWd!|gscnIyn-VrHGNl7f?BM9)`)v!ERv=bzv8M1b1X9( z&I@8sbHy)8&%`a+;0ietm*tJV8+Fe%SkKlwlu+uRG)t)GpTBe8+*iniY*qa=0o? z1IrNMwXKAPh9)@Wcf+iVPYY@qC>fzOhN<|capVdpky+!S`3>y1TuKIh+`agN#6Ge$lQRL(k2BzGV>5!p%4-tkxwJhJ1+XGL6xj6E<)@s;yA(IRr4A6M59J*BV~ z8Ulx&7l2M^P$2hR#i|<)tzBnAq{b(*&Dpf!a{UY1*a0j$K%T4fz>U0^dC3Eh!US*8M;3nb%HIvcE zgXWvy3IdV%6%xtLxU_k#LqGrr>ejN}@I{AFYF~Mscr{S-)P)#Hb;#}qk!%dG1{1`< zGvl(&jED)0)*Q`S$eRnka;H0fMY?zqZ-tau&Lj&>*WZH&L_L^XUPlt-e2sn8*WcgL zPX(i@pXAn*yyQ=3iSpCIdTKDixHpl5c1#3rK5`k0PPJx2g=t=ZWg)+MURYQd*21#i z|DiO|vn}4G?u}J}!y7LxxoDv39UGaJe!XgG>S8{aThHPuNKSd6e=9a*VzQK$sQXQJ znAn#y)-EYGAYollJ)OZs&Nd73#7q}|R*Qx7Wu&re&*>H$vE=FcWW0z0rMW}1mS=da z7al^U@=T(p+z_S^Kkd#&t~(lwA^LARc}P#5tJkK2<3_X)U-$5WfDf<{%U@?cHbfuD zwoxJ~3e}QdRO|J>b0Fs7{9oYWhdr!U`MHxFwPI7vhJu2nlYMxx*Nc6KXIJG%b^6JO z7c54OG>Y%eB^onMF|p*>e9h>diT~W(onj2CkR^~cSpXj7G6g0zJ&x!sxvj-8=-0JC zwSuTs0!zzQY=64Rxf0=OoRQL~H}EjYlgQjh^VMbOVMDG)oW$prm1zJdVtM(kb@v2l z*Mk2Ha7Hyt1RHt3+))D?9+>_3O8IqKH}>V5G=JZ3%`K;h2=Gx;OI_sY6%?owo!oKq!<&k)au5Yl@t&8F8X5 zFmT`26Qw+%j4l=r4CHArPHM@-et!C*C3s)RMDSs7p};sd0g?0BJ4#^qt23UO6f*(1 zgRrH%mZV(FM;4Fiee__lLWU3R{KMl`PTPCHAw81vz zm8LNa*0-RWzV?e|QQAUJrkxMnQM@D_r@1L+g0=STkN=K$lB+igLCx0gtL}n*aYEGjEPwLVz~}#!jko1 z`U9K(y&0#Xk=vI@hP^cz2SPr7R zhj_buNBmiIO`|OfVdhCNyPks?^j5U+%cfspeHml{eFDsDJ9+M0JjXeJF zmT-1t#!VRJR~k?3QoTBf6A!@*8=u83@lP%|)GmP@j!PYx7xm+HqVti*mGaX@vSjVO z`~{E6;ORGRv+^Jrg7)hkfn4nMiQMotLxpaj!Ggdlup&gJ)r)S*@ZaDXH#-O2V5^?t z;le~3?rOMG*G={AL1hJ64S=OajBSVFq$DJGosESh9KW0_5OE|gjr&cHn%XUSmgwEO zb)1R;MHLv$gKTJ|?|A}?hOD7bDwOKqgCC}|F09A zH}iY<=KjYjWaHL7+}(eZ$d}bm5}N-YyAsT=11fXzVN!Xoek95QA|q`XfvCkc3eE9e(Mb05Le)6u*goP%i-Xk%Rfd9ezUcUdWd zba0!p16ibN`xGOOJ6z1xWsq4u1dR`v&qAXEfeQl54#1*!pt2a9u!^0ti#s;!@*>Bh zhP797K|jwhb>B`u;9@-XEqK9cH%osh@Y?&ORFNCmM8)I7AipR2bXRt)qrz_02%!$3 zbnnl4dQvqfHNf$kuawcfTj#7`dp0jd8}m%sq`sPx3guS$r)7+Wj;@l65YSamPL0H( zOC)Qv*>iL9Q;xbG7t@gojlm5d;q(r)Kn3`**V)V!F^E%^zr3xT-AJhEEQhVLb6HIb zGoj?Yk=A^({#zx%tPgo#9VI~bO*pQ4@dLX!^?`J9jiN_t<=v4T;VFkS3c22-rd~DG zV|M7vqv^hLzj^4`v~Yd&C}c7ew}PsREI53Nw>MZ|8`BlAfl2XxY96QR2ZXc%|D5TV zG3qYuIkqoxFq;*2CX}F6&9_&bukv03tJgg;OZ*7d?EgZe$yeM ze4(2EPn)w$UYHF1DY8)(gh;Gx#dl}u`)qx>Q@|yezAn&2#0Q;aKc^_=iyrpqzcqSJ zIr0hOY$K1Ss%F_JwSAx6-kkp%%m^VAS^nh*>Bh3-&Q=&@$zEixFx^n)F(WqkY)OSr zfh16^T3}<%RH~&mo1fwAsb`b^M}7k~9moZM3);iDPV17I=Ca5lKF%xlv%(uY+@zDk zt&Sn2eLa&{hTJdo^z=0txKr@`Ad3sx&7)eNot8 z0q^y*=eW_(d7o^OS^WmGC=V|~Pr_)-Qq!#5ao|01thC>ZvFu#ff3Kq5?)xyt6JM0F zdcmRK+&fw3^*jisvg)kamk&%)bn5S^ZOVMCi#j*57LqWtN-DN$*EKOg!p9?l_RGW2 zMgfE9H3e;w<)`sH8|(J(zdR1{PEVSS$IfkioL!JE*?{hQ!slTdf6DVGfv%G5=nJer zrwoLEoMcuQ>#_=(Ck`vHF&TC7t~twI)`q7d;x<93T6&|j(8p;7UZ>TtpmybJQgHuF zvdq2FCZy^Sd5?T)0J!~$0jrRDy$3^~hQ?ks?j!M=BMiP-Mh_r&s>2OQ*VdnX8b6@v zgCPs}e|P|Dn_ZA1Sy674Wi>W!b_iU5&Jn-;I|22$CcU^Y;{;ep(e4j1HYuLGX^uvm zFHXx1EP=d3unVaEiM*dQ?%kNlc$cG?eBG>2^0aB~GcwQl$`y1yc5_w~e>tj%!6e0V zB7Oi=WgJ#M=N@p}FdbwnhMof!$?!|~=wjH_V$F=kbA-k}#+puI%6vK|g1sOiJa(^0 zQD}JkO$kAEd~9$c$7UeLbTO`0Bd+%REoeJUc+UsfF~&M>B*@5?kh3*%&7?X#I3cXH z-@`a+o!~P;e4%U{`94VYM>@%sOm#>KH9HZFdN+zal%S!F8Q9V&xWhzEsjwV;z?JNa zri`+PPM3NeroN7MPv~U1S6gApU+*xvR-i}yz_=<}{ieRZzdu%7uG#x^3F*c+eN)%* z112FcmrO#;_H_2);r`BSee77Ml3x)`)F4?buNoCx)U+fYZ)Fx8cjlG7$6m{0Pz#Yi z$7X#>S18&HA0nVQm%Tf+Twg~g>h;kAIKiQFQ4@V=O1}o_2V)MV`=9yK0T%rr3lrA* zdfWp^fpQ1}ovgN5#);KcRbqqsL6C%PJpuobQyd2;2|n79+|cfNTUNGXs`yO*Lvw!M zw}Mb8Cp^IYEeYtGVJ#(H5gAw~q3)^n2hVpX`3wcNm^_m&P}BcuQ*_i7LJvLJZnvVAh}XFZ zHdlq5etBss4A+am)+}#WB~MbTw3W0`6}NgEz}gKAATJW~@N~pp`P6XhLDxP}m_RK{ z+m>M`&efZ)UJq(T;xeqDcv?NagYw+6<3;u7HF&5%xN&}1b-TX~aRfAT=q5s5M@OzZ!BKy}KByImw9Apzbf;`t^V*KswH8&sxYW(()zLy*G~IiC&+JkU<@R}il^ z0iveYJ+5Y9pm_ks7V?5NTdv#*43L$fg1oYxVEtArk2Ob1j{mcKJ02J<7dZ|>cO4G4 z;CD}6^vYkN1_dS$l3jsBA`-pYN1YgQ zK>|@RlDl&kG5XU>J#c*sWY)&2M(#3-AwOt_M-JMw$|{2C(2b=%-T%&_7jIuFmlNaO zwMQ3!PA*wLEyeh#a*|~}6wx5aB7+PhWwR|amYuWi0|f*cGmXG8#0jTnfQ)KeH2)$* zBNK_U7lofjKQbc=SQ1_c!Ne08ZgZ==hmIcNwSqmK`y#WS@B&A;EjyXXtpdW;{C&^| z(Zc0p*ls*CB9bY&62#I>eXjm@^3yh@-%bGDc2YFQCOWE1J5?!ve>yhp(HX#OV=$Ks zB`+S5SFW_GPU$2y#)hkcql?Hy0}a7O{@HW#*DSq)ncHQ{n#hKNtJk5BepP7PR%qO9bgnl`@o%#nco_%PnKb|N)BlfNdQ8xNb;)yw zFmkg0(|Z@-=*`OL{`QRy1szr5QYa0OdX$6y?UiFJ4*lDrKP2LEGfh7MPCITDl;0nk z5g~D*1$qke59))V0RbIl`!9c6{vs}V9%+3I4Ty~EkJXLlla0XXq(uSH1*~qE9_N|~ zhvC~?&%Cx*u}KA}3$+F*!CC+3wa~gAX)%o>(C+>r6W-xkX$mN0lplnACAtEyW?9+X zw$v{Lu|ge+0!sC7JQ=dp(Hul*3koJ^<3jV3<+0T1sa>Ex z@Nrn;*|MoV2-X8U4TO^R;M1TwxJ>HRsUn&}+aAfzvI+|)7ssPA?a7=tNLHY%^{j%# z7BK_|ou8#K3$mS_lElt!Y;jb>JhS0r!sL}UuRVF9PBHfpJfZ(fr2qi&{2LdQe-q0=6P=nMmqW&N;dw^ zZ{l$RQ`tN!N0gM5pnWACrK=YC1n63SA?6I-p$7vF9{*=>?rG!5N86VuKWIh;=L(!w zIf6Xws0NS%Iz#KGpWXv&1#)4B_S!CG0W@l3bpanYsS;xa8w&&|?MD;&H3>^Hb$Q%69!iMb$#^?}RTiYM>suGEalGdXuez7J0$HrQEBAa>-}oL%jrHc zciXO(XBdXh`H4hJaczA$B%q6HDz;<0tE!esqfEd`*V3~Mtec2Aa zL#w0{)~0xc*<3~)RvG}g%|E0(U>-C>f(d}Jaq-F%z>!W5$wPl2*Pw8sRuxKMXoS(A zz(xe4?c#xF2MJZMBpmIC7d0%h*gaoE)JW+E+VIHTxE7wa$+h0v$8dWApo}Q0fGi_e zSvY=u8z_0oa)xy0Ek{mi-OtCQURB;GL^}|0Va>z`FbF|(qJ4ek7V-&0LtU%U-1`zG zT_&i7xw(yyZq7 zSsBi6EEbC?J%hX~9w{*N!W+#iv6n7SXH=Q}TxTbXm=t!-U;p>sS#I$^TuZI8**9aC zF7c=1zjyGIk0?DVQNl_7BF9twuGVO}FWDyR#*CecC1q5|v_rp-xQE zf;H=`cyxVz!l!_J4HJeiwXS$|2^C#EfZA??oKr6Ci`90kGzG~e__*jcL5C!kUY+;# zFWwt{Qn7!#v6OStdZu(>^KXFe@ZBwGPaqH$@vvqfqj&7U%#<5U4YF2tL0%4SV6ZL# z^#j~6)czpG@Xy_gbeTQb14F71S!lW$OJ~FLwCe-cs@T{no30ys)q_JRUd2YXmpn!C zq96XqE>nl})geR%I1h-6i9O3LZp-Az$*+6SmLTrnx6ENZdRHs9J%j9<0g&=-6rORT zW35CrLYqb2Z8+Hl=#~a+h)WMZAbx{P*0T|}y3Io<=Q^@AFedJdw?(gY$JRYrU-uF) zDWij60?`NR#dq)mA`>t6(>|^MkC40HJ^B}&W~QfqM;Bst!*S3O$)kosLFw}j>ZjZR z7F6ADf1w{GxJ`kA`YkWa$N_W%#GKpZgPuvCzH;>gc3apEMw!Q8JPLBK32z+ttJpxh z2^33ZQ4M(CEn>*!PmN1%t&u!yJ#w2maBX9vI5M4F*`3Iqq>y4oG#txM`g@E2X3KI9On6$y zOV$2(Q5%tI?KV^fB(P}~98-e6elQMlr$A}wi(^^H z8`F+-w*ntaqb@12y9JbM>z#r2)?q>hrbUJ!0fCFcO6@dmO@aHfg{(mkw_)V2Z8yHV zb4k;M9aKx8C$g?)0OE5=(jGw!{EA)$Z())SazRkQbgoopwTEW}Zwi*e**?~;KaT?M z+IDZ+>F9Xd0@v#?YaRa)!p=Qh7laCeb(@ao^Nx0lPR6V?u5AAl08lgq|E2{`4rzO2X$Aiq-{3`ckHehbp50H; z#NIvjEA2f8`H;`KXz`im{QtS4;zNegnSGHi{V!LTH|K1y;=yXeiAN0>BPDat|8rgD z0(04ld$Y}e(a%Z;3a)`ypXlxakS5e|*V44=;2eE7z&AYx!%Kkk zq5L4r)5+8T>-0lGubx$h@8|jVkwKx#2L%ff`r)*OCj1+#c4jP$mk+x?co3Wz>1i)( zR=5wjSsgE7saP#b27uiMM-5!D5Uqh=_4h8hLNxk&RJ2Ou6To%C=7*$hiB>Q@tmMyB zGQlTL1OPlBh!a%E4z*3Xw`~z^K3oiJPr&mS#Ry-%EZ{6#xcl){ccLG@#Z~;3|Op5P- zsA_4X1j!KG`|!H6v$L~ID*{Q|<%J)&ip5RE>=^eQR{-P_e!d-A6R>*_>r^Nk^w0*O zU*DMTq+S*m*DD@+O3+3lRfh;@f1e?5p8);=Y^8hScUF5^BcrD69@m5UZ`lKN#^A{e zZr@0QnWtLod<Zq^Jg!lAA*wb*|ruV_YssIlo~pQXGk| zYQfOm@4}M+-Pay7eT#SyLI3_AjJU_O(DPZ}VvPrpf=a;R~=3uvp^5`!`d{HWnpwmnz!QHR6d)e|*-M>8L$xr~0 zdano|`uiZv?R+rZTX^nUB_q=M`0m`_ahw(f!8fxr$ek?^f6gsg$KE2@dNLkA+0)ze zZ%G8Dr0!gDLNH`?Y0&A`IS5Y5pL~i?mvGR{fszN%K0tIq0s`>0z`pkYJZYR^V~Z^( z@W#*mZ)U!03mnbQ63UA7a*By?KNVJ$G_!FvZ_wVW#Zs>>?(BBZ3)g-H+g&#f!VlCT z>O!ZgzdyfnGMq@7gQifsEe5JMME_{8gj}fV->KH*H5eVmt%5iZ_0JG~Lgyc9%J-ni z3aOLW#z~(ZuV0_Ikajr7^AL7>lwqw`X+_&x?G~He)!z?h_QY#xvj02UrvRZ0(GBhF z>^wH!U?vdmB>U>=Q`(_3w*e?X@&b14+6qX3fF=EVGondpuR{rJP3w5Uf3KR~L^J;@ z?aPS-3UwtqR>wDNrN{WphSi<)u4BRmnu6PmaE2XzuLG`eX^t8(Wc{iB5MU9Dkl8hm z%<&wvG0 zD13)~nNg92VxO6tQjd`>hAi^6@!G;=9cm~5)Szx8rttjLoPn?*+get|0T?hOc^QEn z2Q!k1(1KCV_O3~IKVYDCn~{8B=j|BU^6zM z)Oxv(U%YRIW-750vUQ@)ru+_-ND(i^H`obqhdmfAt(2CubSPECQe(&vJi*5p__mNZfb4!0E-J)@8)43Y#qLp=zqf z1jFA|XDD8(?9OH&wGw{%1G5rbF{w-$4&P)_479d{uI~kUm z_#(d^8hENqXr|~@mr3o@E|8oJ^nIPUR;i~`E0Js5ZKuRFzFp91+atkE1rhd@TT{9J zr_iy4IqQOXEEqb26!Npg7pBNK^)mQK+KMFk_a)IDKgE zZ6K}z+69)gN+xK(*7RClB%Ym-Fn;nN*G@m^bu8hD86&UaaAs-)s_67}Erq$&a?7$`^!5>#3g zp^KU}L51h~V1Po2b85x*9MRU~?;P<01P#bdz_0P7Osd;i_P5(3?lwd;_1_At%JU8o zVgag&AW_#~JlAJec6dCh%dw-$AkYjziCdn4$xOI_LiE4MHYCg62ef2K8DD2gjbH*& zun>S*YHnNlOL^MPd74CmAu)X~w=safsiX54E>VH*y1II3#kpL?)xMY3di2&*R0pNr zBCBt z0Op1A103K9pdX-aqS0X)YO$kF*_|6eUp)+n3wfrMN0c?QwxGKEy@~pPxV3j{wh|b1 zb~XS|EdWZpD||8lIvTIek6&XJGV37rjJu9JsNXmaLfOM$;W&y2*CF!X0!GE$cjQW7 z9|L3Z85r+zqAmc|-j~wi+Y~A}^bF3J&SJ~fBkEF2(+ALNJ7nQakAlu!3rpSa-Fa~K zLah$(2s#g76u}_2_#H#+gl1q1(iYDU^4SLoo?)5eF-_dx0`uvA6qt6;C;8Fj{ber0 zT}0x?h|^UFG|2yhP0YF}g^c%2iD2*wnUA``OO_IxXpyWZt0URd6w7hSuj_K78*`v| zjI`$wDNh{bC!gOmlk|-G7yc{87T=Y36aqhNg{iOP-aMFL?y{eB72mPO}NaA zl98RA5@lwT?2&Ol-XGQVy?)pCcmDT%?sK1WpVM)!>!i-l`FXUTW~nQS~i zY`jNo=R~jYWYA4HZrU!LP6q>eA+h`}j(fHX`$a)~p-5{;tb8IUDfk?Oig9y~l@qgmM5NtG(sK0W`{t2vlu;H`7 z5IhhIXy@cE21>Q{p5C)X3bxppA6@es{Q3H90sTJlx<8hcKxOlT#E0F)=T?7VJ@X2_ z;mdZ_RSP?4j-1{Sx6xhQ_`H?VbnUmV5$%m<#E@6jt-oiNBRlDFWJH^nj$F#WKSL=509xk!F1{5a_!Et)|qs^S!+ z*!jV?Rh*>ka7OgX0~>|H_~5dxy1BytxGe4YUH;=vGnogxwi5{k`Pno}B{+@%+U3%HjcTLc7#rIo3g@d*3@rNX z(mRcgk4jAZ{Wc!>zH3y!f=40if(YA#5d-aMfOSsX$`89($K3zL)zHiPN+bnpS9f-u zHaN6?B7(&^cRat!mVYdvvxN&rK(tyI=)`34D-2FvZzBuE--&S~ev@C9Y#m+)t(H3w zcR-4O^d%n`w|q1az|Md}=$M~=K>^~9fF2?YFx{bmqi=+A!-k7^_8khfH^g%^J+86D z88|mKJ8_6pE3SHKR;a^k2`9WP7fH*a3SJncFzM*Qz>|j9r(%av;Q ze9G<3m#ctV6zsK>rG+c?|1-$UXIJnrtfI_IU|d}UKIt_;RfhxxSM&!D10yFU+?_pp z5tRr!ALOOIX>8nzqXY(tY4$ZN!hcgi!sfqj;wqxEyoT{dBeg{7A=m*l4I>wFb^f&M zoXP*%pB@GUFm!)jLQTH>pM#xWZBOI%D?5h9O`hKp)gS!dAlli>73^^Tb+zoM}I3lC*o8Tr0P zxn}lCx5$>BV4j}C1v$yL6185~CI~3cY{#6fDO(xxT^g4IGPCkA+la(7tG*i7*QY=u z0Y-n<)|MD}R{b#>?tf%V411plFTn|eXW{<{QXKMwfpY@|{|Y)Hy0R}#5|7zDJUm_v zP8f6>UbsLF-Eowi2Uz95S*z^LtCv`-Gc`jayf}jaHwWGNcKzjd^|i7ghhM)5Jr}cQ zL;qnkZ_7x8F2DHm-PD^U;{pN#gu*pQSo^EZB{uBA7amq;81{7}{)1`vb1$cmic5Hjhrh~aDbBjbB1dNCv$B}~r#0U?0|D@3!*DfyK z+|z0|Ze-Di919HxIzi0-Mr1Nki=b`a4pI$|61dkDV~+iFna zM#GSQoj$VvQ034_+@LgMxPLQ9a(rGPxu^Ypoy_AWhp`GpHjsXkUkC@P{wfsx{JS&1 zated>cK4IJ&MVmYq0cs=qop7vJwm(GSyodt#L0 zXRl0(z&(GK^PzdNv9_3K#v?JYi3wW(;Ucx;3;W6YZMgzEpG?yNF^5YOPv;ME`_8BF z%J+>v8N3heqW<_*V)(GNd8gIbQ@QS-ZC z=gc3i;F{xmsmhayhi^f3Tf_w5^fcf$38>|7(TMy&jgj;Ji-a{qK3eJRNT4-h(aEjI+!(Rt8Rp+-ONeDsPv`8l zxfK2DvqSTUrhyHVfF0*50k25g&&uU>-_nkKYBQS92v#^&aI z`6qUHKiHEfZ~FcPJ~XNrZD3_&=!qF!*Q^**Pn)Tj0O zSeite)%6hH?z&X~mDJ;D>P6_4T~ZvsK63$tM6|6K*@1Qy#Vt|`MfD1zjG}64G5uil zz9eY7yd!_pc7lD0uTyezRI_5F;#4f`4f19O5^fujvqtdAURd|j42q^9VV; zo>Mzb$NZkgS1LO4M;tu`Z|Bzs@$Khn)~q~BsVGmScgcLLLLxuO_A;FnaLv(LTOU?K zs3&${Zz8=SOP3w0gZN3e0aNRbCzoe!u6;YADwWa~cv)BX?o=k9x`Pqf2k2YNwTc9Z zP=g6;1$(H0Dnx`}G*b%B)4cI%cQOJioIll0?Zl5b_qx_r&g4DB$*+2q7W=2O<79gHv**t# zKxf^jH~ad{#r=EIvFdons)c0%Gma(ak0&Z83g76hVraYm(UiQ~;<&Nzm)1TGc1vt5 zu=bT%RwSy^#f6SVJA%qM?6@G}FzOTjz;QWFRRn0wt=0fBJY8id<^-rq50?OJ;Tz?G zQ5p?I_2(HLKN#3OqKSVwT|`%K8|^+`u`9S^ZqG0!HS!lDndc>`se)q{ASAHgWdeeL z7}Yda3JFK2<3jCX`4l*zY+%EwCankapcuXh3k-|Y79z186P7g^OO>=sYe}DphI2;u5CBS1vHaO0V$Kyh*VXl7l>IYved1o~W;v@`G;eGt zEXu^&-Ryay{KIQ;+N<+Zu~J2$PN$` z3;w5L6*gq^mXGz_&tO54laoDkE?9kf2kr-3Z?*6ewWvzOje%KQ2dKwBG(f5J?Ffxq z^@>v4Fjw#$buBG1s)EWh{ZBq=%(!yfR0K%Hs5);8*P20Xt@RysIdv(_qkxjmr=RoF z|0Lvsy&pQqY+)|iJ`Mdx`@~eP?qDeQUy=^Wh@}_4$!z#|pHq3}NrXd<;{CT4JK)K9 zu22?JW6Xa_?kYS4)zT7;`X!@;Ef+ajx%~*)7OD*f<0G@eG&64}dBLn9PAH*Y0?w}i zOCbb89n&yKxd4r?F2M6n@RJ*Nost81<}Aq;5h!~~ZY!$ZPfQk<0_}G1@M2~3jEIap zG4|C%WLB5p3J}{T@&fAitv|(|Q>c~Xa6r*=n9$bGUxQDEI|0w=^XuE%S@GjP?x`NJ96(l_jx?2iO1F^!{#g?{Tk$fuTB& zLPJO_;bm3c9-@0xC?s#E#E&AFajkn|(zECOTCnViIdsTl>-_;ba-Bo`pa(zs=XZ~< z;anxn?#BM7hhc#uJ_a8C-+gbK@IS?c52HQRGALg~?BmasHg(G8XDR*KloB-2sr2Vx zsIK7-y|4tzxGzld6$X_=57#jb5KZ`>x5Y;!dgZ-=P^f{BgoR%l@|MrngiB;xm%YCD zsE?iZG+7ypHOd>yac283QANpl-!QdC8_&!2a0 zJ+=7;3(XlnedIv9CM+Z2IoHV8N!0LWSu4Q!5kjqODJ>*C1_cM-SywLoyVZIsRDeaj z9YZBfo(juaiVPv$2FnIRfm^UjeB%g>PhF`38f0ExmZjaFMe%{H(dLK~(CJQ264)!m zQG%D3-GN0a<+ zj2LSz>3^DY;8I){*B+%(xFPS4Tey9JWyH;3(URjYGB1mO6{E&oC(Ed1Meblb!;}>Q z3ZIzJK?9CT0=~Y!H~ttDxzR#dUIzlnLJRwm5;`u+uc|^OZP}YiSBVMA#3d ztBc>G+%AFI4XA!e^_^=x&qI&g(1nl;A3iAO&7&O!CG`fUgguK5X{>Ymf_AEjei1@O z0ioH(+-9A!hucNWk;V6;@q(<5rKNnvjmfnY=rEuC)LkZ^dV_7t>(QP{816CXj{vZU zp;6Y>(2(O5L>8vIR4eN^eo=ezmbMEseONe_=NP{)#Ya1s5WW4wZFqq6nq51aELc8$ z_C?mtXt$Ke{CER{&E4gr`XEfuH5wVMdwFA+c9EO36IoA%F=97bFLkAH>k)P3DBv*+ z*9mz3Eb-mgF{ng4Gd)^~5GaprdYOef-MMVf>%PvE(F^r4u=Zc_(5Ut3-c98H5_hGJ zU{eELVAAK#Tj}a%cqfdo#mMpotF!3cm+Z*7)OvOzI|%tGZu3SL)~@>-NJ=SM`;reP zHIAsh{#zxQxugvO^VWn^x5^MCV@eV;~`=6GSFa_f38yfEwrnAtF8W z)6>J_M{qXMPcR9#3%?O$3YVI9M$5xGK+)O-9+MB=4VFJ!VEUyp6&3j1XhUV1H}&ve zH0g;?Dz()2C-VNYz2>L<&-VIOWP8@8MD9lYme$s_DJ===%8mI+|4oS6(daIE7F}v@ zb@0q0RBTrP*p3l%xIcXOa7Z+>s&{>YPfm@rbt?!<|EWoxpSy1DNFBW{9Qr=XoH^yC z?Zm1*HLJaI{T~Oo;5n-Ek+si>tzD5nCt#;}w|q>tT2gjjdO!ELt`Bx1_1|I&(arC5wY}R9`uZ5Pl2M}IG6iZqP-TRS1F!=z1ia(ikm)P4w|~5_ zmtL~)MdESl!NAkwaA==+)F@b&81o=^8$==i?5e%Y0sWCvjzuqHyXpwXZi(T_%^PBL z-_Uf}4U1N1I>RdOze<{pv>de@S${p~le)jiMLFZMn~$J2%Smc)--Vz~;1V(GMs| z!)uj)$ms6>o01)1(M#htE`s88kL7sh(O-H_@ocy#>-u}lIic2;m@6>ErU|Tg^t2#%oF8scocVPDb)qdxitSh85$J?U1i7ulL z**_;E>pYw%II??#j1}ybBklkaf524OEAAX6VP}K(;@IJBHD8l1#1hoGz3qH#<*{vd zS1y4GmAATHV$d$>6}T!J3`9}k2`A2*6koELm!BQGCu&*L;Ha55AwQj9-M{R+uVIsD z_9H0a*agq(hUl;xSDtN7xdV7`i{P?#dHY=v5L306Ewss2l#I{>nj`3!-)dd<=%~r> z=d;4|Q&lUVw=HY2VPEMb#Y+HifdJwdWJ!1^#dkpe0l5l>$wT_+_A*duVERD;VTz2G zYHxCfan|yX5kpB0lR!+sZ|&_VoATue7jpJ|U1;kQV^7{?^N8(AyXK1Z$6ENv4NArN zC9>bpkR5!ZinpNEfYt^AccWK}(MqFiV%WYNLV)Aa((4PQ^e8uyx#Ytf|K^e_Rc~nQ z_y4Z`;Uq3IZnj3X3rp|dGnXBmeKHt!hwbSdG!QV<5g8sqGSF?4D>HuX|h zt~Tc2lF-wL_Nh0YyhQy|eDb}X&|oS}OSJrpU7=$mwqzZjybL6EkGO$P5r&ZoWQor4 zMw`5Y_{u4JFQJtReL>`C?1p9Y&!0mWL-kS?OhhQxcH#r|a7s(q!5 z@d!sW;~|=~umzCi#Du);bwHc%r{(O?<>|JMRloy>qZ}myc9o+Ns&0t|`)S;p`VU!& z276676Ge5DFCiD)j>WN>YDf2*R0vc+-K5%#^?KMmNOahe;&4F=eo@Rx@JdKe=VKKU z@J{<=R)(6P=*x>ew*cVh=9{5*z5DuN=?MC`_D2}bRXCq%AYOV|c)e%J<0WwDYxilq z3bTX1X~cxQj^I1g9W+kaM0rcP3VrRUb=_rO$M609Unu2)3ghflT4ib( z<$kGoLvx?)3dSLbjILygvr*~qrmN)rqq!@E*jI;hdFD(Sp7<+GhAsIY46Iq!kwF0i zP*?1R%sY0}e2DEDo7uExC5Z$fa^B|s%G;TC?C3Qw*6Y9jBkSZ+kI`vP?L44Sf27xb zOh4JS+?L7JlW%CaOqUnF3#l86Cgwj+>==o!UCr7;S7lK()9v0m?J*Q{!>l`=?_JJ* z8q0+^2BAKkU7>GTlLESI4WfP($U{WmMPu-*cReds#o<7|BNa%5{vOtbvcNk4@Zbn3;WYmR%w=dh$}>BRk$ z3}wnN&{~naExbw7K#MwSPci*J#ImNC9nzqx;h^T2GkSrMQDWVRf3T$g86bDh_ii+G z$sc--o$MQ0vs3)-YNKA>D}Yr*xO4S5Z@=7`Nd z<@j?6-1$mrT`cmCk4Wfw%OP&q1;II^r`s_$*}t{DryxFB!+UptJ#hG|U4xo$INrZE zIAXkUuR%&6q{J_>`K5!z&P}`zv|-2|O%rgKa9|cc7`ax@Pt4n>>!qbVt$+BrZz=8O zlT@$R7x}HUGdnPD*yE;@#gmAdWv{@_07E7nmD~E)Uw=Ia?%qwm0rU_9>GO5?kskZ7 zt%Yn34)>rgyZMQh5h$qen->0QO#dTOu2>%9w*oxci_x>fx^FR&2ah^AkM8KK{eQ?c zDf~5g2Mxt$ws=a=o!qV}+1L~I&CX2|6cE_~bx~UJ!;}Hb`y1ae7joKWJqaA)udy?l zX-5@)%jnc}Bd zYnz)&=^8Vw+&?V#nv8_g8EzT*AYnlnIQ-tz1X>D_9ji%*+WXx_FW!WH4^(yB;vNV+Yas{ zpwj#oiq>!!=ee9lsw)O7;N%6_fCpgO6K51pIkgGvAL2YYf~w&tLnj#$5P}to>WEeE zS&5q&%qa}IVz|&^jc0~5<<^oveE;|F66)$q=I>z{amilK!uD-Ydr_DK7us<@ef%!) z7ou1PG(iM*$>`ocInAPnURN!o12_&aHz^~_27LL&+b+$@a3hJ@Mx=fvTG;L_OREFAAayy+jS*kdbEzq zmltuxP1I3aWJs>zYI_sb4@NQEydxCqTvN5UX0W(Cm}v!(GH^7zCqO#8dbkk-I8?} zK!3t21T}iYA>W=L6E&{@o&hP-Q~zkhL14Hddg$_=p<01sIRT|NMTi=W$Xm8K`=2y@ zLSw(*Sjg6PNnr*+;D4hP_G>m$yO-P)CU@^Ko2`0Q9HHA9>VGmo$ee+7rs#owik~bI z-~7jts?YT%XY^E?#P-5K`^{d}lKBGE+AJ+=_jHWCj<*dpx98Z2w68d|OxI3N2l3b^ zop$Nldf-4-0ko$-zaup4mlJhBLZn){KyYPx9-eYL;kqu*I+L3x*`E&BM5 z<$N-8Y3Q%w@G32p80skOcs!$v(4el7&wqv`GbTq^#cX3!_MFu94uyD z=?y_FM5#|xQ{|{TH1q>1q59)UUcT-v9Y55OKJ(LMrbO`E2JiiYO-9ti0dJ8fGk774PEr6-dL^*&pG!iT|vNjGZ7~;kk z9U-kHENlg`0K5@V*DJ+oCm#&d3mjddT83MhI5M3hPkTkcVj`cLwahN4GVHk*xnlEL z6y=||&`HpUG1Dg6QFf1Cus5Kr0Ta|E9x)Nd&00piVBa$)Y?(e2!5%(sIr4KJ=*z0K z?kw&~!hc;qGxIMHpT=Wx%sTU18bie2!sx8JDxiv`%m$>7q)-< z?ty%iNko#g2y*ZkL=RjX&2wHgQ`D_4-t>{PU&je};cNKlt|E3}G|f{Ya?bSrOe2+b z{`oR<0qu)vTEVUV4yW;6*eKC;u~&Ez zbItY|IuP2up7TwRrJtYQOZ5W|hJvvC z&F3DoL9snobnhv+93A94(1sd?>i-Th?;i^JNSa|#iO&X{V;r&e7ZeO)dKXd z`f3QOc1VTDnP*8swh=!xies`QV+4UUFx?W+V2`t9Rm%yirb~!5r`JnWk#M55%cW|% zH*Yff86yz2CXw7Qb8ikPNWHI1?&gGdJLsLGd_qq^3IhM}aEw;3Q8p(#Ob^f^*>^E@ z+%D_d>?9MO6{Q2ZznaD(L7u|S2@{&)Khi~6q{X%~1`?Rm_{IBX2 z-uLtw@cxvxpjw}|x+>~)MBTlSF($iUWo(}J2G;h23U@q{gZLp_!RBPm^-(TdY0P6r zU;1V3YAmOpY)tR+w_+ji)f}z^Vy=pK-9xfLd2V&;6BSuc4eC;d?^fZ|%HAUA1npid;V}J^ z9;zHmQ&;ZdsYS~bM)yxYbi!eio1rkPu#0X;eKeM-F_UK0wAbPZT4T5|F%6A1vgHiR zjIIPq&L_{*Dndn9rhoh<#LV_YeQB%SMHDX?t=h}^QliCpKFF_#(h`W|QKm;R<7#{` zb?q193mx;9!VtWYM|XVx0FwrP^Srn=kyd8D5?+I&0B1gY{786A5WC_UOy?oG&yezl zt=QsB?FwP5o+@X4-+jf*ua~SsndA3zfj+9#Q0%i#am5yFbU)wK{mBU@kF;H(Cf}cf zkZBKiGa!l4C()ulXG+}0GPlk|>=#dWl-=gnicqviMm22wT%#oq0GyS~Ts#@qbs;!m zbFY8lA%WTxkmsLSY(J#Fae!h((P3lW*Co2m-6 zwm;0CUZ`qUbYJWo$ZZ|7qS925rI+muCkDn#ipOnrOiXj;S9;;9x9oF~^ZKK+G(T(w zSD-=Z+XG7@p?#~bW5PrGvBz9tp4XHvN7JH zJ^F0FcuI?Lt`3*BQeUM8Cha`wJ0Ujlp<;TJNS+z1C1Q{7vD`YxYwLO8!1Vn2wH?7F z+%8z`qx0xaWcW>7$Fzk*&$a7mD6A!4AK&^sqsA?E#&I3z4*!(C*sB~@8h=XrtSonJ zd(hiGfv1gv1MPd?DahdBFi){)AG&ew0o&5SA~pd?hAw97H5+ z>6@naRPDg89NuvL2a2QE3X;UMoU$J((&vH?xGRI!K5flA<`!{j!#-VNY7FEuRySP>(x5s`j;g{%@G0J)28vO105p+mUwDFH+A=hGH zl7buK_cl;CA-OEbRCEUwQuhoT_D*>IrN%PhT=HeYcFZ9m{4h&yZQluV^ArQgja(m( z*L*xaR>Gm=00a@RX{S#^g4czVJ0CrckCwpT*fDk|EkFpUfx=(y0ajE>9%UqEw37)~hJHZ%uxNkzh1qPC(#=SL}{_PJu1ATg5dr z#ecdqiiOX4W*#%%Z&GygV4=yU+$WB-jxQ48T-L_w(9G4k!cJ~nc~`H+)%5+rj_ z&b6mO4|4t|)D<`-1#L;B97L}@A`8fT_`F(t`dFz2>R=Bh+6?&2vFc9q2?<1UtaDT& zk!~+K<~<^-S3>w@tMW?X>5sYg!Juj>B%x)hkFtgJ48Blb#lNSE~ow|jg1Vts+)A!sU*OMLH98=!Z{RU+zQ=}(TbNo~RZVloZA!d9p; zHnGM!Jf3-2Aef|BKEtqq!eaV|4p?h&((bRL-m*y@hkP7$PSKYagvo^_f$Y~mOQW`e zdqJ1dXghyw4oij+0KBt6^BfPZ&x`qF573d@Os2^bR_r>8=lj}KolZ^BWr_L5*ASF1 zv&xeEx{Zy9V+2J*HtAFNgG`3n1i?q|wspWr_K>ib6K;Or?RF7sNDzOc@T3t;RUi}_ zk%Ab+!}A~}1N-H(^O72rQME2hSygM$YUo~VrAc?__2C&m>9`>0Z6!?YezBi?C%qKVMOrPjW5942+-Pb3JVvqw!Oo-+)`Vv552$R$s{I&&OPi&686aL9^IO=+9HTYfu8F|ITr+ELTk=-gjHwXVkT zJ`MeiBD}tAYtO}8NSEP9pchbZsC2AKN6@!pZq;j)`;)1dGx|H0>}g(XL$1TE5q3o` zYx|Q4?`GvWCc9_R%byZrUZ?IGUSvh2F-cV&%sg*NeCH1L|$Ml-2s-Uy_kyf`mtsP!hmK)#u%^K z+@$q)G2|UnvvrDo)w!s0R52&O(Y)|Xu>(E9A9PueULkbtUQ&E5@=vF|UPDl<>W30y zkryLR!GPS$U$>+dI=gt489{3GJ-mkxb5N)!Lj^_6rI-0a@5Jokk{`-iHmA}Zg|;GS zYs=WNTVjOp9GSVw@w1%0AR3Mx3|v{CFZ#8#EI_`+I3h)%=kv%-e3x~Y5*Jly_4DUX zyd`n=nY%j=@GOq<+`0Cq0*3hFi=Ij9SrKt@YbDK73IuSeM!peB)Fv%XFkto_fU8iF z6MOqSfNj(mFO-o=N07UaUGbEmfreTTa3}IW&b37wfkbJ4dinaL<#2DyFWX0RNlaz@ zRa3jF@OFN}#=+_@Aq+g?nVw%?cFgZz{DfV~`QgKjM3e-|4Sbi_w_p05 zp=~^x%gdvlB6xgF+}6g}3|ullv)Ebs>=3h8@-OFxxD6H`&1x8bb}*}3q0IEkpytxT zC!@4~=%$Y9=|uuH&KavYa%Y^%B)@$-@v&U=3FVZtZhpoCKg`I5)N7dzJsV5+UQ9i* zyDPdlHx}^r;A+hqH>N@;g~#d$?MNT0XrrPuhHcxbgcq9-Otd1(E+rmNtJcQE*An00 zZXng?TVlfRo~q#~Dl8e z<`1g3sX2jPx$Skw>!rL;Q7ymR+~Y~LMzb?A0*mF2)f=)1r6JS?SfI zHB!eW_OOUrcCW##h4gdp;vcT>r-(snsnw{8fYi1BlMrj}^!tCH^6K8%`}6=n9Ta0N3;1SEq~urn?Gxd7 zvW?o;{mP{jk*OK`pixLuieSckVqK(Ro!cYA?kxED90EjiQD2uzZ=SwK_j*>pd^Ir}gqvT50zaalAgs(kX+vKHoduKioWbZR z5dqeT4`=P2HP{2}YgZ=>SQm^uyFFK>^xUX*kDK7^{8J^j;c81?6!<*2K70kzeFs}+ z$_9A2l;t=_8`g0X0TGeJIAg4B)Q6)>jwQY=G#C=UR0)x+v&{-1WdCwp{fJEDtbxp4$!#MQ`TTei*K ze=e^p8`C(Z`9H*5xjv8XA7@F_%2!KI`T%b<|00HNT#aJ)3N@~uMBB8`)d+1D@@Y4% z`4RJYDMQ2FE_0zR$I3qRD?H+>>K$ce0f8>RbF|bVGcywuMRjJ6?jh}pJJk*7Z}@FE z83LZRk%edmzNaC_1dm?pm9@#Bp?~D&)!zvp8$ZqZ4(4%Wz|8`hrU31i`QG=GUO}y6 zncHS|RUSm*8?GJyNVVeqoVJ@?o}GIk36kTJC$PpU>!P8Vi+(gC4CS@Q8Ri{0%BG$( z7*yy$u5f!G>jbBQw8QdO2ztJKZG{>O&QzoWLM%o&L>XmM(CaM(@~@P7Ts)W`!68Bk z#-ZRx!WCbUFGT;jnc zJ%$sNXFqWb)_-BB3aO~OU`+WVu12k~?fjgU8pp9vJ=wlS*>CR~#PZp%55)W$3x|XL z9{4)YJ@D@qJ8pX*E4f>${@R=0*@IKioWYOTAi}9HTZft%c4a5U1f`K_7{4sw%)y?2 z;*5Bcp6rMB4dbYKiL9=|nCA{>@@@=kEo=?{<~~j>K6&W_9(Gu~=E}m`ee}C;7KNwS zYn?U|&>rum*4~f98bJP0zd1ksKv@l@|E8?_pP+IlwXs2R7|F<>p}dAAs&Zkxzajgk zcZP2a7V0kJl(OT4(qfhq?UGU26ANutwfZ8PUCPI3X}j0^n9nPi$3(eMh6!0w5H?C+ zaOIT(jGm)e480e`8z+u$FC_Glwx)Xujwe$a)RKE$HnGT=E2m(EpLXDwn0l_3+4Z*c ztPs7YQ{doSWwZP9keMqKM>bAmuxmRMhED~gx0;|GzS$dScyM%G{EjS1<}5@S^1RWp z*5;9I>H+SW(tj(~AZoS3erFB;#91w!(Rl~B6fBXiyOMMuIOy)hbNnscm%A1CF9WS9 zBZFtA`f*Iw9t&7Y7UM$I{Oe=)y$)72jW|YXsTj z#vg-f)X0_%qp|Gg2en&nF%7~w%fF~xSYnH$=IK;O=X8@+*fdRMe&L(5yer*)4L9VsBy}sG{);i{62<;3wFFFw% z64EWp*(C1vbhZwI5@x(?GA_i7R&E7^?jjz?6o%-%l-A_)t@k(fbYZwu=Uxa$TvW(* zx*vBt;bEmzV+m!?9$@dZ6^#R)=`AUHJ)vERD)@zZH96suX<+-)m`B&rtn~lPKKE-E zQTL=2yd(_?7g&4jM1b!tH}dgXw)-8_7BFwGGM`1HY(26D1*xhxkR9*58!J9bEj>~Mig zedTh779#=QhdSoz_l+!Pa=5h%wh?Jq;D#|k6f~##j}xmzsJI!^Ayxgqq2CTeu}pc5p0-vxW7qnv zYqdF`H^bIV6&0SjL-?9x+e+jp9Qd8mS$TTGey8o|H)}%)mj3^3uK867^RhF17)FE0 zzxK;rGO~xN0z}QF19=8Uj}S?n+}eoQVP<;ieTh;HtQAAcn)I1n6Z+KZPom zx5O1@r=GW3UZm6{TCKq|&+5l6C`c~Ay{!kmJ4(8|me`v~v`iVGsZmfPrdCv;bt;nk~pI_8V+h9jD`A_aKX3%E0ibpLZsXn=-!ZLX4NmE%| zn5utxa0z=yp>X%rk%vQ$>5MOQ_$SA^;|EH-9WtyS8;n$8T{GC5%&`t?So~Q$X0m&7 z#qJiW7qz#(TXpVzyjtq*|9&>2aiRKt*jYXE*8#7;%_?CP+ zJX*k2lPV@wvO4~s5%iqVF~mDA=MM1*5xq@}l#jcA2NQxEyTetxHZnafdofy$kl2>7 zq{qZJww>2!`YwrYe3$%4!zjhW+ywX_Xy>-IpNw z<`&_4(d_s~l|2=MtNmsvlx0F*>g@k7@VnLzuH=aqF+GW2&eisbFWMY6Ob*F;aX(FW zVNJ5a|84YrAu_nh8${Y0*+*P=X!=Y!M^9-w7%YYz=1xa7&(ahkWu(;YzB&3~t=rgZ zfbcN_%B)owmnUtZcrK@adGEYkgNaZqQ*jB4MTtWHOgR`%WsV8^rvsxG1h4ZB*km_O zsHO0p^{pK)kxv`2bAI9)sO1PSIBj$=u(66Kc7e%pq0g~KoNvE>LIjgzl|u?t4(%#8koU|R0AT`*IQy=W zpCJfRcybFVq(2y^UY3q`Y3gidYK znqT%CoCaq})x8ntx9--vyqQQ5KI*3*wgcl8?KsKCZ?+-tBF1t^jWCf2#r@4T)Z2dB zw1rT7I=&a1>GlZ{r0pr%V+O-E97hO7Zm#NRZf%YG$n>JI>Ny{zYrtzWC&*(-f`nun z1?!ER8|i3Wh>M@((0T7GLXwEA8Wy2Mll4O8>#OM^Rc3&;!>#c^c2}a{%&%dXap35iGT^K@Cbv}tA8T$A|z>ho0 z?|M>qvhFnVDO1yZVYen#dAITN&j?C#Bm2w9Jnr>UD9a50?MI^o;y-Q7Pn~aUzd?gB zq%N=e5x?!SS7lxrp{L!Ue4HO_>+mkEFa;LK zF~duP0^w(;1h$x~z3=cU(S>K(=leoy`=pCV=XPf1)5xGCGUhMMAF;LEZY4IODRGm2WC6_uPA=i|E#skcW8{9&zF-Gn^?NxtO1>6;@K#(IjxWrzbxF z41*boRy4q|>vB%q%V>A;_f3tOIZc{lFqHwA;(v2+$<;E`wmE{BWfHRbyi9RhQ&V(n z^Ik%vZ)|*mZA;zUZA=gYp^nT$A+JSnmea4P9H1A1#H8!~DbwGgUfl}RX5q9vYygcg z-@4l83`70JLx=| zaa@RqE=BGOV4VjscA~s6$r~BD$y(L`b<(r9*PyNiolr0qI((^`&X-rbnsM8^m;0_%AaoKVgRbD?4LPmyAb-vGMKb7k`m?h_z8MH-7OD0lTBM|B47Z zAcl+^qHAj1bYdOdMwis<-_)u_OkKJ2n}Gvtp@Dk1zER`Mz$p`toyD<1Oa})Qn^}DD z8u(k(!gs-q<1+^cMnMGd#@vFfG>!>Mzw%aNPi)uTT)%};)0FfU-c_sHnt6&MLG;q0cx``P}@ zo)BqKe;A9cn=YFm;q@03nxS9(Z&6Pt~g$C z<>JNLjFuYc_i)Gk81Nx>HpN+5mx=cf2%U*z&6tVV!I`~ayRT3 z`nUCVC1Ky?e7WQ~ClN|C;GQKi+<3y>%D>8Za90AJ+@xH4WMFIy3E7A4Am~W9mx5$$ zFkAQ-nuSI2I1wey6uP|={IW@m$*Z3@h{lBMgcu}5H*c3!@8Ck*CFdY* z(G0)k84nd$8zWYyAF`);YC!Yi>$1Vc{c$E3`rEOSWj?a8|C&)|oCFpt7!)A=HHXFE z$w7z`+)LGBN;schfjgLUD5UqLTk_~e8U6C;R*{8s8Xi_p8J|RMs|rYI8I(6NH;2*_ z!6^5+D|N5R(Wuq#ow5Bk^%E0b?-9OVi4*@%^~N%$|6l2ii8P0~F?3Y?I_njMDqefc z5Fy9wk_?~N=1<-ffke-_+F(U}M-HwG$PL0(m`n)XinHU*Ak4Qk%4SyIFMh;b+DX}E z_5NAvK|94JVYdbqiM@0$U;(8?@@6#4w#YB;&n=rJ zE2D|%IU?%lA+z{K(q)sx@B8hDjvu4dlco;61Xi*5B6(78cku(%b$8jPnE)K)7KX8gyp?)_0YxH6#TO z@804X;!~C;W$}MXJ}!eXkwm1T{@di6g^5%JVKM^yD-Xh3dIvc_mmiKnHWPIJAUSNj zcu|~=SG!m$uc_FSA$?}K&j^E1sflCc(hQusvsZ7y!VQ+~O8x54L#4kh%~vY)U?!Fo zo!A;at`T((-1Si=DR$V2QZcPEQS&A3^_QVkuXS)L%DQ;xoqRZV=x&c8mM|tqA>jNT zpcP^)EHGnmd1c_UZia4wI(xFn8JV@XuLru?_C+}yl&I869-BnSZ_uDBOV0u2eBH9z zfu!tq9Gv-Dte@;A+UYVDAVQMACws)XITwjKeOR6X)$^gvEq9$EM z8!~2Xv=j#&wAL7^89dfd1v;$i@4bsbh$S7-6RY5B#(qgPr8a}-eWB1A++>&$P51BYQN zYv?d$Lr01L$suM#H;gsJVy+*eK#BC}>o31tJ$!BOLh@Kd<9p98!jYA9` z0|nJJ?QEDJLaaIs`6ke09%!xkcyuQ!>6q3`jjHbTXSGti%?<#;fvUZ+u{LM4amc|S zu-adKZ?|2d?>fw~{UM^3Zb11o-aY#K!(Ugc>Lp>TA#6WR4MQm5&EV|%suNkC(3&S%X^1I z*RwE@oA&l0{#_D^1FX^lpee)IDR zaS_+VyM5&jr*<4Rl_3iw{7>$cv_MW=Z;jk4veShY3-T@S0@5k#sR`0m^%G-Y8h5CL^ zg~4H!{E4cX8Aj_JS+QRZ-~Hn0@npv?p)1dlPds?=fcDIV)fq}AR)Jz%i?5s&ziGJR1WDPMGiND!JS>+=u#`*GAsHhz+B+A6R9hQv1 znYGIuk|^{>rx4)5N!5dng$IVcED{_NN(_l@)vLv&87sVFJSZ>3xb z(!W^uwdTn1p>I@rbnk4 z`ulv|`AkNq=z7XDxt*IpbVD+Ha8;Oq`E`NEwy=yP_{2{_mmlx*bTD_rnzpNNCVG~=+D!>#V{0tgl z%^QzQ3tW$`@g=!dWG9t28x&RY6ml2(uRqun5jMY5WqX#k{fM)0T>pC^?GC(f8qF67 zw=W4tUOK3{yM0VmMy8w-@7n3Gd2(v`f1}&vijpkJ)$lgH}h` zNBFt?)z|bt4z{z^oVu2(@-?PuJtu<15#x9?)w%?mX(QWF3k_N#6{Y`LZZ~Vi2(F7j zH@Bi`!jU5bs>B$^uelp>VqQG(05=du)-D{PcqsJMk8?*sx4^I|QDMpx8PP?n8bpX1 z)g@*HJr`LV%8Z)6b}6k4!su!P>*hQozrjtrc176o zTc027WaS$j>(SP*VR|;m^!(DpuG=G(PV_g5)VFCgO#^VaT_W>XbJv<(TB0d~!gvnT zS`+hjUVnU9_578&Cqy#X09*Xs=T`_-Izpi=bGCs)+^25IO^17kGU>4%rE40FU8p>X z(aMlR1$oBqnFvcE=tXOJ#i3qxH^%uR&D&YzvN178I96Zb=IQ_; z{-3u>1n3U@Ok*&9(z}dfj>`HB=SKR%zi{DO^P{$)BUyb?a7*=K!o-g^RFym5A|9d+ zVNh5TIDZKq+h{b`wOQc_g$KKT^uOJdXG#yeDsN#Ey9aP+g z!cshYRHf(BgU#cN$A<$nJ;lsQ%wzH5hqoyi)&`e2Y(IYTWVi2b>c-`_r5%+`k}?=o zW;CtV#|JtV)Ni-w$&XG-P+EBXI}i$cYniJ!Bx+=ErRpT}(navxqI&zG#Y0)xWA zXt~xT7|>SYGz%SC)Ae3`;HC`eDojq5!=pXk8wb3$3yV3$?0zE6m8MtPoHbBYj~cLR`VE*IyJ2whjlsh+y8bxj!mM zHwxE%pP7U6q)v{Re}vs0UKiv#{j2XLSi-ZPSew~xZ8iBYm(UYcNkKNR&K)Yd>(8oa zpQ_%^!^6vcMSZkr)#Aj=Ktj7SgwepJ+pmrrP2dvj;!+09c0_c9y$dtQ#8HwdA5hA9sU&6z4= ztoxfq*m3J8n>&mADx^AXgJ)0R!swST&tAQ{nal&u-3Fg)UnEU0EQr(+fj>}Pzeqph z8+x`FdeDn8#)IyK`qTnh%48D5NVxt3*Mdu#o{|@=*(GGxt*Q{n)5xfM+A=?9lV5jO<7r4 z*@q%=V$lbtAraQbe4ZP9WZT%^qj5Iw@AB-lijwF&S(7*91vWu=O#Yb|uf|EJB{7Eq zLoHvK9H{%V%b9IdnS9TJIJS+dc2O@7^o~J+&*4W3&4cwa7zbpK1z6Z;6t%(t^)a6n0VjlAWYfD~e+u zBnD$Hy?Eub17)P*53Sx5U23V-V4_OBr%KC?>tAm; z7z(C1$*?XY1ZE7mzz(+G^HAhKj$3_8QmmwEg8p!ywMNk&&cTUTEeWLDK!JeHZh<(; zpe}j9fMe$Q8+$+TfH+uXzj^Z}yZw5P=NH7#g@Td=E~;W;=3=t*{;pN>iuk9=Z|D87YvTi=HHu_B5DW1Lp)sMW!$SFKJpt->;J+k>1` zI6(ua_1m|{TWjS37b+SvKKmwIR9@bFznK$JyEV_6MJ@UMm*)m8PLCQbVaJi$^TP{M zKBvAYdr`6@lQmL${40@!BF(mD6V`w0WFSp8R6^+meQKLv)&z6vw91g%uQ^YX9Nk)v zu-g7R)h6;3_N2jNzIw?CVCa(L(@&D)uiZOua15}GTCzs6q17$_fqhn(-W;k*G+u;QRBi#z99coz41r3WFwl)^t=M~D`L zak}%j-}QNw5hTT1m?5O>Xm_3_)x5-CoT}!>mHPcr7;;QJl2kt>#y49%^=P!Yq~P9k zdbw)4eoIhuYPP`%pQTq=oNyFgv}~DGdp5qic=?95ieE>OR}haYafi}tW%xe4%W00T zl`(JOPgy&}vVQ5Y@-|)G(mUXvY|k1l_xyfC{NH8rKYo?;g+J-9x06+@YKRFg*`$+I zXF6?Pkdo|UWvp4QA5PU+`Z(F#tzzN{%_+wW&UKqadVcC$R`yGvTjkh~EKlMy$p+xI z5LDCEq8%gzF`NB<*fB-g?(P>9TozqVZ_r|o8WNWuUO*|;IQCZ!!4z2yU17GP;*oOV zky_@eLBfhh@V{_<7PmGOc5{`LWw10ck)2oCECYV9V)5s1?otI6?%N>lCwjPqHQCT) zN>0c&#YV#E1=emZk{NII?TU?cwM?$i5>m9FeLP;mTj+sn5FV*9wfsRf^SLi}3{Q`J z>f_EYdYo;ZZ>#TaQYo2Z(SL+Dt?+{9(2=;o4$$8ERs?4ZA=yy7r?Gd@?~#@SP?_4c zI?<91d;oZLDz^W`;0?lj@k+*!RqzV-sME`yqBgr*{V43Es=r~Ed&=ZK7;*CWCBCTa z1zz~I{RVQZm{zL~-#)%@Fysm%h<*JFmSgsV?xp&h>s{sQ&PnSG4_J?dUxb)Ayhbd% zt_NHnLK!kJci}>o!Hquv)n_QSSC|s=bUt`cQ1P-zD!?eP@29tfPl|#=MGX!x;N(ol&?MY9X;E1V z#h;bKj)!rv5EO=HY=E zG0h?s59{)Z;~h<(91b9$kcfdQnUAcbiEZ(T%Nr9x?D}|)_f%s(9lTzuHD$1VqwCFQ zo_oLxfMX;8`@K_sFI(x8={-NPrf!}+VlP3R&Mf^A#<_4fsi3TtaNL`ogUz)_on8jB zkxXlrMtw`|(b7qqtf)+McW@{mBVX#V8^Mafp+RYGVqlO__Oo=9rsCimBA0L53-Svj zc9)H~3s7HyTWjp*#>PQcfQ0vMs%wRjz~&Fewd%WK)tqFxEI4)SyMvl_Lr!shNDbVq z?-_roHj}AbMy5T!-f7$8p3CpAs2i#Ld`>^nLVmpdrQ%miAo&K_7;TNBozURONU*Y~ zdNLE@EjT*F99A8keRhn{OMYelG+}!FJQIL?=*!Zkm|8fAmAzp`$DJ@{I4}xpUN~tE zx9g?icGu@M7caAe6Yb~XvSB;%@Cr4_Q|gnb zDJyHnXhn7$=_b>q@GcASrQwNFsAAx77o6!d5PvDB%RN%EffVkV`h9f&JrKH=k-FR` z6rX&5r$D*{%e}ptj?>Vq&@%BoPLDDpYG{U9vu>?e3MQDWK{f=-E`+nNgpVL!S_x7f z1@}ych3ScH4JOw|T^uRO?!CJWV~|oVy-_nB9`WdTxw27``|`nq!HIu-XD$8qoZ(tQ2=0c5oOVPodmuTtNkgn5cMqeC(vqqHf)wh(@hQH8A0A)LU zn3P^=`jGWpO~1n?`hZPWdlJ+*^_{4iD3D&Eo(7Z+ISK4I*D5yNsWTh97d^Xt(Rm#O z;E_gQg2)35U@&?GQ&o)(f`CXQ}p9hT*i^Xy*2P)JA>+PaCigi81NTJmpV%>glr8DOCW0Q zbDt}=I|s--4DK#3dYxDAb2FGbzlrkcr4Ge=ZPNid7T<*ot%Q(|?->itcLev2E4-l( zV>=`HBlZ z-qVD@goGY^_HoOE`hgeV`kS~3%ps12G-{Mj*V$d_>CSlv;Vi`0cfW!ob}07QRfZ6C z>mr`zijC(mh8X1Suo^Y)vYHu)N;dtJ^6iHgr^1h8Ajg7u##d0V-MK{wY)bqV>j>#_ z@MQ8k`v!}i%CS%3cEO#^m;#ynsQ7TBFoL#D**Qp#<9eRfFClXGC2Ua#GS`t_I@he& zU*Ez(#|YEXx6RFFDbbEW$(`=bZR8NSlz|IrfrAHZRe{LEck`(V%a${LW*iwN;93>N z7|mY57s1^}NhcbSN}>+&8cIpcxNA9NIxyw<>*pv@y>jV?Ao0*)-Ley)^(kS^IUPC-+@-mtGrCr7j>gCQ&{i(OP;@6x%926Q2t){}R(Ur%kAX z&%aF6%V@Kos2mGU<28ZRYGAbq;f_+|U!=c=qwL`^;hE@&2-(L|`^dfOA2qm4!!q6w zGqxK= zqkA^o%lQ2cqas8n;;!T@-{#!Bku={~K67DwnPj9A0rY+5UFMg~l#>aCIV`XWG%M_O`t=x6CM zuxUcyu@Rq@q->|E-;G@^`o6E(RL{y}>gwZhZDf{2crbnc{+(1o!kgW(EVF2!d2wmZ z=-UUhT8l>Wxah8z0tr!^dC|Hew=4XPTXQ4pB^3OE(PMGWExH!F*7bm952t;nd=s7! z?1<Q?-!3&Fq zadax?eAxM(GQG`a6M7+bFk=a99rwxkC$P0iBH_5y5a;b6Y{Wn|;;=1@>}+@6czGQM z&4wU*z4WO3^4am)@ry+})yW9pSE%-SU+eme%m&;&srzmjEYmX}`27QfkR;pWY-^;ap&Su2~fO&(mB4th6o zjl0B;wt7othr^8iIr6ZRa_7s`kA&I2>lt?Go@D(Z?TYrI$E%hvBVxaJWaVXAdSuRc z_FYPxc=)a&NZ^C%lM@kspN+EKDyOoj?OgYiZ~^UU>^yQ~P4bCLJ%v2aVVm^s%lYbK z_v7rhuIHxa7w5!9O>TvNI%5c<@eoE~`t$-X!e|c^Du(%cUmk{p?k>$Ud8ck2c)Qp#z)j=6!L)4cae>>RVfhUa^K%;w_|lhsLE^vpDy3HcZF zS{#&9K~DDhrp@u?oF8h-3*Ysh>%ZE+U?#M#SINciYATxyh;)w&Ph=}BKlCznz{%j) z{_vyw!-H5}Ds5nq9h_P#*FF15lzKvAg<|7luLVyCNDxV(jNlt$c#HZ5CZg3@*JX9l z1q8(wgeler3ynHCjeCGATJ+VwR4chUPmquL&u_D4-jy^s>9%d<-MSF(_g=)%^~-E@ zGNQLva9ueZ%N@P5#`WP z6}HM;&Ge)^?T=61J4qot5)Uyp!UUD8J?>piB zLwma(l{>?FFGXFGjbDo~O+R@iXw!O*@Or-w74qC=sTIV}i-OH9yJ&a# z{+yg(_Ej(UioG?Q`+WBLcipQ3&SLZK z$6e>Kq@4`-igcNF*k^ImMR(rxqrMFnxw3Juw z4!o(q;BY7%k~Z~7BnBoipvd}AokcA65w7uWrN|#$dVJXBf5c<3f!dlhJ9m*Oqu&1x z+x7kr*zTl%e5bpdKL^Y1l3Jl=d*e1;`|%5D&2CzLayy80?#^UTB-_+B*7 zp$=90p3NbrY5gdC+*6WsIpaBdj%BK<%Ro)5-bi>@PG0bTfds!r`TuW7@XelIlS5}a z4<>^#1jRiz)2&|Uy%7`y4nud{)yMsN9!UAf9?l5p9t}yqAH$e$ZQ`EO1WpNBy#3j@ z5WCw8)&V_r_jT)g6_{do{UvD>@cOZU?MEfIi~7?u`N;C<(e4lHoW!In%)3p<1(b_h zt67NT0-@Z=>1%F-emZjI=81834>tk^+jozKr`hh@IThz#PSMjj+nj$MDYa@_TN!f* zTxLKRi)wKG?OwC?2kLvrG&;Pf*%!;MKa_-T24T2E@5ucBgakjfJjv0NwGh@XF3Vr` z`r-Qm7Pk7U09t=*D&W;;`nZ0upVMD!bTqobXl*m{xINmuA>5L%;XPNXSGobiGS+#fQ1%|1?D(Z@Z79WfBE^D4Cu#CBI&+n;roKQl+X5SbVlD>OijTq$} z-Ra5b`uV!&^iM+;ZX9K!e|;>n5xc6d`2PJ``;!R60yMu!PtnxrudoANJ0pA1#^z+% zdOlXhnlF{*@hGFjU>XQqwl7zM%Nq?=BcL6KifuN`U>INectva0BUPCHRworD{SoOED~dEVk2&A69(rH{{8#S z*wndDS-H6A+}`#O!lMKM=F=`=9o^k_@gG+Nvj^OgZLRw^X7}w`wUhrJm(S`SsjkO8o2_6%&GEhdnX|vzDNZGMy zt6=EK6GO{2r6&7B&7;t+J0=}1ns@))T_Apt<#Ppb9q?37zHcn5C6_ls&)!tLgP4j7 zftmVy=+0)p-H_qAxIH(nYeM7`N4>i=-d67}{r(A&6#eDw!J@*#8snlbe{3#Elo%u0 zR5{jv-fqX{Fx*+flB}_+%_x=qP>U0)?%hH1p$YAsUUd%JcEM$`MsPjZI=xtl?JHb1 zpz7Y+{y;wdF{nsYKToAcOwBb4ug2|8my3QVEd?Ogr}%hg-ep1qb?IiSc923UBP>rG zsco#;u3a6@+PTHxsg>CKzoF(NEv@tcv|3^NeoEk54pT+1J!V^|I>%pokzNmEB5c|+ zInT|RXK(MZ)pF~oHBJVWrCjZ%cL@YJ4ESbiks~{SQGKW!n4r!wSH(YNeY+u@p5@4p z(5~2u8UVT6+Y84NBsIyj{u_1A%u%+ss`=q>Ki$KWl?27`Z(kClwT3BAn9yQP(<@km zh!wS)k9+4X%HY=3R_hzyRmCtb;nrljiN?7*#OD$Zi|twQzkjiQy~s8!v)_-{gcpDP z1@VhxS1D)4_SN01X@cx2cuoUN6|fTgY{o1v42 zmmE)Q92b(7UQ^zkxi6j#O*-8ulYj(E#Aw7iE%Q1*Ilchq8}NpPg*xV$ZNpHBNW~`p zLA$Ie$k0SAb;A-K;>TF7KzAb*v3tFIlG|jIy4mXfbcQ3MJP=Z1+!}h}q@5wPA2CD= zpsY7QXJ*PHV28j6snw)Qr}>kVD9lJL_u1JXYeS=74e;rQ#PyEqjT8)1|LS4yk* zWtcxn3))*#tfcMHT3Ql{J9vI-+!IS_s0~-nqf_7L^D^6p3Q9!(SZHnkIbd6`*2o?n zgV`!DaQj+v0YF;PLA1G()f2z9c+Isy@~xJ09H#@!mXNCGsZL3kZ!uv_8Z4=p=X#w0 zO9e$j9cD`3_o_Dv6Vn@ic6*i2N&vLY(mAOpFDt9C6=ziUt?+92tb*m|+rbvZdXenG)2=fu+9P&C3!rBt6h+dz}9&wB%=aIoya6K zTE4$)k@X3bR?t%*>Aw4w0dxVk?;Nna{{m5&;5bArIYbmyAFEy z_%AF0&SY9jSi+x+U?r}N0bMR3UWma}qF=L>D!R2sIZe= z{WJLdbqEi2ke%_welC7d>Hh+$n-g)7*IlK^GH-Y7TMs7~aDfsLUvLbBg+h4UvI@WN zaKi3Oi>@EIT0bDvd7MYCE<1{3!oAC&g&3@ZQ;IGMKvah|=`j}3M@9Nd&0i4tPo1E; z6I;42yHMyYMnm85?Vx*LyW6WhheL+U=~B0nF_wi5nTB~|?=dZ5#sxeRw*dt+< z;bGS=wbW6M(TfMiwz@zo?v!5*#H0TRsdp58&8I*QPW}WTVv9o^8jXT>`@<^$UPGr~ zDvfTBg>Xt=SWs=h{Ewiiz6n=?+KCzx2pPsDG&3#XKONBBr?hChH;0WO=)G+l}e!8SMM8IL0%OH*U-S<2Pl(rW=yy8Tl}rjmMsj#^T(g+QZt+OaoE zPP`V{OS{+Fl#at|lB!KU#j}&HMloKka4;nmp(C(y7Yd#SMlA7rV^#iMBbN z)W0z9x$ls$gA1v#Rk*|I+dyKpCmVz|f6-V=HpsJh=s8&UnE4Y#tuKr&S1exny8(9Obr5$#90bP-Hz}5X?lodJ>!& z>@(LvU=okywOs9r)loaTYuzKuuny(oj9MpRg_ykGeeCwmC68){u+uhk{pVYbNK~oj z4F7cV3>Z!tkQ{x479DHSIZyUovGG3Syrg8ZY-A&&aya|Gef+}0kMc38kxW2dJoHa-C>5_vW@9HX@kCFobHQs34S1N7`07g zA262E?2=7V{wAU*#Y59z>V4grIMfzL&eNVS-(M@sPFNMM*?H)5fs+*;E@%dH- zwsxaQMYJu`O0*_d?7vMPlofQBa{WrDLzY_IIG4b7daeGmKc6fQuZmVBr%U6Sl!7=i zq(GJ}J(1W5>fhk7FFe6)o_!i~A$mhO(Q~h}Pafqy&NQTfF1$PE)mH7i{6_-wb&yjZ z(3{nWi|S44nzPkTpR9N@8mO9Ho8uTIn$rIu{ZZEh8#1xP71qglgQ_fAGW{99Ubr_$ z_usV>5Lm-cuKYunBUXflk=B?UQZDRW2>Bq6T|1d5&AEx?6H)v7@+NJRU zKghK|1INsmu7^`m5$O&97K&5KX8L0}k>IWJK6yE@E?hMH9d!t1pW5u2U(spPBx;s6 zsfmAqd#yI<*1Mk2tQ#}emKthHnrJ)hS78K{lW3KoSipLEXvv1Yk9^lou42!XJ?Sa@ zjJ&7(ria}uw1tL$de2K)HMvIJym?&baZGc+22Dt1$^w7%q%17=x@@K zP3l&6uUlJM26k6fRRt;~DoFQKXWKv7NFux`|3_$bq;;eI!-iT%d69I_TQi5g`Yt0S zDn|!*v;3m_jHdhwlKa%h8{?Hn++~NZ!HJ%u$gHhYVB5a|za}hQ(<;fg{An_amW9a~ zMez1c3D^3&t&u$c`A*pKq=_ewQa(-Pq()8JR8EAlGQ7Kub!dcMIdTQTr+etUS(;*+ z!SH|#6C;gH3aVCb*{_q3w~KP~)5P@AiRKq(t)mQM7jxGQxN;k%?N8L31W~#t|Ma2s zP@$*&+N(6Z9@PKJ(H|!Z+Y5oJhn)ykD&5~%6qi`A&yjlxhTiJfj@%1;USd2Gn}151 zjrI5OSML$OXw8q^#y! z;j`6|v_lOi2XeaSe-vE0deuBpjWcHI=Pj~_^uny;w88QVvKNF8EmuuhY2}k~12)za zVW;%a9~a|5#E43KmuP)@2sxH%CJP4Tt z)Dr|k?B`_v=lh0+k5l&sCgP~5xhg4QkL?@fo7*RXlq%V)Ky5KZ=>iH>$3 zulAj6^_fuHEtVApdQEt}*7ZvJFSq(Fsp)lMYgzz&8iyqcIu8-UN2qNKwgRI+rC0SalPsUvbm&N$WNecX{*?EuUc3*MpIEc5DDOcOL=3Oea>J0o$0EX6V>0U z+hw$ePWMKIi^g9x*F!#AzC!M^!nZ%8E7va|soN;#3@b$wBK}(OW|-_LEn_tB#{hJ7 zLFN#uqFt$MOik~YPJS@tVUD*O91#-1a<>d)ykAFM78BKbCW{R>0L=cd?_jv4YBryy zQgp~Czs4cU0r%#czsBDw@Kuivis?AtwBP$MVg^So!cizDiuHGZUVVXJKWKzHKWu9o zb=Fir6)`K7V&jv&HWB}z zah!?J@s)Yll4|u{{{}2;;3@L<`zoi&*z&-at80*yHtD|GV)ulGRM;P^*Oz;KY;uHy zPVA+>Z};di)Ffdd)_v}~?XR}Kf6p~^^`EG@JSsLT;+t6{2sk!EVmb?m`?*5cX7Yk;{w2+0r+_~ZDeFItIAmCG*=RE`vne?w839Jx?p zmCUC!yPcXX$^K>nnNIO}Op~teRl0xnRS&BMb%XF;q%svKoK_Lz`5-WatS{rujoN-S z_8?*-?GP>EpRlDlHRm#R>xQgCi#xSQg~Mc8i;o+mNt+fXiCeuFbVC>6#yggjU4cnc z=Y_)SK#6J*u47H&U`Jb+##3|gEa%h~@_`Ur@$A_ErAmJKC`-D7fqPnU#%+wJ9heCH z5ukJ9G{4S%|7~Fj)rT%(+D8qY6LD-UE%YSB^nxc}k~Q$t%DydQFWrtz>!o1t6*7PG zfZ}n}xY{Z_l^jwu82>-xp(5MY=0%+1uiCGz&g&eatstFh9H$m}xxzI$s<%EV*m(Q? z=8c+Z+@Hjv0ewaQspuCFZ|VLqGY5wMWAV0^gYNrSY1vccr)rzor(-0rud6R@~cgdwitE*;uH6LOw`d^T}n4<;E9w5+LUA zVdG)(4ND;A`+S@Z5Va1N1#pF)466*g{BRRy=#_J(s}mzrkmBJ@)Ln;)Z=V0c{!cdFbVFJM*f2@!^5)Z0JnAu-m8A~ z{V;BKP)~6oP@25CJIPGglmR`cHBM5KK9l7vb>67FUDMj5VF-7$sjE`tz>VwI`2_^d zPz(We?hhxo8|XI*1E<>j7Em{%1>*LRiFaxYESEzkK4}SguLWc7P2Qu2{pO|t3>ibO z8#@-!#4Z4O!7L5St||j7%(g1MS7&Aq!HVn3m+R_6;E4%isv6$(q}a*LcteAUMm1}Z z4`r77pYD~m;=hull`$kgv`H&98GzgI_#tsbZXWwEA z@qSAWD{)5Pe@hS5#xp7e(z*BuTLWn0_oL}NXo$Zb|Aton|Bnc}jmvX^`>|1Y?Rc-P zrus%|Wr}Cf{>9}%oJorV=lUIro*M^9(Lr^I0ZOS8A03LCPMD^H7gA<-eG;c}c>xZoEvHE!^dBI6E}PmfkR#(@?t`Zynq3eTW<@2(17h#YGln zq!K}-3_N}sY*cVKNI3&2P@2c$=88NM-Dqx59z>9u|!ED)y<69 z6zI3lw&#b|OaN3QfZ;b$QmOmseaVxA*Do>n8{Jo+r@}IbxlWE&QeRV?W@J51Ko0~H zQ(`Jks3^dAnBEbnpk$8=PBczWqGc36gEQp(#(WY21vGT??YeE>+aV+h1Gwt@r4yt;o_s(0vlndhOCM)#Iz&Nw zs049Ju@R^)dqGySn;h1Dt_t?X;}4Nlt}4Y)(!pWZxJ48R`j|6pC-oYI&!0bk`t<3T zo@n3?lzlw=BN zYi_0^P0f@qTy%2oeCa@)JZo+J0@NEtr?Rfzq+o~jXvUkad;SkXq{9K}==!E#8&`LE z)xCsl)?PX_KB{dK1fGqJ>e=0Jlc|`HPoI3x^^N_uDhWPC8&zgI*QH%?R>+G&NN;%e zESS>N^eOJm0qU~k=JI>T_kdRpM3n9cDro{rA{xVWH!V(_-V>EHFyRWXaQS=_Q0Aw4 zW#)}TFAz#&6N5(KMdj%NTm%a-c{s~yNQ+>~ETK8s-n2G0r%G>;-=VRX^oI8gPd(^{ z#DG*xfh8uZz;>J_po$hN8^gS7E`YA-N*kh-NH8yZfH@@yLo`Tkmw(tinOi&ZWAl&# z&?G>?mk1z>;*&(qzLhQwupf}vo`Hw0jmX5wQI#Jy_k+Lb z_Qu?KFmzjGvkAD4u-(8?Mo6gw6}7MUc~9$O^~|FT3ihwzx6t9-em23f$3>$yw`+0H zHF2O&++dmNG#0q|(zHBJhQb+%(xZXqPffmzOr5Pia=#dQ7X9Q=4~JlBs_1l*Fz`%`op) zuPYR@iS*!psyc7KhNE<9UX+gqg`CBAYvx$Gm&&ri$~p4_!!0FMzLRv9P||-Oc*um1 zV=QUD(`^XJ&wACk$1{Jbt}a`c8--1JsC4(j{7lWOUs?ez+yRGAYh*;YUIF3KGF2KPd2E5^8~7 zxE?;Q6YXXsjdxEB)YrPpDP%3(GuePGYLl1o`fDznkshjdsBSqy}@iL5M&{7h8(+7<67UswLV1w2r+dw7&P% z2?mf*4oUU*G);sHo2>=*J^CZTGK|ikg_xofa%a4R*lJ2rk^_+(xHu zV2QwtApe?92Cmmz$c9l9pv%?fPz%ooR>mHLW3+~?^7ksYIN|4uR``AARc`=OChJA7 zt1FO;uE$@(HdA-f{Gt2&^uTzD#j{DcK=p!a9P;eY*eKD7Q$Xx)I?2qFbTs!tq`1>6 zj;_ZcLkWF*jE64eS+|6JqIvqYpqsbcZ>NF?@K8~5Wit&NWckrVMu`J)q(4X=MEjZqOZ_FW_WLe+bO4avgH&#qrp0VgS9L# z6vy1R9aGXvnk#5UG%%Bh_6b&}X6t1hEyHc`s{3+_;YCHIwUBgFG^2~dH9_RdA{&wW z@;vgp+PCZ=;(dCCe5LKJo^;KA)<(7x=am<_Jql(G>pr|9>a>2v+Ip{f#{(WOuCG35 z&&ZPJq?mW;2_?V&gciA81MR}>yvWWu^G8GCkX&umUPVGhU?LxvVkQN`*-Q##i2aHa zV8rv4Y=^9&R^SfX*|OWa6Uj~ULSgm4EaSrv#8}whnEUT6zA^&{=B)MEvzqKQYnrUV zY;>J4L$C&uWFhL0^AAr-6B&)!(-`JJZ@@ln)S98m9(cMqygesC8q*iGL_JA*3ZqHy z<9l{ZK&p!rMxBx+2~`tTco9!;czVrh{xZ&(b`JyPv7l@qnWK1L{dCgx&d?`}p4LRcF<#nQfv)BJ_5XxU*LSc{@6GW^= z;Xju+4MZcQvB_zZRa=>xkG*3P&24WKwQ!jTO{mLTNc+*L4=$4_SF5ga$zYpw>fAJbXOBxf5TEI8v*n5w;lpx0+@cJez49b{h64K@efb!H57^nGcbT0HM+Cg0^e}? zKUSH^RGr+oKSbNQs8uSOiV&ERdlo&L&tJaK@ccMcI>|-?5FokzXz8`1uFo(3-nML# zk7^u(C}me5|7up=#r}1uubulAHZ}~Ty~b&`RA6tudc4I6lvsk{6BU8P7{q_q7gq$8 z+j81?2nN$=*V)bPFdI)0h&^qw8n~{Vyxs|NyLKkQcIqr?aX9t)`m;HIuCFo>>~|do zjUlkkjJy!s+U+CzJp1Gdi6Kkv^!9;k$;rug-Fm^8ia%?{8M zHsdrf;)oKQU^|TFTd*B>u2F%ZpO%$M*EQ?edBnTgQKI~*Gc%-EecZ+G*wtZ`@Qvo2 z2<^^JD2-+@vXZ1pT(&E zYr$rlGjz>?|AuR;feS$Es04ti)}0q*BW3oHb|=kDFP5GwYtfq=Ic+{U8gVJo`!gFKwS6;pl1D0N9BL)ZSGqaGi- zc!qp>R%@Z1s?uRcOy4H_bV-G1)w0a}lHgp_+a8IO96gk7mdpp<%cR>#(SS)zS(rz; zevN7JD(}PNW?mUvyzosfjCN(OrMZT^OP4O6OX}d`U!6QG9UO_6Phtc1iPiV9md|jF zFwZ+g-fbLdd?)Ir#q=4hKZXFw@tcemC@24gcSC})>#o3?cXK>Z(MU%tOK(1v2Cfm! ztDUzO$$?d$KNDjaA(3pm!|LK<{U#%_YMlYQmGSoOnld0 zT|dj{nd&dYTtUq|9MmTRx<77}d-=#NLwzmS2{yXTD-mwEY&^kAL=sKhr_=Zngvirc zi#Oz6t*Q(ve2Z@ftOYC=4~EK(wx}BlgLp#dy!SR>DF(vCY!SI+EG{la)_)C~6q+sA z-)sC531^WydB0m|m$H1~E1T-ce)6ZFI1J>_nqJRHM~u#O|K4!SY6FE1mzwwq(os{d zlRQq~$K+ol_%S26&;(pZz@Veu7;Z{f2kjUp1a)=3Gb1HyIp|)XvV~F?PsM3jHcjd1 zkXivx>ACOfZ7FYAHFgW)Z4PF|pV_%i#eC!Xh7Fb2N>?<*ChD`G&Fw#l*!dYq|zO2SWZa3VIcP9k==U^SL%5O-MayD}puU@b#n*uA`xggOwEx z33^&9HpW?Kl0Bl9(O^?>LbIDmE1$#H>BjTj>v0+PG!m>8%$ld?wR?uO^$Ot#Nw7 zIhHa5I+EnnhpX8(SP^5dh3fC$Vn;FkTP{9BC>HbFvo5hJ>@I!b<|! zgC9_4Fj=4(ik|IbH~ncWb!$jvWMA_d_CbcQM~3a1wQjM##XLXalM3XlI`?P4-VHwvm6d8+Vgh_)^X-qwqEV<6!_LCwR0ZH1Ca zyu@rW>VmbD?bmZXQ>pzwle+rbq;6j;dUKWGDr=`^b@lGD)oE)acYpZ0E@Uk zd;=zI&>Y_17X2tMPqS+e>S~A?W1-Vd!2B6lO@!RFwzLj*JFA_OmT2JoGkT-d0o#^z zJ@)cAm)U7|X}p-o-5fKF6%-Vz9dNS&!+Q%*M!rPlf26By_WQb|MBI72QCPPNx!!-E zs|<9S9k3mmmdcLn>V3tz!)sPRMPC}>aR)=QT5ablh}`DwPI;RC7!`ZnUr$&fU*0KGUV^X1T_@(#7HGf=atWF|1nU!A#3S7KY!3q1>nEsc3TP=<;;e>5 zPDZ4P93iJO-AWH{(qhrFBC~)izTNq@9vsm+C&08hhdk5>6Xzj%f`9~9MEiuoM%C@H zM{9nA6fx=PiovNH$#D@o2MO~B$?e+a+Sq+e6(qpKAnt*=Ve`FCYe_w0g=q@dvM?D6 z3?^DeQ$vMq-s7T?ZbN6J1`H&s(l-tsm`|CLfop z?0m1clu9OcxDG@D(ar9^3?Dj|cV{~YB>=`F2DTR#o!PL=F9(}m5?y?4o%wI z2wulZb|>6`4em19aI0G6cU4A5nEvN9qttVIA5a~@{JYBE+n=Zzr4Nqr*F zP`INhgM&oC!_yvFog}hcokr$YDj1Vez?gI%BzEReLVWYD9 zH?wwr6$6(DyCu|P+_Wc+R9p^Wh$5VXu&?7eX7mrXN0-}!`T>?Yy|v*V3k_~|$5l9N z^~ri4VM_%$)35!?iaaf;!RLdC9e=Yu*1sLcZPV`n@ZIj0U&4~44&n&b(35hu0qFLF z*81_q4Wg@IlMmO8TuBiHDeK@#{hF7q2Db!I4li1&4ad5yys7Rv)84&o&6=o4&{KQ5 zh4Q~o&ra0$Us&IF}@x0&o56qx8E9XhY=e!ofd)@w5cQY&^5w=_JyBtVK zC= zRt4HeR_y$lM_6*CukVIoh0}g)9%r~=f!9k6fWurE&*|DzDSx%Y3fot-i10E=)0oYt zinpYFt3UWL`Y#q0cVe)(w%-!5_QQMY%oV;@o9261%AW$o3BbthMssS4<`g(Uaq}zN z%OJn(-O@cpmr?qI;*Mogi5&$a#TsBDfEHN2^vvtrCtzZ%N15`BuqD4({_`RsOTd=7 zyM>Qw)fvDvdyQYCuZ%94)YPw*a@+S&GP5saUP6=b7{kd)PaBnG&K7wraS>9nrTy%1 zTub^;J{z*2rnW}Qst#_KV^uq2&QH<65z5%G?Tqot_&1JlijpgTJrQl3^AMSQh@FgOt5_Fp-$k z4hUuoXvdkr)KF&Y=Cr{6cuO{dlSBkEyYjhw$shAyqUWn~J$l4EFstz-tzF#pY=619 zsk!b25BDLx?&pf4o!8EfrTZw&V6vjW*Ied;NJiN&t2Eu5gp`R_=vop4K|+O+FgMHY zHO@{92J=|s?Z4)=pzYK)R0rl!9ecrb5!@;Keb!u3I>I19Lga2l!(28TY* zz~X;rrKE>#Njfi1rk~QJ7Dr9;*7Nq3o!qlwQ@68Ni?~)z=3?`y#(6AjuvmQ2H_H6I zjM26$&rahSKd|27E#Gc+EPZl?qFN>(uc!u#rY1Exz{8{E3uRBvcz^8BsGVlKUTkiX zs6RX`Bk_r=*1@R=6m{${gC;R9PcR9sQlq)ju~a94I^A6ukXEAx!ME@V)(8$i2W0B# zYh4nYsR5i!^vwoMHl|L5p6=k)jCa3cUyg)GqD!rIFoN>c^+HDRb-}8$rB+h&!?W8r zQ?sAA-QFE{e2?1rIL$--?ay?|2jqd#D%4+blG(G%nlM2txsYkOjshZOZL`pSMLju? z@B1K1KAx-VAP;?B6U&*b7V8rDc@oM*BctcJw#ezGKCboRs8R}B>s%AxJlA4TZSqc- zd4uoJ-r}(O0`n+WIuw!9?qx=PyzG#}>>?y41ZsS&du2#`Linc~%VW0N1q4TeBA-SXMf(=>+&m4J3CK?D-5y|!N-M#B z`GA!Nx|9kn{|RzZLluuPJ4R4(c1;1vC6sN7^0`TyX(_Rjx+3W$LXzQ-8{h0puYb-U zPAO7zYI)(TWB#K*XBGuu5dd?tpw1w=PL5?O0xSepE?=foV7m?FW9eN{UJz2odoBKE zc_gj>lsNO}64-=HQxg0@qN`3k{&xZh-T>)8tbVd*3-zc?vUyo4r!wmP88Y*j`S>r^ zYW9aBPilxX^ZmxkHf%ln4+u`S=?^e=tOfGJbhGNbgQS|<)uxtn%&vMcZMOp6PvdT0 zUvJpL@@&p_7XKrAmEi2mhGABo^-ee;0>8k=EbM8tG2Ie|iqvNUj;Fbf|d)xp( zhy-I+5GJDNFo4U;-)~=oD2P5d2w+rQKlz+A_NB8-R;=Zt(0mx5fRk6rc+spHO`({C z9XwpMI?T7VI&_Omlzw|Q%&J6s99t(&CZ)}4+n$~r{u!!XnEhmcu!NH6^ti_`GNjmM zGqADgYtAnnfuL$`?V6XRn`(IgR$+n3^2Q`Hb#tM6&L|D5A=&@x^rP)K&v>94d2VzAmo&lwGW%jcP9iA80WO2&O% z;xnVKIbqKg%3UjA_%y_8|J7Q)O|Z3OG6+l8)CwktOknpWtMv%uU% zYu5EZap*_gw$GgIM%!eIzuP>tIlaoM&++xKry)nQuXv1{wYn0ou!L4K`z7a)NOn)g z=u~Xd|3Z8H3wY>#Lpq1J9<8TMW?1yzY-5diP~dhtKm0?y2~Ef;p1F%gp6UH(*iVVH zo5#P~&yecYnUh{cI9jDQ^4#}Maa>X%5~bi0)B@K`n{Th>`B^pCX-IH6gL(cw+Otc@ z>)%ui5(F-i|Dr-7y)qGW=#0>*rvxK?CLS6q^{h5;rZd$pvMYHvsj{xyAiQtSz0kIY zA%tQa!ts=mGIQAA2ev;c9)CJjTTOH?nYynP!pimqx`=>0EUgYCSh7JxX-{wy2!lJO zs~SKlfX-a2AK+fePxBPge(DJ#*IxS2+}y1A;LA3FCJk0!tYyx>aN2fmI=)SykaW6# z2jMP2q+?~XLVnCAw_M6RYBsS_Xq1_M;$2Y2?O>ZeA=~bLEXfFMb&_|zLJ3)`Q7ttr zTaSZ5bzn(IL?TE%aG*6%rd=;EX`j{OV!I&9Q*O5|)^^zu7v1MhfY>E(`?;B-(K@=Ds93I)Q1Aa*RWj|X<$hs*`Y+uERgX(|YZz|47@t9f ze>$wiB36Xi_gOrehyV^zPojywvZCZ+45i-KQpO8z!&>94Vy9RswEmgNVGW=83 zYY}E9-}?w6-K7v7oNv1M|1kC@@KFBW`|y;c(ke?zB(g8rB}OS_SK0UMitMs9jEd|j zvM*DF5Mr{6#AF?^lk9u8j4WfBdCv4%e&6r^|9YO^<2BVw#>_qU{l3rpoa(YE-fpz8Gip}W&!p;~;bJD~+Cp;{n|Gg@DPY<#=)9 zfBMVrbS8|KWn@^KuKve|{3!6$u-JLzVaEKM?jOzWjY<_u-CGwj^JZ&Fr zlVX!Z3ek-cpSUXG&ZK#qd&_WlXtiCb%c!lTqNrrgbOyF%})8mQkVpGA>!C#Xlm25PLOFfQAN$cM{% z@zcB;CkCBbgN}ufYdQiMHp)H_9yykUtx!ZT%EWCx?b6<>{%QOd9p4#}_Pq$oNLrZL z&Rz;h3l^Cg28@fxq-ux}Tig!YK;(#ULQR}0gF)YwMJMzb;12}?lmJvC*zb|s3u0Xa z$EuYZ0O(T#ZdbRwJG^X8DUm?>+PDUa9_Z-D8+z=yB7PbtxAEg?0hrljE)whHcdk*) z7jkv;_9A3=e|NdpJ~R?5nx_-l_FP0_)zQ4;h>Fm#Yp}&eU5?>dSWfaHgYk=s}`71F*n<8q|8k)&JvilOP6PzV*3=m+{ z;x>W~gmvZmfH_M|OY>i7E2U#hiK*&c;eS!{)Vp#H9?0mr2gDi=(Yuy_rLrPGiXZUu zBRp7`%%^;PM19ycr{;I8p<75GE>BT$#c7%)%v2Yjl75u!-_p)>F;zi0R1f|@$@xyk4I{(xG7d7MusokXPfzDl!XKdrh<~vxprinrsSwfxDbGFt&iLSsC-kVE0rC+R?qLpgT@vGSHu1=hc?U+C;Az2uB zP)ggyejZ2hL2K1Pdl2N20<(JC8lS5vL+-i-H4I6l9<#BE%JppQX^ZL>w>PCuB9)|6 z?y>FCg2vYtMw4Pc)}NO0?`Jw=k{V%Znl8Au_=kZTIQD-Q!hguDYQ3`5Wu@=59t)GH zoOv@eQsnLZ_LVIs?vi@Qw6F-Xc~zMEc5}%GP!jo>&R?64g8uB@GACh;&AsuLj-ZGO zfQE%-$j+$B4bBiN2hhUmd(Do%b8AxW8!A3FQa0~`_*~$(a&vPtj(iaDjghFPY%s7* zzO>(Wi`Rqj;c4cN9NXel?X zM0thL{oVBw=g0*%@@6C&4@auJ8iEHd;w$r1`H?we(+QTD_gj{u_T)--$mjL&`$v0s z4(Pi?nr~U~wR;zJm$uvCwXhgN!(&2>eLn~^2>%hk&CJ7}!L2Hsnf!QgE?Fb$zO2i;2R zE3~*PdXFAm27ne+g(yO+pe;?z1N<~np1iZ_TyDHMC5+B)!(cOFUp}J!aQ@=v@ytORfEnsRrMAcQuS_nMqMiYlD&cGz11xHevz{e{=^nhZ5n#5Z)Yfvj-ULj z7xXy6)1N`m-=1o;nzihAa-v(WLaZ%EcZF~6!#2|8RYqrTm6Xa&)FPV7xQ2>;*y;Hi zLoa!$2DkjNP4@@dZ}B*RZRcsSl-tT)-u;9z z~q-K$GLmt>kIF;oNz+7tZJLO%n-EI4x-!n;~2>4P~)VQ9naQDeARjPgT@cjWq zkjD5GqwsR4M=^U4w(y4m{@2sV)k^q*wbeay;d>9vZLKARoE>cu2!Ap<IxeD?c$qUxT9r8e@P(>)tZ zw2F|6Fyoi&GFG|il=EpcfPUsp z8*+Q>E&TPy0mfpqtgWrj{pADss@zxzYH4o?pmu_qLRBKT{w3%(Pci&u()g+f_SaiH z`ylrB_Uh*R06#BLj#sy7S-}eKuV=jIydmiI&o{>vR9JL={blcX=L>W$k3)Z_2cBqi zDrfZXUx1(Tx@gy4Ydl=yu&DL!0hzvd5o1xS*V~j7a+MwvKz}r*W+82plL%Yz2^jl5 zuY!=swoMAA*ch=2yIUlmur>Kzw@I1tT@_<^v95j1laniO1BKG|yEe#WuF z=`xaB=k%+x&$B7XO{j0hV0ND0rll7dNZsoZ|7iCtYG^8qZ+JV79IiQX`RzbllP79= z8%5NeombT+-5->vM(m79)p>1T$kEQ%{obAw2Gz)7u4PeMau1cq!aZ+~@47n!Vr5Gu z`w^e@+xL3A-3b{N!?-dCcijd9S_jE9$r@Qz+3x_g0(W0%LJiF=9%K(VrFDlv)(x|- zc{$Rr`gi2#3^Ld93C+XX8RVChDygA9ugB#PQ=T|o<9%F_d(4n*yL#FJD+&Aw#%wyj zCfg%6oEp?Or6p=DuEt7O0stlIe@%4{2r#ef@+x#j8n2@!7zk)axtR z-N@(G=<(?F=ZRG;wKFpsqsMBP+jv^jjBzdQZ+H$-Bwr|9%3Ji*h}SuM8W?26U;Pi_ zi?q(}?f@7^Kzs()zX*6uEK|G@Uw^Ey^Cd|iCt_y_QzI=#t9Bhdk=WR-^YI zqaxdNPk(z-*sXFZ#A;g1Z8$wPc?`WwTBzwp_Zno!kF4~T1w0{k+;b>zWFG?Mw-hjK z{_d3t*7*WicB5iYe9%>pZN;E*o?jQPFOJ`{dgr&?JM#L;`V*CXB@sG zG(wn!nY|<;{9Q3zO>;`3BV)nGTh+zLecOysnl__(*si27$g(6^ennv9A*mkgGRQr= z&4gbGbYI%keBA$(bd$_Y67ojeC7-#X`4Yg?9$G%AJavoH~HGLQG%o zER(xb0`Xj5fDr+n)7Pb0_9G~5dmSCb6?OF^Ik~wK4Gd71?@7Y%M^M=3llWx3%~QM$ z|Ibi9`g6%dBpdr%_gC7~?HTOn2t3X!BBRZ@1YO^_bwz$-Arkl9pVz$Yr}->~It}8^ zKYY$#uookLBJTxXL5LIXni9KU_3q%6g*W<+(`~HJ2MEOSQesJa`=_&|yJ5679V})_ zwr0M2E#+u0Rxh_zJ>xf9F?ctxdSPh3rG(ndASl$kgRS)B@=ndO3C@v~gf`;j&IIryb!N+z-GIoyKQNRD+ZmyGN%$GL&vV6Zl-yD(Y%r8IfU(juF*$oY_s5EtZ1DB8 z7<;fQ2Lr$W$O<5PIaw0U`|&25FMY5FFaLVJ6~$;?CFe<2|Em#(GxpQtJ-t`^+!3p( zRMT=)uSO23rG_q6s<`p6Q)5T@=e^=$Y2VCD;_J_awtq>eoP9_#cz=Lhj0Qqxvp7VtA zk1xp|r$n3)ntT>2A#$|eJZ2!`^8%UW>~-Sm1DToX1I8qYTVrZ0rW`9x9};S9hNxc+ zv6x7~f+qNUOj8zq-@0()uI|}ere_zC&-x3svVPM9NS+ivpWerPv`Cg=KVL6x-{u}b zuIj0*lOo~>mHweet`PBsB1sl2U(~9PqDZA3?c-~2ISf+Wq*(yDqd`or2^#fqJbhu&i{vhX`?BSM(jd6k^gVXMBMsKmZzqZ1k zY1vE~H(H`ZZQ8k2Hcn5u-6mG5uviiAOkNWxlKX1p=j*q$4|#+wv}HAWJmm zd&=kKk)s?YVr)>xYQr3_Ww($Pd}erNob7@mUQhi@#R@Jcq~$-TM{`Rqiti7WC0(b%g-kY;C^Jz66qlYuI}iDu%(c5ITbUb?w~|8j_p8t9`dGB9TJN zN9*}gf&3Aip&Iw)elbDFIb6C+ujETYzC%j8T@~P#2!9akv9`V->Cpc<@<UKn?B| z>cfN5pYX7E;SA{Y!;mPbU!rPBUqo7~J@{|4-Gb30fg>FuI-keltR+dT*)a4u7 z7X!3y0po*Hk<~4*JAo)8n=ezT->M?MI^*rnmsfevzlX}ajMur5+WMVrm^wSuX$jko z2OpgLXv3EK(+=9>WU2dv&jb%QTv+j#kZx$&C}}%gYy6My%XBzz=#$;;jr2Ef9?SP**E~EtDB>(E zM}wwM)$`o~9{Q22u7gQ|h%m;fSsHWJ_z0%ViVBM8R-$E|EImF5|{QbKjU_l&HD*>-6l1oN;Yxut_&>YN^ zz2pe^b$t`<%XEDB6t}&>?MO^k?aYz?}o zTQdkq4FwSn(Q*mj{FO9A=CtW?v}E#*S4+xuNaTAv0QRs7JH|( zr_4%14t3-yR&T>CqZ;D*f-74a8)jc?Fuq5k&D8ELt(8tx3?|+(HCNt}YE`N__jENu z@Yp)uY2{F2$jH_S;w~9C$#gQ&w|PeUYq5sd8IARG0?5zjdppzO8$RB1a=Ll#+DBP& zNy)FpUtbh5ass~-L|h2d(m=5mLv{eI}8zsHSGuRZvtUYHrVvKGyoGV>Kay>0v5_PjEH^33hYnniLlo??^dFvwQ``QSpX^ z2u+b>N=O5I6pGx(TU$Qey}f%CXwN3}H#dQ~QAe6$m37qM;te-9u|JeCuv_Tt2vB3Y z^q^METR<|J-v3a_h&)#JCOJh0V>+cFr0#nv4|TUCgUq-XyNVmZJ&dQ(Zp_oMU}(1p z{M=3R-BCkN!*n+}yC_59!q+A{#mSwwvvBe9wipAGZ`T$yv~zwK^_*yxN|yHd~CH5bsNV?aHkM=M4Da zUw{2_{?dFF@#o@^uaF5{8_uvB!OCJBDWvu!^)SER#M<8SR{a>VI;BKlQ;E?*AFcPh z-Yl=-HfA_~O?r3rrq`-#;hB1emL4u0Gvm!8+V?)Rb;+Xxj`bUun%8w_%;M+|uiI=E`tlKy8mltw1M+rDPQuh0b@7)RI4+p5O zydS)h)_fKl9)NwJav}L}jC`K3JTZ?8iS%7Qu5nZjB{5Q_Tm#wVrkLT2|4asgF-+3UyBe%PF=6a~WBP%1DMUQ0V zdik+^PZEkHzo)bk+B>cW77&FytWDR+qlLRuO!%ypV>IG^!(w}RRQ1rx)C;wPoMju$8Sst%VirPW1qmjpl52W zBssB~-iHa@HWJ_1$`ua?eOt5bf~r8Uj`#WDPCi+- zJ9yK4WQUP*A)XXmpU{7$EUgH9TU4SuAX-YCd&6*FxLtXGGIu>`H8LV%?&xe%_RpX9 zN#3Bn2Nv><=7Orz?HFN}gD}brbt_b|(kU7ri;F3mun_-)`hc}z@iyL?PS_?P1R}bt zrw{_jygvs=v||*rG;oPeAUMpi}&Ky)ib3QLNYF*T{dNtYFsa6ZSX!)_b^}U1!aq$l+R|DOSq%O)mYsS?Ba@3Qt`O=fB={CUR5y zl*9B3C=EuPgu>KC8f>{JfseGiJ8LA|YeJ`L z)F;#VSO_)VWxv+jfAwlQ)!r*2JSL~66El=Dr9z*yoiavW^29`CoM5OA)_ zj1OkU;>F@xsG-8ApX;85bq$5^btk8S%EYb!p4~m7e}D$aThAg{wnK{d{N@OYw!J^+b&zI>a&B?_K@bM7 zc11NfNNZgJIybp>e<>jdKQqG^5V$#wFV59i@^@i!y+6v*G zzP|YqS>A^O!Aw}FtxmNyH>;2m%IEWQ<-cO({7$lWg^V3a``!nx+JT6X$8L!4B zkH_{`%$8Xp{P~@XcTodu5%%X~K6zKfFl0_B-kscP6+I&yqUL+*jDx|c_AFF+*-cpv z2HP|!AYs<9B^P@i3Z9kHmzZP{x3^oZj=m$p!cws@MxXFH3irByLQLyte^+8{EECH5 z?DF|5L)c7Af+;Chz8l{Vq5P`GR`}cVg&L^0UBCad9sh(L!xR*MwNX;?kLfAz8#$qu zy%!OiQOJCNuF(cnRQRZwU!ZIbxhTE?FB)IPvZ&zRl~~hr#GdcQIBtPhWWriq_UjDg zIwY2*RL=f1;>M0?(_gr!SXX4-vS}-M@j(eQao3`B&|9$!*zHsJdtf8DJJ53-1lFr^ zLE^)x*-@O&9l- zB)d4y`U<0v7>?+|r8=y|e6`os&uWD89O5E#SnhSW+WkX$Rb1bF?H*dXW6CRwyB@c= zwAj4)o*?j4NpLs$^PVXc_y?{ z1*NUMI=yhPdi3zOZ~h?p{1$6x{qtGDjhXBk)NS?e5xKXxYQm2uTN(Jsqp_EuI@B*& z=N0fI1t0S_G{5cMaIvw}BC_4#?$cG%${ltAgR?22;szT6E?j3_)I#9;Ufdogh{&n&tI*Bmgfx2xkGO8nlCzC!pFNh~4Q=i_~M?6O2s7!ts_$`U5fF8lye>d+e_xk3f;ut9P{_%pSJbTTYbk zT&Y}Ysp)(8Kqcscq#btH8$dlOL>51ty|Q?vh)h|2YWA@-L4Ce}N2y+pE%J)|>h<=# zHMH?t`e+zRy)lmc1B7g5e@Vzb=HHI_Ccp86yFi0G^+4E3t5d?~Ir`!Rd<*8ve2^l^ zjpt1JiBaV&!5Cz!`2>BTL9=P+m{7Ea+-P@K9%l(nvz{D@0c5DmQGdZ%Z?Rh)9b$#AUd+Waj|LLJ{ z8PA=XH~1`%{ZO<_mj^||Cb8b~#muHa{hG+Qhv>{yCPKS z&mqMwVR(k@)nfLCJWn||C9Ly(*9cDfBB8~&Q>c@PA7#;@IJKDiN=#?xQw#0Ib$%(7~=vGDzkR2F=W zn)A(SpPA@-FyBFol^v=|y>jlw2Za_jX)*I@R6>2&$a3U-bZ6>sC z(?bR_ukq=*K2r@;4(d#K+A{e4);l9Gt%k0&(2$PcDl4Y`71{&q~So1*oMiW~E* za?Zwk{;XPQKI`&p{JLd!yvseM%hQJ}#*$EShP*Eh+t-Rz&fVYSip1%0a$0gCvykD> z<_JnCFX6~pJ&TJN@`I1(Ran&G!%{RCd>~!~hZ|!IY{oNo^7Z*SQw&(Mkin)iacE4L zD@`G4mpJmXVXoVIci9F!NH2luCOjz8R)p#IWpmYwJOv-_0Ar%ELIwemwV}?h|Fqq4ap_+G@W*X`C z9o_HmNhZpV?S$BR=botl`o?G3j3idQGg+GGixu3dsXpVm1RyXpey;z}*M~0Ofo}A6 z$h1ri^m$Ofx3tQB-y3H>(fZX^)Pg_NK2Dy#uX*ZsJ6kO!ncB~8BV?wn_57(7r|I2s zlub(=CAdHt@M}poje=49Io|Dp)Btb1oiv4u1w9SmrVBUDX<>U;yef=y4PE=hrjngMPnuKoD8o5|Q&$sb#I zCgPPMXi7C!9*@!!%ifuy!f`jzln&x)eoJ)u1RYlhc2OLBl9h)1z6E_O^W!7^(%e21 zSo)^}5myY3+!6|>a+~tFUwiG4Wyc~qB=l043o&fqi zro0gw{R5w>=nC0ee`tF_{@HF^5 zzt)`O*1Y&|HI%CaD6#l=XKwSYt*ueaTY`ZUTAhm10|T!Q8?A4cgk4Q7RIB-g85FMq zx8;XpX%+7q)Cjkg&x%2F<|7p2)>ddjLJdFhc{>UtJaWj_cR&a|u=U8O(F@cdRvX7sGLK>`I_ai77G|Wg6CD%Fd%NyyrxnTvEgLJqs{azS*7cSVa6LA9R!1S!F zo5L40kjp2FuCLttef8YPN^+}vWV?c?ndqFUW&YBN4LSc$Q`D@&rHQjem>Ao(Hl>nA zhzoJp{Um5r1@qhlnrbWS$CJ831Y2I7*b!SX9H2tXrK5%V0`Kzj0vjeoerJ~ShvXr1 za&p86gI$QBnsF^SvK`ISs}A2k9)uy!gZ!R|D>^z9vL6hl!Nf7*7C7jvma00ICXo*u z1NQ=uTK;3kLeaFT+72&9}TQ0fCZEC8@D4o2p5>WwMb4iGRv{oKV{!X7(O!Y1dV?S^7k-`EuBUdxDDTANbf zLhBLEV&;-H+m1Dji|P0^mAdB(GeyP7gJ=!3nxCfe5Hgebw5wjcQ^>d+?MUJr(3y4D zgyz+yuGNIGL#*r99t%SM7St2DN2)UfthYQb{J(dh@34f*p`+%^GiSu0 zpzxsxWgG>1p=z8^jMT{}Cd#-V83kUhcu9_WOEyr4#Fsc26k1S|1xR-<=nX7B9PaVg zsW=fD=a9bHDg>S7H#PhL&1}3jujG}Hw~$m9(WISh7%h~X>-_fZq6b(2W<#H@{()r+ zaHun%vs@DO82!9wXyWwg)15NU1jk(*OCS9VWHKF{H-)x7Z|UG!Pc^QF#UJI-%iO|* z>H+Byn3cS|_G#Hy)06BzKG^tW+#6i1wB&cjj%86^ZmpTWKn=WpMv}I^_f<Y!WqvcM!;K zepc4#()5M1px5X>YKUq@9}gz186&yWX)rl4;FFGa!q<*bZD6AR(Y2fQgo|Q zsB5D2*{UJchN!plhUeP>_qaIsc5b1KAQ@PxG}aN0ZJAaj>6;d72AEiz4zDgu|Vox*%K_t zNDJ!#V(ix>_dy3qsFDoaT0s|5f?TRE@L|Bg&d%;Lltiw*flYR(y`MqP=y89mV?qUp zS01k%wy-QVG%p>T7|$vxVFXo)Z@-On=&386Lz8m|1=br zX)H{R+%foQciR|63Bk{q6mL@$@5IH$>r)C~qXTVGja&id2qq3#Tzd}?RWo@G)kslB zwh7Z!&Aq<7aVxxVwXhz(HxkO1LkY|n8(_i!P6%_mlqPCwV&ZAUXV8AR;MA;CF(&Zw z&OvBxS;utLln&hG+@;&CUiXqstR-UwZ*NIRNzFemkBLCp01V!|?t5`)CxK`8`yE-f zgk2Ncb!qIFxN1}9a-(~K9HwRJ3xq=0#>}w;?wE8}gQL6{oK-q?q(1E@EibL4oQI+Y z{0AShn(;#TR}0u>D4?YzUaW>|t!#gGAgX24ing+ijr1$XWiVW5 z?wQmO&hALDsrQK*OSmT!PyeR81}bSYGb1Z2sDMnl!6u?Vxm-$%o=%vIa}Z>W48n*R zkSw+-`nFmSDp4b_E>mx=5mc)u6lKjoNJ$>kFflf!HnXT#vGOHWd*iOliIHA!xV087 zSi2rs=9GTe#nsR@#k#kclq{*J@>cY2-aV*F8u4SCTIIxLrn7B0EM1`vU-Eu4+ za;%m!@V@xxe)02^QO#MM5k;=WQkL|MajjuYGlt~d^+`T#Oq{TxYYIKa^&tystqikj zw3!)yeG6FAcrrpXsnlHUHEAnIm=sH2Nn;vz@02szsI~8E#U#ANB#e02M7D4yJDljc zCG0q*c-S^FWaQ;SKRhXVHN=|~B)`KBJ(wQ{4Yy6~&nFWUhyoR-9BU_B%f(e8DFT($ zT7c=9YQ8OCvSC2|Fo>f_=3jcmG`(1)376A#0y3gP2qEr zJ-#weFQ#7h)DWZk4hLCK9=H1E0s@&i2rl2uNt$fE&Qslkc2%cDkjS=%J4P^nQI$}` zdrL=R@oKc&1*44&%{r;74B0HBO=ySSj990Iax-i%LtsyTp~mgHeP`gtEFbfKnh;0|peMQt=Mv zL;J7K$$n0fYZKs@v81{kq`*G3DFY6!RXIG;EJsZXxsQ(O>*!<*2%ibBx8;T0<8jyA zWLUp(-^}X0l1nKX;cri^m=p}KUoOebrG2qK1qmK2A8fl}L;Nb=A~mbvS~;|Hv*}L@ zry~4S0;(a*3%L@+#BubF@Djh+N#Pr3k*HI_yN_eZ(uIduKUL`|*(|F<13kg?N7Iz9 z&H7er<7Bl_@TPe4>{{XO{Li1`tgk5AD(4~`58Avv)8;@n5#bwhW~Ajq513CbKak9b z_6ywc&5QHP$r%SVRfIqIL>UaXT1T~ahQz5^O(5ePN#aRK|PECF!(x1TzH7>QCt94 zva?efF1GvO+9F8ctDD9r(HxM)#M?MH*2j+~*$N)XoGmfa@uKbXs83tiY)12LODDZT0g&9(sI zhCBSAiI~<*p95xS%D}{_^f7UMqZ*E`m?mOHW#yY3K0slTHMoHYgEMD?qJO>vGheQ{1W7EA)D!p=-R zme_vw%|Rx`@!;E!w}>3XTPj3>uI+PE?FrzcKpzni0p(4PdvuXup0i8}jN=Of^f*-~ zdV{Fe;!GW>B=p;-Sc6kUH{{lP)<+8>&|L|rGyhmpl2%xF0zwODRM!HrU~-67sKba( ztlu*}TSAa=+$aU$f4n53Y(BqobH?IccD>Dga62 zix-IY0`Mv>1Y{90n1(6WH(0FL*rkCp0xFQLXX za;#G|*NT{Nd?o6AE4R#7hpOPPhKZ2NOh&)Q7l8thLMfLROPz{Pv3AN2S+N@ju^Soo zSbEG{tjQ$@Le#82w$D})iTm&~Lo3G>VibVsg^E=gB^2N2@Ig(aSD@;V|w3xSlaJboJ ztksNc)Tk$?QR~~vgw>|)S-~bkM2GVoWY;mK z>OlZ(k@(kc;W;@K6LQuZrjU4#FV%vn7*P7h;$_C^ zE;DPO<^VBU7;S6x|B zXx}`pGegz<?N#iwRF z<4mtG2iaXRA5m*oFF(y7ptSkMIJZ`q;lX zw^B%w3xh0ps{m~k9uF%`hK&iC+iNHOnH%FA4$mFUE-0Wu?0Z@pz@ZseH0S5bsRC^- zP|M8DRvy0o_xo7wPrS__3%e9$eCQWN2MYFisiA^M<2AT0Y1P%NKnB)sy7u?e-){1I z0bc$MAmh?OK_^jgF7-h8Lm~$%3*K%`EmLdOVrN~mwZA0r=Jqtp0~Bc@lqT+=|a9$S46frO238C5X{!AfN?3iPdG7Tvvx7dqBiL znAh!Ry-A^?0>j?D0eA2I{V!nI><_qyU$y(V_N%2eD7j@JG{_qN4YMA2J$b=zs?c>+%-} z{&71*aLh>DF0u#lL<9mt6J)IF)mC%z($YL_F{$Q&OVqR;}&Ar?Rm{A>Jh=NsK{KzOyr+ zn?u9}Q$Vg}FbsbJAO}2q#l2*}p})!-I~^ZL%XFXQ1QP(*fp5T@0M+NYuga3Z8*Z3V zjU^4F2DUgM6KJa!{+leq?ExK3xWt%4$$E zjLU(G1Uy3ls|llv z2yuOZ_y-mr6b2&twMO^pdOMj{_x+S<>}$cO?*k}&8+#6U$okRj-|r0IGG*)4m4#Ad zM_=)_Gf*ggk@bKxO(aao9swC0NGsnbJwGo(=2Q_ouu^My=&X$U0g$7H7RXdXP@Orn zI+SHPS^o2KE2MSijNYcFh7$716B^sXz2;!onV7G%ut#fY_*K3dTR(Wmz2;GAUbUsu zD136Wpi`lGKR|y@BgntXBiP65<-TEc8AA9(LvyywQOF`gC9Qcc@pKgL z)K1h<-C7Ra;SZ}tn_Yw4%pw|44r{@73#B@WcRI{!+Bx-a`2aj{@g1AbEO)C4fHKSGm^n>*tbw znoADN8&$aT9zVmOl>cLoKRAlDMxq&^78gZ4kP`i=~QOeR#wNYLg@fl38PBYQ?=%YX#{0w?43mpo31w|fm z=X>>5Pr>-ho2<1^FmbWn-`wKk)G;HW&hhgFX!Jb@XLq>s zWb9aUKRa^fgH;n<@@3DX!quDS5qAL*@IQ|B*#^BE0fVA_@Nsb<86QD0rv&W3uz~fO zy*)p;($+;1!CfcnnH$Iiz$w}j^_DrnnbJxV63*B@3zeW3sK|pOtmoaS<`|TXketnG z@xa7Cd+qfM33Zu?RU))ixeKO$X9j4jUu=Z2HGs;zCC-H#eX$vMdE`$wIEZYP>}oI^ z8=l~tDf4w^^}-@Dh7b8h%(r-8UiD%A-HVN-O*59^nN)X@Pbx;4wSG(5n-Z9W!we%K zg@G>0$?CZr2(WOAujYFm&-C#uH-45G&eZiHP}S-CrhAs*2vp)uBAIbEMcQ-E_sxj? zyJ5T#ue1{ZWSBCQvMHFJbN`2cI&cNx&hV@`9$04%6XO&v$+7w1BNdgDqGIM%&EVq0 zI*Y<{bRnZ4Qo6}laP;%WaXY@h%@O_`KMi6kCX za`DAQnGx4;{r1meh=jB^L-*~IJR30;qr~jVEQx3IShk!5$ zLAg_xHA8@@ufb5`pE^VKL7}eXTv{*3KV*o|m^X~FxHe!M?D38G=*Y`ZUWNqh5i)2h zghr|?uM^il&r~`XuPf*UONdd(I9L-&+y_KrNCV@V)?hLbPo}I&STz*a2UJy}w&nhR z0wVicY4zMgtWo+!wP74D4?vH&V`N0w>HPn^?(0LT%;TVj+u15WDVsvBJAtE{u^~EEuNSz-JW( zA*(`OqyZ)bzPCYF&V7!9s;qzUp|_qwrbLAqB^9mb=@RsMCqdm5XFPT3|2L&VRarUq znHq=@sj7xmCDOD1OPTmQ=iZ!~DFKv5%7jvP_h*Yj1mersMKA)USkJ!8;bT+COqhFE zL%CK;OB7`Yci4PkmI;*mKQBIPNm7mNxj%);rEnXRATy_==6b)q)3v{Z=(n8JEkAiQ zNaW&0$opvEJt0f+-ngd<86F^0u-0K_stSlk1c>C3AOVgS8Mwno%%CLi=^OJ2bi$PD zs0<{R10lsM$K$}uiV~r(xVf!*W26MQ}_#crKM6rXMK@u;5=a9-4{OK zAPKoWPw06kKE4b{h@rbkgMic;GFDL7qFAZGmeD0tz_q$L5t$CH$Y2}}Jk2TIf{z}Z zI27pL{}c_jC+=3$Di>;AK}lf!+fPnwJdm`+G8Gf_CrGo$DArEA^)-~Bw#|2h88eO$+NbYHD= ze81n%`~8}5XDi<#jFI)?Xa5)-uRpwjn_&YI!f}FvRKq&)4ypmTe4JWZE;brb9#}+2 zRZrOv%S6v|WNw7Mg}4k~49Rq$?lG1OwcAO$X;(dzE`Ga3{-Rl^FS+p#gKx+>TN3wU z$KvJB*XSiC97EBsVGlaI#KG`R)OkaJeUv+EyGXfy%g(%Oq?D2Tb84M7s-MYbRj9>0{Ird%d%wI)CD&^C12M+CB z90xlMZrq8m3-}_3J{<+QmOVGrM(cHSO6M$T{7kdU}Xlh34 zY?~rPFSsZOiQXc)gf$ZLg!0eYpmp))ubvME{oae3gKc}*Z>i@z+zIkmg` zi$JbT`H7q!g*oDO3|0Pk)hBq#la!Qw1_m*GTQJY4Li$~|*}jU$>UQmzGk6t{zdve9 zSj*y(;XGP{!4>KKSCRbym%}Iu&w-U{Wq3TmQncuM-Oaqs|Ka^bhWdJ8=Lc@DCfb(2 zDB&)iha^+@04XB%l*@Ux&-IgWa@tb?`Y$l!f%}2Q0~saKLn0V#q44aEfQi~)na@Cs z4I2ScLvIY9&1fd-!UmoUy+KF0r*5(JgYW}+a?g$zST{HvR>tLHAyf`CHAQ<@^Xz#3 zOnK^~_Wh;SRkym=Nrm{;XYL zrz8T)Z-sYTba9#G+K?;n91cwVAD0OxwM%vzdbYINJqe?I20UNGWkz?PLJ2rbkpVa0JA5Csq~*(( zPm^AnDFoiQ(Fxs2AvRZt2%MM28QSSN-&7H3dUb}2h}yna^RG?jd{m8)>QO>>?9Zi zoV6}e4nN3FCH^5C@?XiMeX zFna9$IB^!M{W!wDWe*91a~0%0Z$%yPfu+y$T|^t9Gpo39&ZbmR(HCWk7Zx9fS{GJv&{Ls0mKL_QfgWvH_K(P1W?pyG#h1VSCRY-l zbck$WaV=<>;Gt*#!*2DMxmu<2PAbRxoi4XLj8WbO4p7z@i7T+Ttyx_yl)bjr{9TDSP=oP(2!wWYW-;3#I@NC-Ke9}o0wr16H3QcoIZV8((#8OanG2uNf>i)yR8YLq#?y9VA*Q6k;}~!$tL;tOxmHl zb4Vq8D?4NL%=Fw4&(__wJE+ziIFCgAc{jJIH|?J^!{*U4HPVg956_Jnw>!Yt@VvUtTcTG~f*T0{%wYhR@AB`S+GQM|c z2LZ4W^~I=A*tjGu>h;3W%o#0omn9byuf2jnLbaF%79-cGAIlu#y;u`cS)V5QXIRxx zd{ou6KBV)exzyJQ#jI6cUGsPY*_qw4E8IXK?` z+|~TFdIw~!_wMmjWn0*LE!A>N82f-8V1iotg?4p!$?BJFy2UtriN=5yzVoGgG z*NAUibKtDL4cqeDK@*cw6>6_PiN;1rMw=WF!07=6*x1xNwd@V=c7D{%VG)=k5r9hc z0c|Qbagay}Cm6k0vDvaqR}pkq^ypHeHBC`5$(e~8jmM1cBeZvRy53HWu?Fx$9BuOS zzB^^70KSPEHp?m~_+$0sXIgQpn$u4O0+}?xkV!f}cFCv8zuojGQDpch9~xZ1jI0a_ zc@qNZC(1xuhr#gxqQQSv=Y1G>O@^yq^ipEtL)8Ac=bMC?9M|?<@Eb%~2llYXH{p2@ zwQzG)7HRO;F_QjX318?76AwYbo$&@E9ji?#d(YVTKW%?-s`NPj;|C94(_3yk-54&h zg|?4xaEp_gd)l^Q@Mc8S^Isr(nJ1M^@q#BMytbU@)qBRiYaH5_%-!+|xSb`AoyH(S zoFpW9XG&a76ftTOr`L7*%#Nsu3F%u#!l^f+f6|`ojrdq_HF7`o*^B>WIW+XOFpvBF z8&^n`o+B6w!BE?gDp)i+J3H?L`u)ix$gC%F?9%%dR>O{U|9v&Ip!+lMj(9z{KJ|x<8Bedo?_a=QB#)g_^)U-YXCXW;Cg%^h1_+$% zTasyg5bDVtE?z8)$w*{s+#e4;Bq1ZH-Z5;7jlS(Sg;PdTZ$6?qe`%G8qS8}6|IW^s zTdy=c*keg51a3lWb133tWHD|4(o5Rns`TxBx z{x~=_yB$;y@u?Jv;r+_!<7osGl>|rw`Xr*;s(*~AAHsD+ItS^2jfjVu9X2}{Ssn%r z9$RVkfDoY*3oANmdaDFW<6Yq%l_Q})GYKji-W zqT+NT@2Ni1CCdubo1{=$K73$eQGJKzN;F%EoIM2c5c>fQeWi3UnM42OX;$86D%#rI z+s`kB;x9W~y-Q3VcVb@NJ;%c!Ri{yhlKh3W1Q;x&S+l?Jwy}qSy(mQca;mF%NYtRr zp4oYa=RX|Map)ncX&-ZJ!r|Bwh&X-+HR+!_(~`9o(r{FwdKa73+M=aa(YAS2f*zt0 zdf6BG7h4Fkc)1vS>_nD`6v1p79?CG*pW;zxtx>dj{g35-(R$n{ivRf zjt_XGLE}gNROoi`9H3cP#}ad7TrX?l6b1h+TdJRYqP000!s3EVz-?7>Mb&x+2CGR< z5a#q&ahmss7h*{aSij@aDT-Ts?|ZczNkXZ^O2-zk4EQi0!R~&%y66*E-e361dbRod zE$f_H{wf0zU)>K9&w|j+Fg*Dii&G2}GcyLHf9}L>j&osGl4Sw#)$2&6`PkSv=F@L+ z_8JhbHk2~WU;kp4*-dmfDZj!@JVrJ-%0jdr&Hf^$nLM}c#6xt^mR1$BZSd74Prt#6 zTTRCxvCx1YD6j^8AENe>SnN_#O3*KGGEEo3jo_EImVR%~k5zU+ruiLt`z81l(U(CZ zb?aY{+*fFQyLpg#Oasi`gS|dPrVT=usfT&n^t@q z%SmV)-I=0#L(TSXSmIXi$kN}d+gPSkHISgTZ~R=nn_8VtZrQq6!*CrUwwsy$TpS#4 z(P@u%v(+0L{P!zbDb*jol{#zviW{LjLcEO;-`XM$t>!68Ih6_7Q)cZI@( z^Gis{$;r{;tA?)Z9+r>%YA1B5HB{7FWnT*PqKa95qfayQoo<}>Ub^FO`i~7|94iy_ znzNL0eJoNND@2WY%GoF1rsz1or6jaDmRFt_`Fc5%yRmuGewTiA76}(d%8+Z5yUPrw z0FLd|=ZiJBmkSzmKr>q>WF&S-t953)?vDCxcmIdPg^KK<>s>b%-pTrpssT)8x3?HT z?kWq6+@PnGp4p%d`?y-h8j`W=_pS5Wk4{~av8sOmj6}i{U^%Js?hc){B9PjE((w<< zb^TLQ6BFx(bVQ1#KXTQWL4}BiN${lV(7VIjS8hd-KiAu+WsOwhd14hq&*7(9gzCIaGdW= zJDO*Ne>cndLPKjL{?avutRRo(6?f;x4KG;&d_|p z;u6>P_MxBcmsd9qAD*o+nGa1h0Vzi60K{OQEMAxVLHhjV=D0=ybvcMI4NgWKR1R4n z-+H<~kly;UiptKsuOsfbygY|B3=9DwlbiAW`UzC+>|4A>!Slqvi0*Db9X&rWtP*wq3g*v-gTIG`7^C!zf zH%9Jo6es#B-DXEi@fL%2PI3T`vhSD6pHAKZ*h79~@Evpmzm=OORE#+jz}a&2Bi6Fk&z>q;N9$)YjInZG0*C zDQE^IczDX_H7=$aS1AZ_Gj!uhb`!DrdMzC8t|+ta0m0x$Hz}KJU*bD`$B^? zR*fkEgkp4f*~Gx(!wOoF0b1HJE?TO4qYo*pAG-6jj<1#)?0p`F^zryYa(m%q{TH9j z$A9*2@Q6RKS8(%wa+h~mpG9%m&c z@Sk8EoO-~rvg9;oQQ&C^v-)(|^AB$BkAdDaCz)Eoo>b+PalFpcgy`7p%d0cl)(=+E zng@hFj!jIAw)bNiG{zIqAwS@8CjR0ZS|ytDRnjh(oB0lD-)M4Pr~Qfr^Uv`W-Uspx z`)f2SjtY&220?w0l9H0;+K!RgeZ-4*Bhi{ z5C~B#xUIi1CrX0}8(Ahveeh~)@yFpK&#hC-#*fq*UQg$GIanTXo904r1{-1VR(mCA zb47Jw*wW}T7^$Qw*V!lP+9aSH$!vBUktx(w7kY9R_cB}?0@8->WT-;tdc!xjT1qPn z%JRn_o&Sr{3zTWW$xM96UmuAtyo}8(h5rmlSF;D{x$yE9{g{L{O)cy67 zo9+LP6Nwi3_HT%?;=O!K9Thkifp+GI@>@g`g0SAZ#Ot<*mu|i4qpub0R(A2;h1Z5) z#`XiFf+mRsK$5tcp?dRyj*UM~4Sm({j+Z^7AstH4GTyp{^YYsXsvDtv(+OKj9J!EL zkrv_-Z&1YJNQ;TCjB1v5%V~*sNnVH1mMjLv4weeQ&)6#{hzm;$FN=ot#AM!m(q#OU>yTn*#S@Hash=-u-rd9&) zGh=&+Jy}A>xq5JdvMm>m8O$stPqh+`=;`_4uz+lhhC~pqj z34tU6|5rVV=@+v;*|tJ7qT(^t1EW8*#Gbmw3+&nqsvfLt4LN(k%C@uwg7JU}I;QG& z*bJ@Kf!@>)>t1$jE~e}nI7D1R8BdlmMy1_z|7pE&)9og3V1)Y-OD8wC3ep;BQWq3+bGTC$ERBMU#!j>a$fv$CKDe$^3*Zskw|!0 z$K|R?g^cY8rz0*VO)I4AlQDJR`9uR}{n;fX;+eNA=?&XvJNOPdVtcnyu31!k`-CqL z9rkZUQ*)M(pvMZIT)dp)B*8szcJVI1kW>cE-aBjp2)N5KOu5FY?y_#piWZ!e5dEK8 z?z|J7?npfMfmhAhk*^`!68(s}P&6f-ct%d*Cjnc+in$^oWXv*vuQLBA!7$PREgeye%b zOm;om6fl+9+W#E>A6Lt7n$2Q|2DtxBQq?>)`7|jh(B|@6jx|>C{TdOq+uB^tek&dZ z0g2CJt;dF%<@L)5MI)&CvEh>gl5%p_iF#RLVNFf#XDl!apkMLtrO_A(3IAFe<+uW0 z=t{=a=qjDuBE_&?;Tjg{gU|eN^FncMeN!gJq%w}yT&R`*nzq<#5Y_lNKimjAKAO^a zbwByVY?L2++SY@Zp-~VGkOMG1QCyN_oExok(ogGDEnX=`Ry79JItUXF{FCSP-xW?CZ2T7&Z}`f$l@U`Lvs~e}iua68 zY3b@`Jx1SIEyy65h7|p>U&omV-g#d@tqcDSxuTCO^>eX9FmEUgfhf=N>sQJU8|Is4 zuP1Fv&7oLr7~cqCMkYWz(7}frnpzF^KQ9IQDpT*bR!Qph-Yr5hw8o_w~&_wN}7ho;&BfFw=^n zooYQW3LDrSeGBa$#m=HD&v)lJemVIh)OkU>V#j{Nwycqg4$Te+1!@=2K)KkOgyud6 z1!kopDT(~q^c;WQ3ZCahSv&t>u^hbkWz!{Cbo;>UQ$!4NoQ@h7N0mu5Yx+3UttWwA zAQkCv@^iL^ip-=CoiWot_egR{0O49@7QV-2Dtkdrx$eL=B~=C=TJeb0;t?7$yIi!j z!^!!!*Jaj6-J+AB616A%f?W6#E)*+!SI`dp-t0t||*6tV2o_*#Y2E7HpBnS!h37?S0y_pg_@72C~8* zcN`P4IeBt^en$8nAJg zBLvp@+aa@|!wvxEvNQO~FnSn@N8~7+mA=8=uuPVCdjxOv(gbpupSU25|q$ahT)JhYUk(Ndz(-Km&^~)*@%*a=f zcbk?p$uuioRi%{fP!Q!CHOBhr(f%MxjZ1g(Z>F*1^wam3lw?E^AmXTi4@O)2hU;^y z)o)Cmb4FPFWm~+hJfM~^8+9WBoF9o~D;HxIor+j{`j&Y{(FbJ_V1-}Q5HIap7GPwnaFz~*hv7OPCX&}k7#Lm zy}r;-839QmW}G8$M<4k2X7|CcCNpnh3+zEW0DPB|;B?zHMI?M96Ga;jw3jw0^)tRa z@hQvcgZX$Q*b|5{*H=VH1O)^QyHCA|wtf=p+{fwO@;~W0NuE}*m*qN@n$3C?s`_Lu z?Mygyw~oGVH9?2=wnj8fNdJB`715CZ-kM{XC2%13iDxEdF*GE{HfP}zDTaP}8Dvz3 z7?b|uUd>uF`zoWnh#GiCrD;A0_lc#PPj1BoUR<&PPv9-(jL~C9kG|_J>^#@;C`hRQ z7Dh&3lfxdrW&qoF4e%xKY8E8`^MxRmph4YR|E)2tD8L!{HdR7f{%ls~awWE##`2sS zI=33|RQA|}ge*SD*Y-L@JM2m4*Ks~{M|@kj?O`3!n^TXhz)?WO6@3xeLMK<#2uA*~ zohim{ThzkBD4M!l^BVEdZj84~e;O3Bk0We->cW0W?MRf){c+i){$z%ve88z_2ca;x zc%4HSq^U^`LoQLrt*Wky$SPO%^;gdpC=q9_iDvV!fBlAbv4cL~S(QS^91Yb*Xf zq6j78GPErd6r=U>4vXX3y+*Vu3zAfcPASR|By5nQs!hGYXoQSvHzkt-A9@aYE4gZ& zp+60F`Q%;j+QS8Qk@ud)jaoPI2_fo6lYtRrHjUMkWxM*d+7;RDTg(z~xMm3+L3#Qe zpIZ==pJl}hl7w6{_NPfR{nb(a(_dXmo7J;vJi-W6PD*;YgpCHN};%}BCvV%;NO z&ZW0(EVFqOKTk5GZhZNiuaDRfgJ(k%91%!TLI|$G?vL4<)^Vm@UsxAeZX;}HVQl9u zTfPjkB!sCSl7A~poU$u6^I(+6bE47|MIE8!lK6!s<0>MgI}wTGc0zLDA#$wc-y+p0 zUW-3jQv6<)W&g)^%l-Ab%wwB8{!-bN9EE^rMbvIwmuTz?J#^#XfuM@su7PjoU-wki z^Yq#rs|@Tpta=oM#(tPOotg2bLPH1G6kppOl(IRPZU`k=@Yl=Fvg@8EAE)w0+FhV=cK z>Rd-3x@|eevJ9S?%$D=3(a2@OyULpo1QQ0(Bih;*=r{9VfWY>V+2$h3qDu33R9XI$ zF*gNj7}nKpY6gToI8`X(`0CX5JZ`*d#c;$f;KcjNpDtGq+^4@~L|5suX{KG{y(VE! z?`1G!JOP7T#$h=SSPu}lWtVBtrV^jFC*jS*O)!W3-VS=I;(W1>8`E6L0dVFB^iyjejaf1E;e?HTm_nIfhSrsGb z;$#A zwOJJUx(7u+r5N1|{08VD*%%Aek}IBlO00e=BocZN$QcSx7CwDdzg!Y#8Hq_tyf>*4k@ znXTXwu%kkmLrWlrA(H~dD%HcsdAiC*!Js%b%DvXcP3AG7*dCxuY~ zD(4q3UH}4v7|VQL-=G;8Us@w^2ZB9Aj08oxslMI|KSl=i`VkY6#wV?{W#v<;^)a^CpCNIisU?g zry-GhEo5o;3-I~1g$`>!=Qzv!YWeczS-lZVVM**LeDR>;0T-}&|NMe#_^xEG5y!diNvJGLLRCttNMY{(0W-cq=yxfPux zJ3amJ80Xq%S48-KU+OzM(h70Ve`8o)v7F85<>_#tJNs{P@AmNUXbCd2msBfS&UePz zdiOsYdT>IBaIgP<4?}|ZE)Go!BotRcG_RMG*3QYG5!RFMF|T{D?aAX>r<2Gk;BlYs z%yNi{jO;kMt|mg=&EMWnclEV%@^>0MpSyG_!mR@BK>Z&>GTPZ+rSy4BB~7e0wB9(t z$_T0+H)3M-uCAy!UJ?>bIoaEv2{O4A zV1+Z!)(e+0{71fT1US0;%$Y5?29e5E)0j~4_43u$RIrgm*vVoI5)j+KpdefVwEho) zTy*YN{uv^)Mr-aeX2)}e5qFOd+osk`;b*0%6ynOQD8s?Fz3g~wQ;&vjaXRn!lHuXI+v?KzjlbsSh|~h*z^0!L%Z-wM ztBrL*D8R1R`t+!gIQ!q#I8%=}NKm^9re6Jb{Z0KJ$3LEa2?JlLcjls8*muV~;PItq z%1@%ggbyzv{mANO$V&-E7&LaX3l}o%vLAohQ>Nwy_j$0#KHDUAIldvN2L6}r-e}Vz zN#HgLcQuVWkqy)8@RNUXq?i)UA@Q_JcCCFopd8YN%hSwbqhM%AB#g)1gGF?mnkhk2 z$1p1m62ox8&i5;jPe9To0E-VV{{>iB>nHfGo4X8-_=O;6%L7gyF1TjaTv!%?(x?np> zVS>yVop7RnJc#RyOi2+3I5W=&MtlUN)wIZB1UU?z5%@yKm!6Rn{gz+@}&ABVZEP+594;`@NxCS96sr~%fAE<~AkBeG{a+tas4+p(v$yq_Yr_{XL_RUmxUy{9xwA`-K{WasLIx|l6phGZoSHBtQknPYHVe%2sFm0%NDj<1l*!`MSy7hyFtsDj0tqb%woFD60g+b6Dl`SU<8LF%l{%qayH?Rx5hzCG#-yilktj1K@gk9R> zkArk2Ux$f#^6>-2)`~h6AS+0a<-xxMyGgol*(Q^%eS=^cuaoP$PtytXh$O`kZ* z!$^}RPCsFJe@8Tkkq3 zoVgCjZEs_6#Xw#CNjYP}l3v6!IiCHBTiM;vIQ$6OA=mMqD&kB@OnmY3Lo@wv)Z!vP z)yycpsqXXVRq!k?4lI~*s7&Im<3t^blc*2jlzkaZ;|aq|9AiO}Xw;YQ*U=^iqK`>5 z0?~dALo;x<(Wc$r;d{|q0TsdcNP|GVqj@@?*M0iIxwsSAjS0ecDM#MBo#{~uS>xit zi`8Wd|Jgh$+4({ni-vPMM7fH4Y!uL=u*Klj`#08cVKo*5eS>?5NU679dowKZiShf^ zbzHsbJ(LcXlv(MSWXDL+R8=}%ag&3riYfsY4R+a*-Xk9)^LeHTh3i}f_`&}YJe9Yb zt#G@P)Pv^bBxtv~W3}eqvw1WL6~^yZ#@5;LMSHR#`$PR>t3M>lc+S^{+YyRj!V!fe zg*TVJcQH!4bmzWLU;EePNGQSaWNLc}8T9UqR?VM&qnrCwH|Xl5W0ON^0u~yC+t{=2 zOB!6~x(j)VSavI;WO5$*h49W-8phspsG!yCv6&365P4b`K5$upU00tq{i3Q;#SR;) zpIB8vq@7ZQpb1x1EZH)Y0aI;!CO!yi*Qk1)8@2FRhJJW!T=SN}Q_n4rX#x$cg~GhF z!<5k)k4+rW5aOyLIM_|MEzN#;QreY`#oia!ZgyC9_$dG$qPo%J`O!a3zRE9dXCEtT zeER%{0tUtuU7cjxxOUi`gj1yhFAj!?+C;)H8t6gZaumMP0OH5WeBUm6;}F$ho3-?rDy_tev$!Ay;<=^Zod7^ahj8QDe=Mo%%?uCiSn9qGsnli5svomSko> zo$mW|QLF!$PxVA)9^U%rWf_O(I_gSZw zNQN>V2g>?t+X$cIRQvFbNu#nth+9UYGHvAy z3I>88=F`?@#bAaTwpV3{LGU&m5{zy)yIn@&08cEw5o;YvWbR>$!rZAgQDO?p@wRxl zJ9^CiSEZvfW6|^S@77zALx!fXoPXm9Vw$=W$-Tb?a6_w}B&?N=gV|m$*5@Fu8mVgkSV-1sYRWn{#IBa1uUi zZonpu`I=Bv%yMHOn{LR=^F~#tIUqrjC$d}@YzbyYGIsfMm&8o+xQS>R81Qbu=b@W? zf)MEU9{hl~pL>Ams;H)tN~HT((uiM0#Bw|{nLB_Kuy}P7i0sK#(ZJc9Xnc1vW%yLX zEPzJ`9Rz|Kc#tGa0CYeUciGMcWwyJ=n8belQ%&M{S8NMB2K9m6j~_m~iin5Kw`je+ zHr4LIi_yIO*ZkB`ZSCUv-G`JTeGz!=4dl}FN6YKkYy{?*2kpELPd;=V%ZZl+oN@%i z>&T!6Yc-c$)|H^L`|O8G)SRkr1G?9@5Nx&l4}*6!oq7ldBk$$$wJB{Mn zb=wh{kuPx<&U6!Z>PBO?QB`>0Y@5@Ug@=dhrdzB88YPkOe&lDcK#ogg-ykqiP!L39 zG3~Y6e4;ujJ=TOn-Hc#H;V7x0BM&nZPRho)t^kQ81(#!C0Oz&OM?w=}?_5YdyXQtx zCrt08k)U7GK~96onVw5pltcO{?%zIczQ@o-EuxG-$x@mT;3k+-zfg4 zzUBE-c1PQmYbr)Xi_CrDud_nN!2(rY@A%q<&=fR%jYTIN8#<^Tn=Z}32#Zc`%;y;* zWmL^uHI(2$W9(z`Z%z@m3m=E=wt*asWzTn$NS?nu0`+WeYSuAY7YWCWYlIt{afa1w zj0d36(_Y`$SZd(;-ri#J=ds7Hjhmt@>_bIN-oRuiD=Szj>f3r2fD`psRg19h9lz1y zl&Hlo^iL6vwkEGo`_$n$wbfsH*e!J1wC*z2ZP&UhdQ?d!CN)GL(2ru3J;lDS`b$~0 zAIre~2=>r9hLe>&3*VsDd7~G6H;T3L6!bd_HF@arX3GSH+Ly-bR`94OAI{_aLN7UIYXhNnt* z-ie&}4Zw=}R*e+*{rFD+cDhok43r+B=~-F7=jM)#(XLS^C@=1Bhs5boW@f~;+M3%) z->7P0LHrq9%S7&4s+gX9z29M(z}r5W6j&u&RsW&U+4gAwkwjQ>fMi)jx~r{?1*4A) zHS37AdVo-Zf{2Z%enOOG)o6{G5}R`D_6yfN_OYeZ1Zq+);uoMtV|jji>TBbof}L`5 zY}<5Gn`czw|H@8l900^DN=v!?O9iFX0hD_~ZbMGF14B2;1x>6%m8%Z0Nw^FIuOU<5 zs0*I`7dkBdxxkw#LnQj8Q5Wexfej0PekmRS4Nuup*RVg3eB(an;w%H>NO+RvG6J4e zQy?H>=Jm1r414G<>fW!7ezvXTGvGp$7Kff}vb|4(Vs0q%cWSqhC_ltF(qso%jp>RWZ%FYBkw@>w(+j`T4N?y9JN)A6X~)IsZ;E z@U2|T5h5IO0F| z3Ok5^Kj_E2h`81e>!c3@)ou5GwB-V)-P@+=k3E;LnjA4PNqyQvLyD;a55645MPvS6 zQE?x7H?0R~ht-(ywaKRcS${!@5V^Gyi}LRh{@63ceCNIgiMrh{{X^yOf?$b{T)t=M>7}+Jqn!s2fS1(1xkI zS_YvaqKX_7!f4ej&_8u-6C?GI<941VMjOEE3#($70D^Zuw6xF%6hqvz&ehcw!Hak@ zz8yX)qn%d~aulPJ1Kr%i1iUO5eH${w)x~_BSsD|~P6;G~Oks~KQd^)j1 z$2h9sah|#%bprdT!X(w?Ve7qeyQSpIt-7jfxR{*3IW`X}gcBjS(Y=@{-M&Bt*Y!%+ z)j;Ki-J9=7x*MoZAOylBpr|)qux}PR@18(mI((B3LE~>;l_l>{8=ZcZ|5KFj#CAe@ zhXX)f-WRb*kl_$UaFkylyjuo9gv^`P>Nj%8q=DcCma2$|cK}6*A0x4$UTE$w7gfBq z{mv*nB4^vaP0iH%6Oa5AzS=8ZzIXwj8WSiKIH^QaD=lX<*@_%gFQ@hVs{FmD*Ee~IJ@ogi4(t{74h3^ha=*o|%G+dN*!>59-h*hNf$aVW1 z5L{QPKN{TCbLowjfNxv(9r&_ys-;kw zSJCN6#=)S%&W_%@+Ah9O3@t6vH#(H??rU+DGltWxK;dN=*Q6`b8jk%6xKtBOf3UMI zXrev|w#STyTQnvl69*N#0ZF=2Q(zhj9&w!*Tfh&`5GdZ($ZtO66K&gJu+zm$|BoB&`_6Wh z0Ch1lGrQ`eu4p1Oy18Vd*%qdP&U5;-1>d~K-d>@FOz@!1^P1xW4@*xud%knKd&6Oy zh_>SMcgos#uN29sE0bUZ`!hq3N5Zd5R$Cs?9xynpF5+>6rB!yYNvdlKws<`eUgZ`C!~$4N8za$ zdBi)X7IyvVyb`yPn|w;ep+tw8Pjk%9n}&Z&I0=y$dXP^~9t4L@76LOoChSlk5<2zC zTz37jUKy&R+AB%`XVNTq5F(4Njhk3f;n4Ct_iNpv9cWh;BUvDf!ob|i&zYNkXhl@i zlVvsZZ%f*ue%{Sdm0UaHc=LtQm2{E&il5bJb&Si59&4n5P9&c+xub}DVvAEVevy`-uKk`-oU-=`l$yzFY^Geth z3w4&#uIzu7BkEt!JqSRnz_^67%nC_dQL|AiGX~xYB9GPGhoyk%1kv4Spa77xMmb|9 zCNc=?fBROXx#XRVue{~>sFy#SP}ro6G4CPE;d!8*7p!Brb)xfA&1S25eeU%yIMb{f zQfJTiX^#-rl3$zF(G6M9D&ciO&HmoUp17*KenGx`P^)VVm)xnzWu9j}X1>e@U4kyU z)Ni$oi`crt1@NDlj*Ia2Z!HCf^lrd`LbnnR{!Y(ZIejb&If#Z=bBD*8v%=~JgXQNvJCwXK~VNGT*3#V<9z z%HW8|cQOV-O+?z?l7U$UaE%{6#Y_$EJWNM+`|sX|Vv zv2~V=N!81f)!J>^r}WOyV{QWPr5J?7m8O@te>jjvugJr^GiR%;?5VCtFJH3aMf)NX zru?~$dVKFDlU}2tdxigj^*?NUsTKr9Vcp#NP;2+H>x{}Q?r}o=I-fD45p~Ik*l=r* zIB^@uaANZ|W^(zPT1xP$XB}%rhw$iR)*h+kGuwDb+Hx$+q1k}mu->?u?o8fnM$5%2 zkrPATIn0#LDHeF%g@|UMibViTG1uW-j37SwYnk7y^i_6XRjpNIlCmw?xUsbUaY7*iwHfP) zB960V@XIm)4~BDR^!w8|E;txY*JVMfjo;iag^R$4*FADLNX+NAf-pN$*S!9Rd;gC1 z+RI<%!eH4ODKEeZ3epZ%kC80M^}gPyTGc(_vb9tQ;^vb9YmWcyi?RJy^n@S;3Sx#I zYeaNG_y?WAn4Dotgw%!0E_It3y(=5{{b1GvUj*VZzLfN2f z^RWJ?SZ{0Rxl{V}uMJbO#>(o-tR$mSS4Sa^@FWLY0%Ro!l!+!959@FXQGr+-{HPy1Hs-d#znXO(ipcz{LqmntZ9mJB zmjV%{ZtXdrJ?;>R9+pBWmQ@3n&TMyAoGS>nbMr5v`@{Y^(!oTk-2ZP$f%)sYhqDJ9 ziCodlY&5kR-_7b~(!R+dqbcat1Qqkke zB1OEf%Ah-}xx0gX>QZ=avB+1*Z#UP z3FaESnd2E#FIQ=>Re^`4B>s4p9lCYfwz14voU%I{e*`|0G&725qD_3b362e`>N}mR z2LoqEtorb28St12m}K}Eo=^`@(Hv%e{kASKI9ZgU*QQr-*ut8v#9>TNi2C`|ysT)L z!zR^%+a|BCCJ=w0quCao<}5=Bc$OE6aRnj+y{?M<7JUzpx#xNCit#0*t`wal?L&J^ zR1Ag;=ZjhLW;;LG>9KOH^AIk(hk=C%@|2Qq;yni+321nDh#w+AOG#|FNuIW$>1}9N zLAEbT_SJ;rc(5Xu$4j{h5pIj69lZJ)Mo)VNkLx2QDLA`!7`gE0=JN$UIYhelm}DXHo3E7WgF=EUxT>>W}=N=Lq~;=!rJdO|(+i3SJW zBatg;`pvPHZF_HRzl~Avt5d2~7Mt=W7?|!V%+&=YAf-Yp(uFQEa7{O~K`)1*b{A!O z+VT~vXgnmhlhdT+2{4I~cJfh1UY_w-$$ok(*Z_Z364>DE1-Z6qkPa{0ulX^)^A5Jo zd4GBKLhpa^b-suXsM3+oILZ^)GkEMI0kg}xwwmdr6AAOEb!TV0lm|L~%>ND{c%3kN zJr85uv^_ahLTlgHOI_;iq}6v(Q=%gNe7F8;z1rYmN_9zKVsdhoi;n!xs6HEXTqN1X z?cFT-`Sig+_S{*4YKBSVOq_bf>K{T0rVn`J_EKQOH5LhCXcua>{dB?8|Hf#z6l=R! zcWdT!-e$x3esv{L9cPCVL!5}+yOyublJKr_o3=J2!@h++jAIvV_083)zD$(lI(J;5 zo3^qy-IdWa{uMd;b8jtHyE2)z?z5m6=wstBn2+0Vm)t_`_RjsD(QKP-T}@`JU~34R zCA_5O3SZ~WS^mExA=G9{FAHExT2Vr7OK-ManV@`vWf^wUh3diZ2bw{!uNTdX-AzeS!SJ}Sn8iG!w3y)yVT)1L>8jX)7(N##kdCZ;BpkmN=TsGol zL*v76rG4>=u`d1Jba(cOS{aM-$mu@%t9g2BcNKKmRxiU3!0QN!UU$h$ERi1rXGQ_l zQ)+Oo@eBbA_kuDHgg(YHpKZGj%WjH0I#k+*jj;D%3!2ZCk+R?&qAHB;PDIoa6mls| zq_o3|qf2qI>aJ57@|XGS+gMh&N3ohXLjORWz-(z=tDR3xf8j)~~5Y&R7tjM$qz_=p>b3xpjz`L+o@_%oB($rCGi#YPS9!{|2 zg?Y{6UQDr|2EqIG(Vp|~-&*J30ipTq1;k~OW123WL|)!(mDIC9OGcIR&q6d)YUnel z`r6t=5%KIVL}3HG=ytB#NWX7OA8p2zpKYYFhZ4??pluKWBfMvxB+M{!XBbi*!?xm8 z2S(k@I5PBVO_9i3g#@1%hA8Q16KVcKAA)~~>6+yb$x~0C`n5mP^G;~<{6ZziD+(SP zV$}hpDDu0p`zK@nzj60*5n8oQ#DkLGRBZJUw80YlE%sxjf3b)ssUx8DV*FkcdLS@g z9S_YhOjCZiH{&h{eF>1=>}(V~MI<8Q zVD_C_&L!BZY>|hAB*ut#Fw%+n_F&QppS(>+U`KTWru|Zd}O&MIA({N8oo?~?$px7Ah36sn-Pe0}exmZ-mHG5~U9U_A^^eU3k()f?T|G|r z_PX%v7T3UUm(l4Eu;5jsa7SQO2*gYNU#BZ5b{T6k!yQav!g|#UwpO z0Aywq`H|wVuq;YB3$Fub(x>Xw1pNQc<#%7)$^Y!1FtpFKp|L)7(;Wr~NZgcS$>$XJ zX@5$Z@OrgS#xYy<5c~#WJPU+#FO&ZgLHDi6Nlb)zM{g!%gPixF2BY1!m&D{8hrfL* z7#VpkBnF%Ws%Q7NGabS*i+bygnya_}fLIGzlBjs?i^OEv*4CE9*d$L>odhsG(B8d& zzYp0&Fa2CRb1a!-3u9D|P06Y3fWOk2)*f8V_y|nu)pz&f&(m?n^ByY#h78}_fzOsI z4ejkU-Q5p-cK5iLf9!W^~l1<-cErxsg8S45SK>(_hl;U|1F>Y}^jH^9z@LHl$` z<0`8(?RETd<56ed$p|Y<`e*$eH+7&XIWSlNj=X8olZ7ghg7PDz|V&gF5tM*zZ*tEFRhNf`8(kl?V#HRX>*wO#M7bA6|z^Yja)2JP;odp$#;_K zA)cmRMCt{#`ra&H3Xa2_Kr3=dM|88i|i*RZsJ-Hmc_#;OSEwi(aY~; zPkeRIVOa*ey(!mq8#jYbP83TM7FX!Eb;T(0*OY$EUT|g=ZmWmlP4>$;i-1QP5fpSm zoMHj|5Ih9_!9y))`VlPE2Rsa@Djzc5)Y&<)(@s?UzWqoRf2|Ywkyac?M#+#?C=`Y)ii5>#H;exVdalJhFrG#i z04G5=WD%a@9lQO5?zBHuEDGyJr}~K9R+m-j|Pq}L0~GXNzCw^yPmg$7y8qJ z&gXqfuHg0eXlomnDH*X`P~iCV@ng~0y@k%r+7=UCOQdH%W)s6LRd)=6eHWo1TCMy5 zckwqz+oGe&wmte)ovl2>y8l`w%YUNXx1&o!27GcYg^Tyw-YCrW{rvqBVTHf~mHFi> zVq0<&EePHXvlO;RC^BGm8Xu^q*v0v}!D+ROur{A$r(f?Cl!$_%3^_J0mxUHJ+A@-oHXO)g1iSmYuEcJlj!i6U&M!_6@jEH2kGC+MRnz(?==7qqLy zG+C_s^4#64(swLbMJ!1GRINNd4xS6;bMg+Qn?3Q?2!7GDD=PqWlL+v z5*4ch2@)OgD_hD<5-ok#jAoSCmD>(8jXPsV7lAja>IZ0hO#9|Bk9jB}t^DVJb~kDw zJdL(99tvuwogBHF77x;9ETTbrHZ^nG6Ho{-!UkOLcK28%rs4>?3^^)#2ra@`#iJ8p zmnqwe7gBke?GOE4fV!8KiX~iTUzi`UsF|7Ri6UZznT2o z*g9(Ta~g(SP-x%7O{h2YTS#9`@?+$EsERcSPH%|ztJ-f{_R)}~_7q|`-C4`Q)YKHE z{rx;Lkur{H_KIk6$1NmFntpC}?P-5H*JDTwVT zd-fJJ1e%j{+cFGXV${Artez5(sqy&e!mCQl9(K=LStfmqF^vob6eW(v06l`$c-g2B zgrH2*KV52hb0OUJqGJo2e&o1isn`sc)|dTx>(}Ky{&Br<3^gz|ehv z@T@op(ER}jZo*P@WDRD!6(l2~K9he#5QCF_Lvl?LM07MjOqYDn_?X`#a_jQKwjGLe z^UhWyBCPZ*R@%NbfUA6TTeTE{#^=xM&oQjuxmzWjz2vk}$_=&h$|A?fo-gT#YIv<7 zI&GVHU{Ye(A_zZbpf;8w$*=PC-plecs^oajm`JqM#9>2|Im9H5A>Y6x%*f2k_*E^IN|Pl?N{Br zBgjPTQ2zZ!xpyYN#;9SCA$(e=OvqF|((u)+;SR&XwDk)$Lv3RjHz_O9MuV8g@L`=k zO8fS)H<(D6nh=(dgq=V1=-2*RkM7Rc?F&7+%>STA58ZJzO5UD_`Xe-oG~iO%+}<0u z_N;+3!Xw|U_v-VW^*a0kN5`+uze75VD>TMM$UwFG}Y zm$#fgk#S?&kV#DT>Ai|4)9CL`eCg^+c(_Hk#k!0}iCbB7jWvy6@zAYC`PfG?rcVaB zsDF`?0u0s)2i+Yp*A5x{y7Prc{Wj;cE?0)6acgpGeYkw5&GlpBZ{?y}duHgySsFKb zF&K%&pB-3Q?R{wSl+aDdet%J;hCo!-P%G9>-8^_i0*RI74!XJ|*lhI)zs6BD%zJ5^p|P(k(}o!w-%@j1>^12BOT zV#L#ayy^NmrFfn!T-=^5K2i#Ss<(HI^3*o*S~0`QO8U6@O6@nkZMMSh z(Wj^5t>~zsO!xj{}rp$595PKRcs_g}Y||Cw6q`LJ2AQtYgOPhY*-5Ont0 z4<6y?6&8^DOpHv9lPl#lby0CLx*n(3u12w;m(C<6CU#3JUmQTbc}Jk+{9jesNSlI; zvon;bM!z0OOvx5Y3Ox_;vYyPIo=?3}>A^gn_q97=OFam%b5vL4>>NY|n zf9mkgBWBy4N;G0deBjHfHy8z?%;dW*hRoYtlnZp5n=t57+gGN0q2!3pkAY9M^5J@P z`Ok6tmfWOp{05!fOC%slTWfpNRX7V+_lTdrsNncII4!9f(o)OWpI6%qo@Hk%=YCaK z%o=!A@2BFOs*fEzL;1x1;4<(`QL0z>RxEGc#_1amD_D4-oKQKup+eU9Sc}n%&l4o( zIgJ}>#l*Zs&6IC^RZU#EuC?_gmHQ^#Z3pA37H($ya-9~PIl(ZSEAv5RS?Q)+j9jDj z=lN1@X=zI0JasH{cSKNJV5z&a3U`ZBoW2paZ3D|qc|&WN72u> zG$>yxy0{elJ-%0Oa!FkB&ZW?3mtE$@tQsw=IA>uXwo)Z7PbDtpBfOhxl)2}e_~M@f z=m^u!j`r@x@n@!`ANk|x)#POQtZVMxjHY8H3w)_MrF?9k>k72RtXxAMr-W>2v0s|E zb>zj96dWqmZuQQa>qO_I@22?$D)*mM6{nTn>&@AFdRL_;rhWTZb7ife^*gH1W0tE- z12l~pG^utkXp$egu-u5OGwwd>cUzLimD)Bp{k(q}repTIzw!2Zg{`40>fneYrln-v zwi7bqxvp82ZrajrouBNN&M#~(+FT>Qef*F}cdXOkK|Bl3noRJd6IxK<0`RJOk35Zl zsQy=<9mOuPCif0$Tora}|CQz3)t;&^wfLQmGLhywVp=frSoAB@)&{2=B87~4tpLko|ySDi&rJ?$l z&0D=i{JAd~jwqL{AqlVSYwZ}M@ZBiG;L}4atYbG{Q;tvZqz-KjZ0{v zTupT`dl8ZsH83!MBP!4;-2gVc6NN9=rZY>KIpyT!ge`3r6cAuH`a0oMye^>)V0P3| zvy=OSZ)4d;8y9LC*LrjfW!9RPFw%X%_V9u^<_6f00)X@LtVU`2Om>14fG7KDp zT7+9133R}E<6oJR49U@HUBppus_s*G5#)5-Qd`{j$+#79BD_c1oblKmGqFXKw&hrV z6i2s#^Ez)^N|$Z5g`WeTc5pOFLkr5vCsE=o&$G zsc)@sX(t!qZz2LxiRRJKHBwGSffsYwo{j-_8KzN5}~|0_IR$CiK25IILOzKAo}W!av#Yyg>DlFr>f-fr^nlU#*b6m(lfKMRRY#tbU!w5M^rCX zZa+oLY;pM6fsG~xAqois5)vurh0F>Vjx|DUD4O(8AkIWNL!9ASSlT+cg(6m~MY(TB zqx=V8hI*T?eXMyNJ^SSDue%b5qsmRQGbjiBsDnhSCndwx^B;B++R^v z?s;-S?OeEZ&+bn`k#fWU{)@_{K6@dRv@|tV&vT+DHw;`Y%KeK{VUbE^tcg=;y7{Cb zGeU1Abu)c)!K0!0b?=btplQVIayQcC8@V*9tZwMIO4e}M@JwXO%^{w;<6P@C_Y3+; zb9k5Q6`WXHtE|l1IDN?*C<<5T!}?Ou;V4-9b#(YSh^G#1PfO(L8P7A> zcW)QC{E+Sdsgv1R^-r_AL}=M0{b9@3l`khR|2@(uHb|St7p28}M0ikFyp6E4Ya;wk z^!2546~$sIV3B8T$a(;E<6yrRy)Z z-$QQy<*aRQv;-qJKCIUNx<*ZO{2j|cWVQYS+2=gAo;z7t!v@A%uLTYixIMpga85fa zFuOZo&E{}tS38lQwCshQlV0gb>iIR*aiJG*hl#AFiRF7$ah?SX%3|6?(aE;_)5~#g zKWkvWvJ8`Yafu$~%dn)kmJ{KFG47z1ixkPTd$+XaTEq>MCQ&LH%~_IP80~9p@*zt3 z>oFsY;(4p;v_9p&s45ZecK<4t_^ z#ZVZ^<;K1u4j6J`BEHtz<(A$x@6GfV4PKY~ zWJbhdVV7C2qoe*yC zdqQQeBFPMHgSr%2dg6NDaou(OqbAH5Mc|8b0Q1sm&)SQrQAf16pJ1fMg{5>3CwLS6VM`NY0?$`l{^hFGnFb3#g?j2WQ z(23sqMM6R?UsCgNxJ!2CZ^fYSLkd@<9OI&JsCRT3qxku|#NlvT?6Z_pA*B9V_KhmN z$+I0|pO0Lh;S3m}^FQxx$i|apkqW6l6!~W4XCUA=Zra4oK)pltBS+2$tL+y116f2@ zmv&od$KZ}o5fXBC>4_j&^4 z!G|GHd@5vJ&#f&+#Iy7#&~Re8Ya>G4oPPZ%dY{oLq9mIVZFGn!B=lId{uay*m;d-- z{IMnm-uJK<*6x&GrGpxWFy-mW$v&&BjI9co_E!8Z2`YSX{**EAA4gSw^3d1Ld?~Y? zA-2`qQG3)oS*rE=raISi-!fQH-O=`4V<6>C;%OenCRboBlme`}qA7@;K8`4Q`mjA6 zu-W-aiJC+p&F@qi5TFjV+XaU5JYQM;y0PpFtX#Y7uZelHC4-?R>JR^@rqv4=yI6I~ z^Ei9_mLKS|j-)=6jy#*t1Q(~HsSTG07B`RxKb-EBAA!qfb5k{b--rt%C6h5@Ras~E z2fvsJhx*{%6$-a7ZRjQL{<-b$8zoOaE$;pYBF(93Neu+pP#*IUm+O1C4 zq(_CO8t}}~H`uOev$Z}=F!skQmCsUkh0P5qs+qI;#_Mz4g342LdP`OU+sqYHoo~+{ zNLK#ry(%bkOti%|@e=wJn>Yv%Yi;`>w$;=b=G{2pL*)m5BXs*(q>Xf}O0NEMF%pH>>o(RyaQhW@Gg!09`T&Su%&<0#cgw+!b{eYs`Le4qy*t?(l=POvMJ?CdukA2 zn3f_$o>?&P+k0`~iEYpO4ZB;MOa(UmOp%YmdQi%}@6D2hifuSSa_ZGXDbwXKMI))3 zHp+JOD@y}Kwu?&rMrJFCZ4s^smUW|}u>nIy%iYS>ZG+nR24^hzo7}#zL}=&cbh{K^ zj+3S2Bei@OTAWNIIW(Z7>q`+{kmd*)hU4v?d>wJtM%s-V^)Vy5B4W{@=sI}0pcKYc zHVex?&PdhVJNcx@^?g4vLYhfT-DX1ie`&f|RVQ@8r}533<6kA?=07K7VCmKXEX{Q6 zp98pQD2yb>+$BICNek$H@DDD>%`1JKTVCp3ah)EoWNYkL!GFU$ybXF>>QgOJZ3q`# zoBpev2_b>w7H%~%1>TLqrE~I4c_xkL3SC79Udh*jBs0>1Y2Yv{jY0yXIZijGe{jTK zGuTj>7dS1Xs#PbcQu{7}r_c02kNK34s+h~lLU&_qgLaaYw&?hR5?|mV-o6;l63+*; z`sawzS8L)DqjKejpDrEiVQM#M7r)*=UvrQyE+Zl>yWhypwR^Eon8w`5F10;yyvX0Z zFeK2*J$(_u4)x~)Pp(LM-*mxs_m0Q8PPf7IygIg_>t?xx()%ulRKvOQ;S$rux+&qh z9B#_B&hG0h@6X(5()iY`NxyP3?owLgY`QfYO5?Vs{6%xE1Cl+glX%){3~t6msV~+9 zcuYGyf6;$3x<7$uxv=Mb{}|e|%!Hc*x>`ol^pwf0KFOpdV$dM9B}}uBZA9XDR9p%A zGeb7_MXN|U9W;|olU9Nf^?D8H@kAD`(!}mN?q3}2+L$Lp)<8?Jv6jZtZ1tcav&pwF zN$1n1?#FWy#1@#S$@Qms3_djf8NKS?l-$zQWhOSOb%^Ob$TirOBD+kUYLj-RVSp7q z4jT*?415c~Q3v)tDpRNMw=I0_v2N(W2tz11&Z@|C@P30pb}h+ec``1%mhoYijgQRJ z7i&1|?CrxB?V4qsP^xLTof$QYg<%LtppUnOX<{HR5vV^v$G7bol~7(j}i>)M}Y%c-v`ylr6u}$8RS26{gUnZy28ru6@5zo^x`3 zEjOj?ddZjAqz@lHka{)byh~Gx?_=g%(@lpHty%SXI;%K=g_#o(RrM2DFyn1g9_p_$B}=opz724+fEBy_D_|D02L^()JieOWsnt>W!Y*XIlv z)eh+{cU8N{5DA-*P@uvI&1K}mvewSj3XF^RM*#neg{ z-S%tvtCeMjbROc%<2`n)?JrWDMAVn|wi8i7QHD!j8pE!vYed=I8yKZ8=!3~M(L-hc zVLC=&BAjL=Y%(+2WqqMojpv1Fd?EVKt{l?OaQC_J3AT6A9PBeqZqmV5ct;!+(T6@ZQfZ+kS=HUa!6sSU#bUH4vUDcm~v*!EW7YjHj~R` zWo6O4NusCt)_fq=;q7aqtAv-XHQnxb4$>p5>(b?w2xkb*m0S4m#GrqEIE=uW^TL4n zgAfPir(F&>^4FGrE=fb}gIUovaTv)uEf`6_qH^5huZ@bjcf#qiY-mak?KBQsM*+>h zC@=h)MVPX&t4ye>s%}TI^dRbE*HO21R(Yfau=XYUl|9ILXT zZS^oB--53~HVV}!f?}`UxFL5xd+#S3gj^Mv)s9Ctx~#{IaX{+l?M{hlJXi>p37(<8 zW)RRcaFEL(YujDvULKSk?CrAAdRp9m8?%ZS^PVOs(6+lwmNqiws{MN3A!5A#G|R4C zuhuQUQySg`VDOf`atOezf#7Vd*1NfDFYAo7Sq!&atRPFhmr- zfbb<96+@T`H>GoC5g;5m`njJ^cX1zIASH16ruIm07UDZNDX#5waQKDa^ws&o?xoZl zyDUdLMe&;m^iWTYJO66nRbnDv1spU{&I$(R!&0y_|FSuqS7RmvC4>r-D~}wko7$HD zpSYWaSAAdSdQ(89-PmZyw$SlO z_80{+Mu-Z&1VCVNLHWqoG9Qn=)0QBKDP-{&o-BN^?=N)x5^od3RoW2EmNkcqH3} zo&{0ywv;=5o8M!zajjv=!p+y&C49~XZWBN6&E!`ZV%UY^(mr3~%jn4^+HiCKSg|yk z)O`0sezEvhQY5S-w1d5_@0sby%$9|~ir~wMVGUG`1QUpnJctF@cLqGl@%U{UkzZsAI^|S@4gMM zHui@@JN1u#^$XhCukLGI;}PScEswUy3j*?5zCDn(7EIaoB*-h8UCBCWvv26fj|VB# zfGiLO-&RDuM_}6PCAucU6$wOGM*O)$DknSg!F`2MwN|z0Igd=4u%Roe9^$)LyZ^ue z14=~B!y*knMdB8HJLV)<^_nx|RrV|7S8|F&W_+jx%46K;p=$mpy@ESkX# zy|QwU>~jDxi7%d?$iKc%r}1=Fh^6U^EL#pY`)=)Im6F5p9e2|+WIo(qV%JD6UKlOR z!xrRnL1@Ts;Qf^Hot#UuTHR1lQnE2TC$F+>+#SDF`A%N{j!KD-^Bz*OYseg2dN?xa zdD&^Yi~_P|v@f3EC^)tu{*whRBw9hys+9NhK7a3;O6u5syBEIR@QW66HMNNm8@qA3 zUXH7Dik8&*Bc|+tR9(-P#*C`p1d_;S$D6#)VVg%f3&PdY^9XRmZWa8Bjb4}Y)KiBoqor*e1UJJy`|(u2kj$riguxjN-wNu zWcXUbhTtT9eAH-WzQ*8)j-OzW-H3*$o6pez(^sY&Mb9fGidZt0kv! zVMKd0T2AqR(nR;Rq?GWix$?Oyj}Ohj)w1jrFB*6`GbZ=4i7(ymWSHjeLc3 z8sZKh`ZbwdF%VsFySw~ILyJd9*P{ACyRpJ{XOkz+P6DB_;h6HS%7aB_m2Buq-VMoG zaa{|KbEe|dxxFzZK9@_a{IfM6PWK3{T~p?#{#OS5-P^cfHLg5Y+^Tlt&e76{P15{s zsGuU_$tk=Hg#&G0zw+rY6(N%hSWxeS`D3Gbdj_p3QopueT6s;ATBuzGBxq5QtdS_w zmV0a7OUu%g`enzrlQEU+S_B`DuG&6mpMPQH$Bo6zu{FXg-5tCRgRGWMQcdi-z(#3I z7fBs^q1bh=ZShv^@#$Sl`xRa(#hMx$6TA?Dq*~xA6i+@$n8Xvo&PY}a;CG*^C4MHf z3E+sTRw=*{KWaw!J@YU0*+c6m*WH5EleC?V6Vi}vhX9VTI`-1L%s0h_Rl@l5jcVz< z@-Q3~K#x$7ihg}MKZ^ipu$D|Nqc`K;t)UYqpv3P4*Q1i6u@1XJ>0DH45Zy5p2}I}p z_!ukC3n~)kTy;U}K+4bKhMJvObxpo-5L{iQVV{7Q6t-3QUcBQU@-g6+td_nSL1_F$ z3KlUPn>z~AJa(ed)4me-WqaxDX_x8DxcWi`dcT#$#uc7p+K3z6-}G*5hMZfv#OpR0 z)r}n1@0#87j5yWyl6C#rZaf2k^B&ZfWq*u)%@7DjVx$tKsU>3k2yz6Qdd0D^qCIIA zsgp$n^~1E;u>YW%s7@hq8h**xV*eDtpwvR9)VqGE6-V$t^gf+Db~5cYvm+MnevmBG zOfI4|ze~)z7MRPzl;Ze~agY5Hc;Wj}bHP$&xJFCN3Rame5WhxzCI!xC(;bRY<0nc^GbavWRNO9GW5cp{zizgH9$Dzu(N9j>% z*_Dp?n_gRyWr(nviFv_Df9&&CJ<=a1Iqd?EUOVF6J~wlMr_^Om&}||(&UiY{3@$@> zp_tJzF{|!+eZk+JQ^{KSTL{NYC?)sQRuUgM@c^Hup zF-uWxGq(a)qNFvK(kliC#cgZfpV9}=F_eldW`py$ zZe4x!@g!ZGhL1FdMY}$%#amibU$?<&m1w3?xj$)D3Z7>^eX;ixxvI*0fQO^1xOiZ6 zbUg_&f@T*lwcGgz%Z3fJ_z(oQQwiXf=7o&g`C>l-JOM>tURyqGy*FFGjo)mC5bJqnFMYv-D-9n$JaBnuF1M$!MOd}2zCoKaY=8Mg_H+d3 zNy)=($L!?n={|f7mJKM>72kiOSMeB73QEPSv5&~k74!TV?Ou%tCaUe*sYkBu)2{LH zINOM362=CPSQVqX>(xMzI5O;~{I&0y*Z5bC5a!bS`vVvTyfONDx>oD2_V zc9TW7#V^@cz5Fn?nfQF8R;0yz1h3@;*cIx@o>MfNk0_7yj(*3dasGYXVM}uUj!vf| z+*H~!sel!A^sUIEt`voHshzP_wYK~hR-^~EdHS8eED7M$O=1ULCJw5t=)vS$iECff zmAjlY+NRT!y{8yBf-;=E>-NcQfn3&z~UN^xn7FrRb=P*4jxEcBA z(OaQL@jZoV3__x!KJc?+@b?rL8Gdmd&JXxN&EwFoqJyraI*Ep=dCKa{|7EHwccJh4 zWcqZW6{*KMt&#S?M7EvIwdF&0N4Ql*=Q6j{6pyWbLttm!o98kwgvZM!)PmFD zzxzFTyr|}{JxML9$(@h7sDjQ10w#b;YiJramp;;~!2$^_*=Kjowgq@J|4Qz46dND& z)kdo_aRv2?ukm3qBh})ig_&zEE}e&YfeJ*~7l-yqJHheDQiO-w9xW}my?0;aKdCLM zPL{}0M#ijJ-XTj${SW0_H#?{4ISj=p(T?{WeQLsfTw0pushS^W#{B_N;mj{(I4#2Y zw7E1kWH6n6dJ#b%SA-ibwU?=!lU9l#c&}Rp0hyr^J=a_g9muG$wQwup!c5FdS-6IY zzUhs!C^6BJ2g~FajkN1RdwwMP=CI_Gz2sy%S6(^0Cwb8n!FdFM_-_ka{&?fY~WZfvCI;7Htqs6NG0csCiWT8XJ1t6=a>!jeJ}*UTVYw=OUOByNy-?&vK=?GYhj2lm@2EEz{Y~ES zY2X+9tsa-*QGwn994JyuZiOEEO^#*hw%+j~I4yj=tVjm|_vh50eu~fkfSJ;RGK7LOZM?| zaKwAU4Cu8*w`8aL$`Zu0fbXGRI&3bZlmT)DjXdO91k{eF0y6cAdT;M4_T+C*65lSK zrcisHB+OBZuB2(9`XhWAqmS7uzdNd2^8z2)zqf*)ci^mf>ka4yiu5!Ixv>om3{!t%Rb1Id+o4D}E_%K%!FB)wO z*TSJn|D*c~G=`AJ9<244wV#B4XodW%ib&;8Z%#U*wE`*q9!*eh_qqMS1dAr`O zML#kO>3@*>6XH_9jp~STggw+BD6(mU9P$qaN;yMr$+_NlJk~Rt;eOXqaV}eFxk@zjzVrV`BROtfF=xeAn>%vp^1!Tej{|gN2(-jE zX!nZdn>Zc7Lg2HK7K~g=q|iJqF7C=ZNKYT?vrRlp-<8bVeH?B2gN1R({_lgE!e;dB z(!t#`9@z;Am9LHFAaut!!)bxkM6PV>SW<6?^B=YzH(X9m4h-pPtoVxh?YtLX?UTf& zXB|^TZf}H6&1T{9RXE*LDev}mChplrP0@s~8l7yxz8MWpy3qRzI*a8xqchGuma^~F?dSB;xplT`*I&QkQ)epU^9PxDR8-iW( z8Jv{Aku+vNQ3&g1?-ul7+l_ALv6X6~ASQH{^CWBsl|BkL`o+BbyyYJhQE5n1O`_EN zQ>j03f`m+nL`ZnVH)}^%FE;;OPi%hrqG6#py9~ z2!Ro*%D%&}tzTNYccaSB4-fZgI57;aSw?k3T~!?LY%mAW%yZfcq%*zGaK5c3eRUvj zBJyELyERvBX^w{gw|eY9G>k!k!`T)wtDdza%u!+Ba;NInRriXfe>@Ew{6bHE(NUyI zP`MaUhI$_wz_LSX!>pu*5HQw$8;v!P5?uX9-7nSM3h2-i=j@DQ^c#s48{r9_r|R zB4GlnujxcsK%cMQjwH$|#>wohR=G)~rEI`g0jy$=PqVU(MSljvrqzU!XCf7;@_={n z={~lm(&C$0HrYpOCrU5HjT{bg=;dYI%#D!(g;ZPUFV11BeR&H38Ef*XyggrVQWLR> zaCe;}EAm*Fn^)jDmWvWaA`y}v*8+gPk|sP>$xBLB&#cI+%8_Mj+<-j+srQ&EFUT^3 zs}xWUTk=j$-#d_!!Tqhb{KUo@ZDW?Z6%{gbr#eRL2*VouvbFOcgnq|#B2VF*9D1v& z^Jma60laT2J3P`yXn53ojvcm$JiVQPtWx5I2!J0XPX{wQ-ra$`A29LEa{k2g4zS6V~CsiBWv@47J}%s9BDt&brk4G zjj8KyHHk*+Kvk|Ji$nEa1+TBjh_e#jV!h0B7*+&{PI`k9ishL)vlsEePPdy9FAP$> z*W>lL*)xapkndQ2d$X$3b*5`#WB^+&AveJG!kM4@L7}udYa*xFTypE2bkPP$tT${? zFHDuMemVKZ+%0`8RhdHP6H!$&_01_e>OSeG+VE3GvYRK24rpgtOhTkUgsO;&Zou)5 zqUb8rQxmJ|TDOpnHeJ5RpxyEqoE6baPpE+(LY>rTioQF|dEuve z!H3*7K73s(=FG7er`?;6W8w^G;wpLn^VJ>Cx%!vY%~^&sc1j(isMHRQ4~IzcavQv4 zGUI-uXAtJ?ogCT1C|+AS^47P1NOsv$YO?WCTiPSz1mJi}LBi#iiC1U-J&n3ks533E4H3b`0Ky0FU2!P1k4X9w-fA zrb@Zg!&l;WbDKNct!S%rk^O%r17aO|Hq0#%h<;rh+!9Wq5=Prfna>Y-6q}ehs3(r# zE^PFZ5tw`f_DPwxoc))~eS15=B4aK6>jayz($c_@3;TsGu>)=S?q5!_42N}Cw;ptt zmR35JI63ghcomARIhBLJZKnp(4v$0~%^WB>0N7Jla3rT{>A@*;%U0vVzTio^Ji?l| zY>NO5LEkawQ?6&NjSe7%X6@>T2=XQ}+y(vpdP{l&ULG~8=UO7710eH$qe@d#ZS)po zc7UI+q)2-;ze2^6ki0c-VkMk!Y0>O6yYeVHI;7=LT+{RLx8?=jRMSda22N>FnBm~# zdinX+=ZS?S%z)y%VAUtE)c=IhQ@6!y2JAKrUULi9%L89{voyRtx~zvnF4eNp(>LFD zO!=0-t+A)?>;T)S(Xu+%g7TWu?m!sbtUkgOF^0_Ja^G@YczTCI2i3@(uB=1>p z2MR8UNsTfgp-r9hr*nGgW`tIFLYEjHz3fEnGxl)=Ql!kWu?7bd^c5k$d073d2%@Tg zBFj-`p^~i4IU3dcdQLWq=ErqLDuo6!l^P9pO|mPs>5<3s5L2v^1ysltCijQD)SDZj|1K%Dks?tk-otPJfu5m z-Hry=?;H#4j~-FC>c6Qo=NoYIxlhwm7CKB-&<6U+s0M{hhwX)zM**1~D@H|8c z9Fm4QLHR`pA3eHrd+2GK?#S5K2IljX$#0Lp{N*U7&DvMfVkqn5oc|jo)jpPgoW0#f zfZ1EMw^(j}=C?IWQP`FXWqsnhtW95pklT3OHQI0T*)&F-hcYKw#2u%HKXi;QH%`Gs z@XY6$!vp^<&);YL)rBJJZro+{#>B0a@aIz)Bt6Fpfyb@0r)^fu$`p`(+7IOD0`Y z#5i&}5|Y#LPNX*|R`i`*RT?up$!*P$6cD!gq@QUk^J$I)x)ps|Fy)v_CJ&M>!pOBRVgitM;sHsVi z`g;$T>`hgD9jr54apLtuRuVY3+XVWG@@aOi(cN5Y6KNL2`mKM6^+_aRdg4Ys;T`tB zL94p3O*?MBdC+}j#$A|~PNgQU%SJutMx-1awjm;Zhydi^-|c1LI;07RJ=otv!0mNV zEba>q%6Gu&4yI{dWDrMY%?~(*c+zawS9bs!**jHl8w-dqkcFmlSbQCKzbOvO>J|-^ zo-3&YTK8|n5n~QU=VsW<3cMN3b#UQ7r_$Va5kHVf#Nff?=sLTIF6Q&a?qSSSg~{(5 z&GVZxti=l{irm4~RpQY8&*;`aIs*Ip6uAyX_xUDAgSS7Tx)=8oA2*6>0%QaRFRiq` zr6!a+Q-$mAwwR9?(oif>l^`d4=sBX}$j7u1^M6d+qkJpL>!QAQJlQo?S%(N7bRssj zm~s7>YXEox%eBD3r<0+%eD9VGq9QMXnkRW?9PCW4?eHu2ZFqE~MU=4vH7L9ewhcGv zekrzq$KaJT)$MFnw6~8x{&rL${ozqgp}5GTYz`5PQ0{{tL!ICVQKQ)7Af8BL$JqhMsnP}X>6aO|OHp%>AE{`!^0 zSD!U-X;>UecY=+L=oLzl4fE2go5(oRQ58PKBfqWI_43L7eygR`YSV=zZez%1>X|68SdgA=^ zp2s71E+sqP%_;QCiF${sgP2)A7LF=Ve6mi+dN^1wIYcVGPN4v50k~KGm_7#qBMXa- z%i~VCv2`(VR>j2qVAYgMpXAvmG4fi2wWj)aNoER{aeQKgPycWDDm?2Cd?n7$h%OHr z&iZ3xvNt(s?bHLX33025UJ<1zn(FHA9{U>hTPl9&$dTtADi;{meYaFo8HMsgWpp#- z!{A~p*F@w$vycyRrBg;`XLA^6`@GR+{v^$@)hchs836(*MH`nvdrnlvokJU zh-($tm|WpHVLSkduv)2Gv3Y0uFP4;tz*i&}cpX$ow#-J{&hIN{brtkOhrTYlcy6mm zf%>}yrH|<8#XtC#pZCAPt=?0)Ekh}-$p%>tCfxSYNzbkuQ%*}~si~WnU0%5hk}2T4 zkwG_=`I0}3X$a&ezD7{tzhvK9ftMw(!XF$9z) z^ZFmKRvYp^e-0quA+Sp;T^3uOi(A=&dapwJrEIyL0FW%^?Q2Mg+O+x@A98lDzn`yfxgI*5Lwqk- zUKkB9UY@(wiJ69%j8+c}G%}zL1S|+-AIM-b4Bav6jYKH|dVpvwH*PEZDh{C@y1=Qg zrL26NA|7Nax|@{`Z607bNL26*C7NDmv^UH9iQJ#p2iyqIc+!K{;pJexYOJY+#Ti%} zKNvb`i9i#=rIUnY00Iakz4`o^0WDNoN3K@Sh$Z@0Y*qnDNir$X2MAFK94LUCqLC-C zOZ{_1Gq8gF|2Q0VNxJZp?z8$1HayJs`aXcXvd#VuRZrD4^`0GUo? zdpd~Uhd(R&Z32%20t?x(6pGv5cvgk>s~UL3x2+z6S31q|U}!~CGEihQu;sy!`SE5mtoMn+@p>`(dj2pe ze8BO8Z}>EnrWiRIVJi*S#kcVN(br!~z=85S8~&a$ShsEGgDkksLN5n&wO^fv zcv&Qzvc7FR0x$$|gnNluaX_=V9Vwb|7@5i!wTe}_-_J1O*m$JXM1)1={CU5{6(EB^ z--uv#@Q{LL2`^s!oT%mR-;@CZCLp$ORIs%oqET_xlV5x3Hf@5O4x(!^RF5aoTVwti zCo`d?fh{^97rY$X;y0>wWaIGAEc)a=p^r!QGWro@X@QMgLX(Kwq6qHoKcFg_=P!&z zD<>D0L~MRMSX@Dw06ZREJw2gt$Syp3mA!{VV-m0YYF1Qvd06Xdv-Mg(E6bkW4~id+ z=FiPLUi|z0paOyjR4W#98rDK!mk(d26cpDeq6b-eP}1P4p{v za*BQL1d*tTC-FY3gvR0ZFLhalHN0n)GZx!;&xGDzgJsJri6I^3v@MQ&4Aj-aDR?s+ zp2VQ1tkZc@ujaBt*-dl@g3m32r+yEr8+zAUz$@R4*qu^VCM?6J@lZs#yusMhe<43y z7@;0?40A5Jr0#mtY`Q{wK^T%uf?@7X_BzQ9Zl823bzS&T+RK+OJB(zv{DzDkcg7ps z-mS42Y%b^#66W%9Z^RHw1Cq0AaA=TC-FDkf=YX1tqponAj|+#!qi7iFL(y;wk*_yw zgA@|d?Z>Wkx!Fzn#fbc6*OO|nPvYvN19D7>2@nfnNR}9bMTPj?t}qD4v_*GGkqTd^ zFFMz>hvPRB7coHEWvzdNRz_v41-nDmY@O-4z}6fk;Szy0mZjq74N6ZY_lg!~Nmapv z6Ou8NpOrSE$%~ueYXt@I@!KQWU800pmbNOLRkU6gw{N<2tmQX{=Ug+Zw%#kfI{n(9 z$j*$Vj&gg8v$EQj>_YzMJZ{Y#)Y^w?P_#hS=q<4gzlu;L(V2PmQSEdrAesK z2yt6M0X9A?7C3=^=;{i<2H9B@pJg#oufagrJ=}hr-N7+KT1z4Xt%rA>Mry>BmURb| z2OY2p;R&AD>Q~-d04cEM_P^FrOZsm}P>2!ad${0Bwa=_QR>bOLCMCJ$iZcu-hTJZW z{2NdC`Q3|c=k{}U6Mn%~ocl% zj0zdVptSzsv~A+KJs6TXxb$wRP73$@Iw2Z`N!L>}&Uju(x6t#F4Y#4=+Fcs>x2jJ% za~V?I>(guhM_u3B{>gzHe%0rJ{UMhOmp@jNwVJIa{TBGO^qf)`ovY(JHdkIP5`yIu z>O@>C!j@6(69)`1I1G&BY_xh5Bhso|JI)QI{CRJCT!#cT&#}~Tg6832aaokg4*a|Zr0flWU1LTWl*W9Z6 zXBMmwBiAN>4r=2|2l3rD@y^8ml&vlc&gz$1KvGU+sWBO4zuG&ZIS6^QSlWWe^yQbD zZ-4yY#0O1?)7IQ|P`hDbVzpRr38rO3y9gZcWikEx+tff?JfeBafr&_Mm5Extz;~Aw zCD!2t#s2rdff9R;)yra+7#_bF&!WshORNHsbb#h0fuW5gLW!2!a4<^P;wWE@kbuB# zx&4@>HE|vJ!%=~Jd!|KDC0$2lo<|$8+uc_U$a2)h3N5fD{lrjG?O6Nm%ch=9=mGM7 zU&inJxu&Kj$2A*OGxmMgvU+WrUfDQct$%=)(wRma{yBK_uq=>oci&yb$x(Y-&KZae zF>+;7tWbK6k-UJC1ZflfySiuZschUoIPPHi>&HcCWb=z#55QS%detrHci`#MaTL8C z3++$^qtn7juI<8x-;KMn5G0Cn`&R}3`p~c#Ec<+dwa(PD%f&D}W-+6cyqk*(r;@=O zq7#rbj=fr%D|9tM2kcK&(l1LZ{qAvhV?_Nxw^O3uCiZYpKZ zF{_Z7%{LsK;d5C>y=nusE_>G<>sDB!c&ttWXH-@Si-*Gho6;voIPJ4TXhIYah&ualp? z5$%d_{SY^|&T?q`JlS-na6Di~m)O4Fr;!C{l?|fC|lgu+3Zil`zFvo&RW6q7X z)wvy;NPu@?bX0+vJSFA&4#_P(Kf>5^8$n5VNON4>NyjbVBSNOz#RNh%hCd4+a zc?U04OySxu+9nmB2va55er=?atkaC1^+8wHm+=T;r_NB7u>4AR`pzihS4?~ZU zkQzkP_+ak3B+F5jp&@~5lajckC-xdt_D`NZeV1kZ!S{NRBt8o+?dIHX`J(Ys_e>=& zth}UA6IfVbb9a}aeKRCY!%B)cXE0t(H&tms6YDAT8&?`!Mo9H4)NZ6(AkxrMT*+NV zC*Ww|IB@iZkqLbA9&=IyBeA!)uTMC^TcMqm!TIHF*@hmGYJKa5uw8}&@n#0!mCgt1 zVSu7*i3{5F8Wk&i_UHjb+?T~6x%5PWPBnGVAD*si$=`$@f5SD9RT=S|k#d6?Z+WC0 zKjh3~=I|{DG^*1VeJkFL{(Y0@8O3wm1T>#;ND8kU^i#ZNq5AdnO5Af*h0w%?p*yC* zRtI^Hq5>!goc5ae-DZR@>~TpKda=DpDDL-MBI(xU1O;B-!-vpuG};4CEV4P|7N%FA z^mejQDi9|NsKWnrG=C%vAp?Z%%})E!EGu7`r7a7n-AWmSg zVG=f7FFgF3I5VWK?wYhICraDcMAJmJP|?gE0EY>brD+HD~^=V zBGavm9JH?=?AHkOjr-9@A6RMCqHWKP2S2zP%_j>%&H;l%@5~kYmd@?xnx@4~Q7y*V zEM{;LI`kaE}p{z$7sBWb?;yB z4Krs;`7;AJG(Cm%k9+&|6$s%GLq0DT+>2Z1q~${p|GM!1`j`9zEhsR zyQY$&$-g=G{?2YL?n+H~ZZFc3(6s!A3jfIT_ztQF_MVp3R!+8A40vD)5kafGxsy)C zgf)XXTj6lnQ`yQVDj}MzT;=AS>yjur5x*q?)Er66WU1vfRnIdU`-M_->U<_f_a>wT zVt?<`kyrWo`N(Qp=9^8AR#yB?YY%Ar_2CO*T zGk4kt6P`R!AI`uo+qw&=zfH&LH#*A{of@y!KeKcS(IQtn-vL6qF%^X(C#EJLjq8U5A2=3Hc z=(33>H{_6A)?}vy)H&U7qoUMzGk*ByN;&=fVIdtjL!m+`hS5Xsu^n^ z4YzAMI5t^5@Owr5FW%k*DylTy{w~`_T2T-sh$IstS#kzNFpzUbkWgfh3=$P>qF|tq zjDX~vqXH^HqGXXMWr1X&1SEgYDeQEo``)?X|FKxJdS;C+oI2;c@4KJ9e|udExOFR{ z`c<7pl5V#=zN}h~Z5VdA4OXty7S|miEt8*otB3x~U;#TF1};6lzkG! zw!%Ots2l^O#cXZ=e5~H(&5IO!)9ym+d#hq`Wy_x_TE3@uH| zgTU5QnWs$@Y>>*Jq@iJO5;T=lK;t_B9|<8|X0IM3^r2LFNe-SP(6I8@;=Y_TcKOY|B|T_HW(?dNNpTe_4hkM7qAhUc6;7 zeM-0Ql;Eoeg{g1uqrJj@KS32x46=}6logSix<3^Dpos?4v4cI_zN~-#*(n^I_(Jp~ zonEbk-JUHS+!r}pR&+1m8$z90D=aLmu@eZAmU<;~v(hmYt+e@%C#slNU42MWYQTBc z=*yFf18*Es&AnJx+`%yh>nunDh|FR`3Fl<=a4)&*gpn3UQ$FPSQe+$J4?d{kB$BV< z!lZQL>cbo-7AWAvMHqimS^pS+I~COs&awys5f0#IB_WQqngf8Z8389cV!bpg9vDg> z3k<9muvw)#4AvY_9l2`0{sed+hjJMOu^0UO?WB>ZKc>VHx%<@Q>V?#a8&8i(MFi-c z>wX7HPP`&?#+JW`&TI?h)Mzg4*R!G8M3zX7s~U#&DVUePwADy#zVrzo(Y79zO8Fpy z8XP@(J=)%5Dn$%*Oo#17B1`P)Jx+h$-XgOj4It?lwYEwLJC4*a6aY0>Efden0D zS6KF17F`){Q+319i;1xtj^*$B`+tSo97xE9l(Sf`t#hf6#<@~l`F@ebbb zD5=x%Yt@_BG^E7iSUt(3)>mvA6t=OIm!liz^o;$`ZFao8-k!Dh`20DJTLxXW2H*xL zSuoehirs+ztEMiNM{hn|1pewMMJpVFBuY?qL-w^CwNNW;%5(5D`sOtF8NiZrul^LE z7tOT7)LAiWm$XUVUNHuyiA`n_L3adNVW7HI+1EU9c@;*|!Hqg#9M0Hs;To>-vgp~z z0tOA-AvkJ>P>>zqSc$c|mYK!McDl@djI-y8ezt&Z+y!LmobhqR+6BN21$dtf1|-ss|@7}2}{cx3^_Qbm*Bb|9oID{k4YhE#>S7^K0GWhsm$rUKb!W*sIB zECEf&p}yzj{V}o>bYnXqD*fQFqsiA3l>+47TWNUOW_Mmcn#6?pD_h0jGgP`b|6izdiaVmMcI(1Hv zpZ@^_bn-0ZPC@=&(c@YWZr`t1Q5V9EWMGv?M0PmWeWt8K^ZPHnJ#|gKg8sl;ncFOX zWrFPm8b!;Wp$dtX zXv;$OkvEamPze5?phU+?u=fW4IH}eUE&l6CH2~xu&@P{kbGPr5A>#H~@HjqwH{L*P z$d~6j3>#1Ihr3MeL8(kzX@t}nU~?b8ozwVIH<0gPgE6T7zN3}TuH@cZmPjOQu<)I( zBn-nSH0QLQ8f<-SKS`Mm6p;c~Xy#d8uG>B~baj8bISIqwsWn;Gtd^+r&~rPijam5j z6O3&^IgTT-iTQ_I%VDES_^MMRN!Vc4zdd5BFT%DdaeLO>=GNs^ZeMuiAzwP?mA@Zi z1G7T55$pQimpbL28BYK7&+KGH`UAgIXvWwlzD#InD{){^nQmJiXj_DEWiQ8%pW)0A z@XUkpO={#Se%{e`)p){?cT_Wj6TjVZ#spV88P|5Aq5lVMYQpWMk0ZczoWOcJslt zl5V*D2*>og^k4<{68#M0O9QaE=T)1tIcjd%($AbM^^9m-A#P-nR+oot4n`E_x^TYU z^Lhbxn%hnhZ?xB+2))FO8YFoj$-&H}Mm8B88gy6G)5j?TPa*mD^q3QzPHVxyisXnm*jW9XGV9X~ZIj&s`(Odvn6A}e% zp|_ZD`v=kz0o#=Zznn}VQRgv-CSI2!WAjNDqsiUahXy0NpQJ|m2XTaG7tm)#QcK1k zuYZCU=V`oc%bo?~ACBrWzJ8o8@}1p(n({5gDEaG#R$G{<WH^-KPV34+@tuj05d?VUJ>l-4f!@t$lroK)R3s`4 zCAqHvp>%u9d+oAHMDkLBr?=w!Fkv+m%5T?~cJ%YGDsJP!iLgWITCBhZ4)CDb8T+554pKOHUM;4!_M|5=QI0tHN4}p=42(<9Vg-;ONtx6- z5HX29S8YbfZnYyyw#R-Vt&hKp1F@a6FgJ?b@&V)ehzKuCnKgq$WXU+XVw#{bLYKVs zeTgOhho_&M5(%i)(eEnJ1I&yxZR;IbyGdEjn+F^8oZ|;>r?{16_vssyM7s4S+0D*f zYG>RX*jhp2ySxHAi5HSYrT0Alrx{6mhxyX4g){KSAH3mg1`i%$TZ2Zl6xxf+5N{#! z%>X-|*!t(E;3N?jgL(q?gA|ss`M=Lkr#v>kS@zV+JmifH)KU02G~_ZR3d`m*Lmy=F z9`n+kjbFSKIZ_a|DZA!8G~jSSXs^&qEjwCzkc6CA`}*-**#;%&SP*Of^Mt@%%bj1_ ztxI&njJQc39Ejj}n|U{{b-v1!pCdOCc)P}Kl4r9WxYCEI&a}vZkPN;#oZ~z{mB_BP zU4b+9m9D0VMQKfJS0qkqy(4qBIO{)n{26T5a`N)>YP?{Z25OlfPfN6B<=SPowbaT$ zC!Jv45D+@OZR(4Eu2@LewqF&;o_uIsxtn*9ovW_&pyOPEUVevRs$3~(_pl~hv9`|M zBIk@06cj`_W-_rFf_!|>tE=_O^P8~HZd^Y7JQ!>XFu#TBO22{G_RX$eP{8ux=0x6R zv$I#^E;1b)ORe#ZfY`l0_9@`Yge5BcJh#TVm( zf7CAumLI8x5~jAuyz(Sy*QGf@fz{P{(K@O-9+oh~>o~Hth#%n3)(t9eYZiZx*w`R; z1l|sy-MFs~n&Vs_j7k~S`WTQ>34{KD;6WCM(UOxTWLIEmUq}ZS&v}bZ-aQk)V=cJY zS_2N`4~R|;4I9Z$QX==Kxh2)L7EGpFFxf6zqU^@K4hhrz@kBkK@6%>Y3G;}2{P zK7r~9_P#WG=h7*U`Ir-?(*m~7?pONU*Syo1t{M*0-9G>^?=;Dc-z%gK2KgJ5($#{s zK8%%yn3-axisIt*EA=qhXT+lLVW{n4^ndxRYbCb3g>O_~?Rd9*ljQA(Yd{zJ3Ih{u zS2vK*QAlS(jGdmP;@C6kwG!0>{pZh|IR+oh&w(uO4cw>~?l9@%D4b2o8ahCvT3TAO zcWc!xOaD4{ZVHC~pa33$6Df#?%fyNB8idkb60RDUm)Z>5svIY?gv8aQg=@EgrpSsZ z;y1ElIT0{ZC-hk_a9KG-S_D)`SUf=G{OIFHWsvpAI=B2oThog7x2|h`5AOPdipN|vH=!B;`_21?vl1JEV+m|N3_(@DInQDa#~F0f6F!Zp_}q->8@Te(edu(> z-Om9~(4a##7*f{;H2B$?%1Cj!z%u@~JxY@CYgcb>I#g}8{xSF_yRr1Ozq>ox^Gb$z zC+xP~Exv}Z?uFVR?>e39fP0AWx;_iish%D!Qu8)AsMGze8dq?PxSGmyEAGNIOyHGV z?dD}bfkMsgn%QjA-AzS7bCg3m-Wp9mPVRi#;)D^$BL zCEJZiIn2B|*PZdPDj6AFnZW+@;?ovyM2O+3@y``(Oa8`pxV zKzek4I$;aL_JMBn|Fwj(@Jac9B;mNiObl^_5iS2pV_V6~8o>$nf2{0FvdCir#%oNM zVF6@Y~RKwgYNXNB9embawooFc5YP*edD_-{fz>uNy%0hxzaH0X6Wf zyX1`3>~YKOzwB|#U)WtG=g|0#T26ZnnqCMP2nw$Ih{E&X!2AC{6dqQE?-xh#_5?Z?{dC4X|%uLeI2dYBq)`3Nnox963x{ zE(=?i)CM1t1j=HQd<)4&Vy83N`fHO@+4n|TVC0oC3BFeN5sU%Iu4xP|8)5$Ms>!*- zy46bOjB{ zNiJPh1Qsy&VPF0L+cTIPnX-N4K@$xja3QajL9582xSofD#M{w^sB)hq$yctm2=P1jxTct- z=-hiiB3G=p%gHR<&shN-|3`8|q5UO$VO zo~;d>s_FAT7pm1CKYV~ZNIEoRAL6~Xf3OX7ue205#my?{_k;CopkOn}CYB8-jk^Z&x& z_7G!O_eOkvE@&zd0~o}DQ~?7zsyTwAttjgiQD-~(Hn;xl-HmHVv(@hI_f1|$)J#NukRZiX6*;=nzaoUbDoC%$7_|`UsUkBp{Y8-4FA!m++ zI+zXP4PM^i=b{fyOpW5ix68&@=}l4aKzS}~FX8#q-5A0cx2gShY}jB<^inMsN1ikZ zm{6IZ(I=r!U~|MJ-@(=ceKT>}e%p15EiBN&5^43@va=bKlJ2;(HsXeJOYCIcr6nxF zIG$XXG-H!v1Ym~dEOG>e1XGZuo}7b6V@xJnS&OY0f5EJ_vdY4H`SsyQw?5pFvNld# zk|y?keEo-B@8tUnM`2ZmZEt|rW_A|q4@Y=!2}_N|J;ps)03_3{EyOCOJ3#CGz5Q+o zJnDDhLTR+(7)X-N1q$4FIfO4Srj#fF1dvZPf?Xf&j-Wn2Pl4sM3Lg*F6{Ow)boKVp za!gDDj;<4EU+>^t{=P*0~sncG~(>x-U({4G_QKe(0-o zfzPj`Z|jWqLx0UZ5^BM10&bB6{%D6OuhMJ|Kw<4tnk&&}kJZFYY-S`(my(*A>(%m`}saz`=T&VU{( zYE=fj4Eo89LL?xHvka*+ASQH1Huw`9tn>%m=($s z-SI4=5nww0&Gqf27WbR$+Zd(1B#3Z15XeM%JD72nM`Nryoj{X-$@)n^1N;ga^t%7Y z@0wcg)2{vC5X7o-f4IPBU;i-Y1+xu<672Z_Gr9BdNA~{%bb`Z+3ovXT);wD~U#03$ zuyJ)PN)Z#9=-!(6XEwpkpV2;npSFWXy1^FP+|BP z#4#7iYKL+)y9NZf0$$KdP+W9{O#D6&Q^!-nC6KLvoE6dj5PyL;SF0@Y7gq)hn}ALM z%u*O;B5-auz|#U515jR-7>=JGxFxuE|7ncf zOZN$oThm#c;$$Zqk(k%pZCxkc(c2Xz86Jmuwqgyx^a8OHg!!T7VDx!pa59BgwmV>o zVM5)q4R~jEUq0(Gp7#edKrL_v+ zB77<1xJGbU-xpTyTH6~591gsn~Flg|LbRz&1j9zB$l9wm7=fYy-q%f zL~21D(aZt14f2xpd$(ce0KC^aCsvRy0`xx zNMQBx-WAV;#sxMMV(MG>&I`cGc79i%S%P!dA3cccM!bJ(4_R^-%jZ$@ic_CnHo7M4 zP>zM|g_rje&bA)~p&t_j*j8s;p?V?${%S&iu}DcDZVLc!xyqagb86n1fp zL91y+X9OJvZVhz>u;Rdk1fe6rfphR4RAPfcbQ~<&@aPFFlx4uh2gbazYre7uC4^uy z)vZQH-|~|7-F0X#KZ;ADve1hNjI-zkZDRDRP%b%m0L8@SH2G(exp~7)qPzX0pZ`!v59D;J@i5 z5k8Uby75I8%-t}k`Q84%z70OtSsk_?ecmu zDyGzlA=FbQ%C+6yW)cucqi8-7Ne68cvvF-)sYgd9z=B}eMoUZkFvO3jnF~%ErFoOA z?xDHvCB^z?Tt7jpkb?J)yPYzcdo%LHbDH9zwy5x_^mH1V=<9N#DenNi1VSYq%>_xg z#t|EmfH&Zd@F;$PL=hnJHUFD{v3_7+b|o367qC`C{trZe0fl1Qt-FRKZ^L(h1T?66 zg<$$bZm*66#PHFi>-bmY;;`wTl?(V=A7<01W@XvjE&3bnV$CtA&=ql(RgFSO%JGzO zabaG|Eq;SIG&pI*6OOMp1WVhwau~TD=1>Xx4D})*4PSv8f3>)DR0@YI#=4(%D;>@H zajZb0TDGrNF7J8}ZD_vqZf(+z$Rr@+(Pg{C3}*+H_TVlCUXvo4ESCF8rY+ap5piWF%rcNN1RCLG}}hBwW@tw>Tb?qKBZHtI@GVg}Y(mR3jrf+3MB3CH5lc0l%6S zer%;BWvhAW%=C1ZhOVXMuAMOMGKfO1t8B~*=&74lmF=cHA6|M2q7W-3Ri4qsi;5RUaxP<~?HK{h=}lq*4?Lv{RV5EQiG7-_mg_!GDQ&kZddDI`o;a>B*SX z{aW`vINfg6=0W&H-XmUHZoE+*^v$n)Jv~;p2fZbBM7?nx`frU|rLTf_3|{eq&Dl^g zj2o0Xw6yL8jK1ZE*_wNNG&pT}M``>Xqqqyf6(mNHO_#w04__6x)^p31?xCUVq5gw7 zSW+A4BLAJ7CGhRx!?y01`}4LlJ{L@ z1;}ZCcCLI}XJ4;vT<#n>Bi3K|u|}RJ9lNVpZ6|1_ZtxIKl`yOx(x%>w^`MLnh}>^wtqD9{NR)2O17FMw^M!hPusVt>zWJ)Z`KajX#7j2-wn9zjY9HpnAI zsrq1hyngQJ!>cZ>AZgqhM5v9fM__8`Is5=Y>U)|~17HBHsx!QGgQ_BVtcIX=pz)0l z?c2U+O|pA-KiIf7B`DdZm2g}#O|F4BXCf*gHKf7V!p;sJU=*BUu*q4C71U@ZTj{DO zacRo|wP|VH$mNSSTgpCufGMQN&37j0_aMDRU~}w8!3KQP3$FlD%Org)WTa}Cfn!A{ z=|czR#rs)VjS8udjsdI7YwQxLavL@~a$%PIYQLi%O?BWYCBmNqEtY{;=jIvxSU*wH z-lUx_s9ZpeS9zA5F>e^_Ps&rNzGkWKYosqF-2ieVL2X0gr{L*^@5hhHv6Fmce~Oux zj>9+sTnw;(gEwqw_7nygF^+jvL);o6|Iunu!YG81hWf_DU3wH!v!G{=PU>j@DjcCC z+qAwTDH3+3@E6ISN204RAf(D2hF9j7FJIo{DEC}5bO;*KmSZnr_cs(sj-!Yqy5OFs zs5#eGL)y0GTU?mEDXE_^BvLK5$pD$2>JioS2r4liNu-F=^p!&8BpV_lkL)57a2OAu z5c(g&1Mg21c1rs~R-FoIpP0fz?hC;`fj~MkVt}_)4sj^&QGHG75NFCGY;Mg^?A%q> zkpaOMLbPT*ZC;mGtF;0e6kuCL@$%O_xZB=zXtJTT*{Vu=gyFo0bT%Yn00SLKsSgb% zb32xIUE88n7y3X+SRQ>Qn|d3nt59`s$RtDI=vZfLV|{mZtQai5SSt`+P{^qNpZdgP zj}N@Ow0%5PYjxivEY5VyDQ_Z`P$>3$O1^UiYnSmjgP}9yGd5361yEWs=+n$+5sHdwzBdc#bIZ<|NHU{Wr6LG!1^}$ zL_l+czcHtV8pEKQ%pX29-(kI?-TH@LrR%j3buQC**ls#0xW9C7?#C!VWJsiLzd*8m{Z%K$>#r6Gufim?wy3$)O zb6TeZIWr)$nqA4iAS0La6tn|{>ZD>UG+2F-MZuPVboe{~*$lEPNajk^jTAf#APXX) z_V4v%@_0d;n|rzb3j#-TqbsZM5iN?(XgRViElZJQuX>XP*1bN*Yz4B%J|CSJX=v)d zel3`Gy~rY`iylz7tFpkiHo%s%M&d-6<(8*Z(!LkDlhpNAbR}&EPDuc80QS5BQ!d)= zTsXu52Uu3v%OIt600U{itIXEIL549FT#(Ad_2(>C!Xw|TfesV{{pOE~ESBkw77aTe7JgDG zzLWg)eih)v2~#mi8{RT*0=U${&&xNEvSXIokQNHq=QF5jXzb*|@m9`Gk5hEbI(;!w zEB_`DJ1H6h8TbIWc7Bwdo&DI64Mtd%nDJ4Tdb3_2T%yxLO2&0ahP!x0E%09DqnC;B zahSgqnBG12pJ-2aSGxnRAW)Ew!03Bfp>UA)3O6d_J1tB?59ApfFLf-2)E#E6U)>Xl z>-^?3KjuaxXcmoHYQ20GkMG58_ z5auDM*XnuPAX7-&JH=i;UA9rF<^gOzVYh}_c(rM_?&=FEN5IN90Ua-5SnpP`$!p`` z`A9HTU1TjcQ^1yjoff@!)cain61rQA4iFA{hP;ANo0yoP8z0>(pd zkW8ib?lxSGy>-^0WMFM$HXkuV!GRa7PY9k1;>mbu&~q9n>*h`f7}+vrCjE$qyculr z-a-M)-nN7}{NU5sf>^_r(+dGWi|(@Mtq=)!6DA6RMccH4mzpSzgx1pp~feD6USQOtHgNv$fA$ds38I~0p2w9Ygm`P zY7g2{4g2A_aqvn(vYm^7*La`n^dx@MC3i$JD6sh?tRGC3dAhE>k^)09ux+tQ(^E3z z=CvJmTBKOlCT-^)vz|E)~JQuMi%=I~vD3nKmBV_{cBM6PTrc&q0&kn;61jKC@88uCC zx4?yGL&?j-^F)v|Br+bk9LYbO`$Kf&`&57On|@@?!mb9tNajGEgih;;Kqhe z8}u^>D!^;^4a|CATdSaV+oH4Q!`8_?`~z$Z^168?{sZ*ky2OXTdiHoUWc>Raw`5fm zg|kfQ;-DfF&x(KBbOXal<_b&QMEF_9frdtSZ3IlLlU z(!%nk(IzAw0g+U)c3vgsCf8}KkB?A?bz>#9FbwY2oUN|1s*Jx#SaJ@G-rvZLZjF!64&=8kCf}2G zb~`35vn8eVRmgjn0++7*>>}h;kcPQ^$Pw>z@3daZY_Eqon-yAhJ=e`|FtbN;{T5ZZ zPo&K1nTz<1xn&XiJhRSY$+kz9pm2EV<>LXLxP=`a5fSL?y9e95_5BK4C+saPyIy+q zBus&F53&(~t!cf~K7_=ji0SA5?B9tN6e{Y;LC&}glFThCOcTm%R7GMy;T+CAYGwkL z*0KM>yznq+Bg>n{j*tKV!4Fku5-uJ&&|;so9O94@Q>iXe`wNtAa2oMMDxdy{=bPop zj@Ouj(QcIsNkisl`t`HfxyA6Y_#hukR5ZQGG3~$c^c=ozvg$dK(pVZY1mSzSP&H9{ z%TyR5gM%PW%tMuvu!r-RY@=B>5FJn1LtvnL4r+;Z4SV0YBVsR4T+H|ZZ}z8HRe)-> z;fa-R-=0Ifsp5mfN3l}}VmRKu1E{ls>|p0~MW>8d1{)L`WqW}n$YV<>^O!6HzcOwh zTh$f-0H>cSH7_J7UoY=?x21Zh%c@~^C8n{au&}G*7j3_-GYGPLxte>v4eoy%BP$0o zNE2%a`xVA-GCNXgC1ZMEp}U1x$VvR56AX^kW{+G?l*!W-(93q&r~3fq+S_(BLwLg)GcZ^`vDBp$oUp zgh34^Yg2y|7qrGvT|kWW)2H(^Vg(c<5EC*oXW@Ewg3T1V@X}@>2G7i>+}>El8qnwL z5K~AQzUmHw2HUa9!BW3LZX;=?CtwqSFt@n`o~FGYNZ5uY9xr-z3QDGgfi4E?8$d`> zYHJNizt{?ld#7SNcYCu|gV|uf)m#b{yE&~I8*kSCqN&MdAAbHoYv31lvcBm9H~K60 zdGUy)#8S$sqxRK3w2zVxctxeXR9@`MOE|3Y4mR$?AOr8CkB3VdKoA;m8yKSEmzNpk<<_)%?zHc`ayy$=Ob&K^uI=;Jy%S?x`!7a5KVbT@RYSa$Zz(ZP1& zru!$NQ-U02AIjpkjHkr7ueCjW_d?%%n9OM|5w~mdnph>&|bT38C$r1}d&2&XtYf$KjBI z^S}?4?RpZZY$n@Xd+`;nDS`84mWhb0U zAPKG)SeJx6&U16-{0oX*X;20nXLk^&C+lN(0$4c@}0CmiVtw+50w#AWP9E45;duPMo>fd9)3X{JEqNXb^I#l`y z?iK-?4}M55#CdZrb|h-~16L_J8vi&jFwW#ha;-Vo*nbnvLxUu4+}%G^1jBE~ZnOt; zY0pMl3aV%tVAlt#5@H@W&a7CV@e5=L_WwE4Hb+(@4^kZAOXDtqc1ieW{tid#-2bQU zqhE{PvEKr?Tb0;p*ek#g^91$rKIkSndg#IJr3o3h5ur~4e%vGfM_wZU1e~Cz{4Z3x zf_qIm$UEu!&&?Xo%SuaW1RpzOMe$ANK%u;y*xoh$F|_>ajpR87XPe6JX;s z?<*B-UOx1^Lsajy+OG6n+2=eJ3kPNxopu(Fk)IxLyrzy&R)O&fX1&u8KQJ|YTOr); z|EIncjtYoLgK7xha7C~)9%k_LENEE>AlB-KtSoq7ho}m ziVDx)ok;KznkxO@a9lzra;Lp+-HAlQJTy2Ncp#AL81OpTYdkCyf|&Qf&`+x+dXBnhLIz5FrPjJ60uv;CCG5q0y{OFPxzfxfzymZ^ zlPKb1rSjq~cV4LHD`=CLO!Z$Xrsy#{&BxDQKZU2E)_SnJY`8;$LK-^YLm`~YDBv6b zTFq5+bLNSq@5z~mY2d%WQDF3?Cw0RL7+f^`{~bU}na%0sbqoNvP=@O*TA|rg`bf~Q zR&sk*eP6NIj!IBC``a#}vHn$vMfAvH^DlyoH;Bmw~ zSDU>70ScuNH*Y_`0XS}C z1zGv;P+6QvrK^rQ_&wn3%2MBY>z58$HMbELYJl*;s|JF<4zLvkyS$VN1#HcMTq$|6 zeXPLap=K*NJK*S6%s3T~I2+heQ~gzqig(Nw9{}RpN${oLvpQi@sZGt1jUyB2EZ)mY zH50~F<#q-vtPTmd#u0b>ZXP>p3gETEsJ8}R?K&SL#yme3l|2`@8DAlJQbz$AIm~fB zNOD$Qp=>-_DcR*H@Dz+GWa{C71mI*=->o3rf5L7Np#PcguM*SF-%3o8PY%p}a~J!% z?&UEx)Z2R>_zoSFwHlgXGp&D)bfdHnGAUz}wWxQ(((-2b zzJ6e4=rDai4-F6}wIH#8W21GJ4SYTN*2J%EV59^a8blx&J$41(s9vKZxz{-ID`?g0 zXk?iNibK^lq-4(fTAV+25UBx$Hj@~;ScoLxUQ+fWAt?%^dIHgx;9a#G;E*I3{P7#4 zl@L3ZS25>-rl0*_&H$@C?P^(MLV<^vj0!>16$Dclf{{0ZH4#$7$VXmoZ#X*CFALN* zT6+@_4IZC~BTlgC0+au3F#EOkp8~%cfxHgoJ#w>K2ZlC)ka zSK92~b=Mc4&mpwRRxOswacyoGT}NX^3FOJ;+Z6CRD#ZjLOrY<7cv3H+_MK?`c{$2px5 z&&?`DE(8HwrPI$nRCV>Aa53GlP77LBo~hbbSMWo*`GL=zc)0l=(o(CC{6)GaN(XK} z9U~FHzK7#P@VKXr7OYIVa=L~=+6Yn<@a^&^W(+h#bczyyXzyHpcM{?I%l|e#)d$n2 z{8|aoSgFGL$oLDsxH$7nKq}AcLg@5SwhcG!Gs`=7K2~I~E51nAs7Ztm`zhFN(jUd9 zKx`)(pX+vCFTdS&DQl?Z5JUH0YfK&v1CO8I^W(t40I`M!JWen`L$IVUZh}eIVGt9) zDI~9oJ#2ru51GC$Z|X~)XA{S&Wg=NHL{s0ZCo#V-uQHPasPrjybVYG zJ@3$ZW9L73hr)uupGRmUiSW{PJc(7NKM~*s0dFfzUwtIoWoKV_8-{7nDcg>3Lk24D zcb||=t5xsf?R~{h6{Fb7NH49HH!`iiHEMC`Q0l(KFaM928R8^&cWXp8nf|M6(m>Xt zyWFd$RKJI}$<8JLC=D_?G&eV|zkE3!RJVWS%q-joBox?!e7@hpvKsAcNjxzGlghIc zhiJ5@)u-|L7m~C4z4V&DeJ1?q%4(EisM~l$$6O%Tw=xWr=R~iM%ya6sGPK*f6wh_q zbG6<~sP=s3T3~kO7K9$*G=y^Qxuz9E=q16Bv7!o)K=z-ELYFte`KDa18EE+coO<|1 z^ldAf`>KCVJQ|`;MOvl67&CDa*8oiXoSk8I4nd8HGAGEp2T0I9r?`Xg|F(HiZY8#7yKtxQ8)VO5-VHLA1hrt&hb20m44)|Y&9M&nQdUGoV zZ#k5P%!VP)AZY-iO_X~Aa1`f80FB-q4#xsn7E}Z%h6B%#q2NgN(%k~8zw<~B#QdJt6`>N=^3dP)Je_S-unAQw*Te~+3qtI$u`f(jL!;~% zz=5je$sBS&mN+CM)2!^x!H3mP)!U6)(X0kLkTnC($A#V6l$8C*@gfR9%&?s{c$NLl z!wRNe^g+WmH|yrK`IC#fr|_^7|HPZP9*UkFDxu_uzXzNHJV$Er@r%tacIA3*39dao z3rpj5$lY+g^@sFvhXjKgb4{{KApvXE#M8z0kwcB7DyRL2Pl#R3Q>ddKi^+|R@u2^1 z`ejwX>#y<~O`mhZ2mNvXM1;B#*dS_zA|KJcEbz@e~R!YKvtEB0OI` z%Uh_O^F?oE50y89C@Or{?f`H8{=qeS zuFhAI)tybg>G{LaAt!92n=kyxLdhUmD4TvV*b0LbG4|e?P>K7lro0Mxu0W}UarMBF zlB_J&iKU9^@LpYR5s@s~6jV$(;C^0K*~Tj)8eH4b@tnxB%o9IaK`6x12FIvw`mCD) zS_-}mLPFMgU`2-+)QAV1!(p>Kc<(1t45G%FOJ+MHl6_Soz& zkiU{wPyqA#6i5CykJsqod7#)K3W$LoScAvMpU=HEn}ku2hnYDn`JH0;!=v772H-D; zE7GwPS3$TUe~CpEbLa=o>GprRH+yrtsA`4NsKI8SY8`mzaJneHQnK)FZ{v*2<-4Ak z{3qO;WTtnEYbLPuLiq`fdG%C4)W_mR5h7h2q}F$%eb({kK3VvepZod9emgK%Ag$-f zGfB7GB;*6yNtdeG4~Zh?EJf^`YDNA8^7U6b54&4emi5p2Z0+TpH#?2dA1fjbtl;-(e@Px=2R!iM=i_$k-zkpfs?eL5_EewVz?_B(**l3gu8eF(WQ%l1Dy zXH7@tA!^MaC9O^()VzG`)5o8O9iEe}gpcL{BnO87l)`K~Wuz_-KZ?s@H}+$Xonbu> zSpJrHhy%j00AAMc4MNEY!E_I#>jI%-OP$7M5e*Cu2Ep_dzO~uW`r`R}z*q$p3Rq@n zskJb0wP7}poSMp%pWF#nRWSV^(=iH}a=X)8yF=BR;{qLJi-T#&ijVf&pzWdrCKK`OZ`v4{z!k9SysFuE^*%Gu$aD zDOaAlaXuq~11ik)dUEtaOm(h;bDAJb2%(;5W@gUh(@INQfnP{|7%vt7%UoOSk+$7@ z`O&^@E#eth^U3rvVu{T}H@z8(?t9lF0?O~|y=k1K$W}M&SX{H-fn_5c{1K?Sw{L&u zG<=n#;{ypUBH!=qZt|V4K~%i*DBJS$Ql>BMmkgDqHhEnmz*T;bui}GVmaR2Gy!-tp1&60F8cW=^` z?wmc7brpWu!#VC7cG>Zw_AeZ)-5FqPfbEDKoMuGdJ0G^`M`_3^_JR1)w`Wg%#$MIQ zRl>X0QeuX-3r5dV?1pnqosQ{Bpbv3|H=RwW0UsFp#*4cWhS7_a5?bAIm|X;P&w zWnyv`pff(M8sDEvt{+ETg6uIUB>;bh(8wMq{5{-?7JOBv_fEt46HfjX_V$LyrqtxX zifwV4>fu!q*%A0AC(58pZ1Y&c|*O#MNB%A3Vf`W1+mg39$?mb`|b1R7wBkm$b7b>AN!`Pri&Mf z?}*ckFO`oMK!^co8giA=2{zrW$jxJBR0rT7a#H55{eWm_wk@y(gy2wxJB6A=&yr-Lj6D4#vJaH<1&Zu&QMj-_< zP);?xyu3(X?nQC&+>aZ1Qa!~{+u*CcSBt{mz{hS}kEt!`0o$r>;)n8BN8wiKq zscKgac%Q@3d4Qcw zS~#rugV%szZ11<(85);4QHU5;LLrR&%%XPA!i9lquN0YRg-+u&^T|7C;?x@ObiiwB zmU>FLHo*m|7~?irZX5%(?CzO7H>-vn@giPv>Q+%$4&Fdns-KC;I7T*9fo0G6ZFaaMur~*r^Aw%Alatfsco+B+?qUgXSX&#Lw;+yD zjf}GMZX#3NsH>~fN!{er2pX$xe*`yUPL}izKObKpEmL|=5WGI%FA7?L@EFh~GX@)3 zvsW^P*Ol3VbIEYV{9}~p*<9m0cTU1{uX;D0gIaI5jc#hPJ$4^Ta&xeg4^N`ffz7$E ze8KcXp*^&OeA#SJY)DJj!BiyV0+YA-z+I9;C=cY${aNkYjbqcNT8qBtf$JjJCBzK3 z_;_S`9YPmB4Q?MN@B0bNp$9Z;BZ9Ca>$gsp?{*1w_U5|S#m6;mH4y23wK=db-QoOE zf2Lpbf=RUA+`%|qZT_~>s9u0W(24G|J(+Rq z>0+r6c}Aep0(Qd}bo~*g4rowto_SQHr0{U{;L~-xQikYu;+MvH{S+`Umx^cSijY8e zACu`Ne$eO4T^--&Rjz8^XZZ!cE}Pgzq#3 zZwMU^`WcDlm4RG#5sgOs%^fdj5TW0mpst0gl?w^OnfpatCElr1R7vVY@>^bnbRhiC z%XQPv2z1ui`=#S&e+^7aEleK(jMAQcyM^r_60bpz7*nW)w?vb<0xe(a;t9U351Ah} zlt~*tSHHG-sSyi46nOb#&%|>Ksq6*|cS!ZMIOwjVa(Um7-(4}>Ev?{e@8~!KD)B(= zPN~)W#Nguuw8rwC)pyK^U(9JfR=H$P?t*N1yazFhvKK$a)Lz5LE5UWDLv=bn@z!V! zrc^T5#pmp>_1&Hg;+y1}8PMP8Rcd~yn|C39-=*@rkMMH*R!p^(lq%*h25}8Y@k0rU zqg~rd@1~~6C3R8H)tsmfx3j52KvnUfg14WSL}Ds)c}QWAWc~n2?HDv&=1u%g&^Cykp{<<;pyPaYq&OIQR$SfIHQE+Lbb|Se@gg>4PNbT zFYX_Vv|az8vwJA%=Tf9YW;>Y(vnj-us(<;u>#B2K@jr^VOaNiAu4H;Ybd=$QAF#TM6AZtC{8nB#q! z2hHJC2oeK=3l?LbmgLjtq=t(RSe&5I(VN?7xtM|326+2QC;n2z4%J(y9$ycPWvb?8 z%-WAXt;IBRC6{Mt&{6n8er$z4=g?2Y>a?s5;+tSK+IO6uc&x(ViaguCV>lP)2dZ{# z^y7u1;uuZS^_vG_+az%KEBpTGHC~_)4R0{H^;PoTjaiqro25U+>%5U$tAOnw>%MMC zG|9^`tTNS6Hm7s7H}^Ckh-2{5H@6EnSDt}^32^IRkzLw0n3ONw%z2x9&g;&-T5GS!|MmX%AVo*Tw#RfZR_(JI+`r5sp(Iy;;dw@N~^OUaF4vZF70Z}U$A`1?jjn;DDrkz z&v-;;cxTFE9F4A8VA<|YwY9R6JxrX%kXJlR*ie%Tf=CCmHE!>nEf4P8g`+A-uV6@K zq|FvFIZn-Wvu!%I{tK~1CVM2ACdbNFaW#X^y8tOerT4Qy_lMbyPuYC0*`?|4{EVWr z{(a6YrlQsO5mp!T)~hWByAjyStCPj=PbpP~_gp`{ZineU`4$orkr*&DZ|Befc(G?| z47*e}FW@s_cI{5gprM3jo;ezssTfy1DoV>Mze-=qW{Hta%_`b&l07{L0*bD%L6!53 z+Ni(`R_?mu!^7*BPr^sr!)UpObiw*q4G!osg(iv#v(9cWMh1&MzX^7)8F0$q06i!f_4){QiS zNXn^AtUH97L1IOS*U;*7crB~;bPiEVcsu+uOXn1yoP)tNB5nsspVzIl>`A+q*vru< zE1_N5izk!mO)>zCGf+^pJ>Aw6c$tNctPeLG3> zx7_5P*#Mml;9H-~a$mjDRfCyJ3Q*Sq)r;5P*n44VjiY;}d+E~qzLiu-ez+}O3|kXj zeZ52UL~;i90RaI>Y!(5nA3hw2ic|^M!J*CXXnKwZ2Q~+qfW%=Lkn%MauVLu+d|@B) z)+ON`~ zSJr7MQIt(MA$#w=D%oW32#4(K4vzD?Kk5B`z2D#K`?-F<>vw(s{GQ9J7ZS(wJRgt8 z{eHXOMzQ1+UxEP((FIfquAFig&3FJ@(x?xo_|`Wm zA2OVQm!!!BHq4IP>Ga~+m6Ysu*lmHbO-1MnS7Tczrwn|E3^*XzPk`e+=^c=b`VrK| zZ#?1|?ZG`bvMx%+%C_vL&ECb3yhYn>r18yTsWRETGEts5ea74wU8mcgaSVdqfBizO zbSqw-ox|u>Ipu!-WqUsk9XKze;JkINhXT&bI;bta?nsPH6;Ihuzb*LXL@kpZBhXcX zjWPG`Ck|1&uZsHGiiyg>vE2`;#13^&fiWL~*JX@qSoJ{74@( za*t_t6fsYrz4^@SI;*A*3tWblmJ{(EW^FqT49x6P4sm#~3DMrX?A~MDdZJx-cF#*m zStz7e)GvhiB|#p0faDWHzM0jJ?PEkL?A-Y2!y}q9=NXcw9@T{Hzt-VG_SQ2}k0Sa3X zu9ArE{+Xyqc)_6pHzZ=m1pR^4(z|HilxR7UeH~=eqd0j-h|)k>LLQ!YM0r@i&Rv7R zqv9$vFktL@(+x}wFy6HT-^6H9uc%B8Kn9i1{f7#&SLi=gkf?tJW+}eq#rvu15Ra*w zR(Nko(yUUgJH9?rV9oT3HFG$av1O6wvkcW*T|xwx1LfTO6#Cw`w)zD-kUcca2A=bPCO2>c#g{S*m_ zHkx6?!W9Ea4lK@*06zhO@h4^!M|UsiAPUsDZZODM#xJ26AhT}keu|9Gmq);vnK30s zL2>Eb8+69M4r)^62J!{$n;DIgL}fz6lFw0}JC|&O8yP{dy$p&Ac;5Il7}z4rnV$}& zPv%|VFcsI+!+5Kt*-PH0iAdpsJ{F2|B(3I8EKCgbuchdP`ugVeH}Y_31uKAF1A}(p7_R$MF*Wmm5*SfI2G$dyKr&-yY1x%6BQCD7mny1koCOAxp!B=7A@wzq z*Ay9zqi(?l4lN`R3h5DH#BVZWz@qRfp~Fl?tS}>V;vh0MfU6BFqNk^D?EW_;ptSQ5 zva_?L;KV{fIPRSg`#K1u3b)og`)o!dB2krK!9CjSRjIo9Ug=&eq-ptImxJsP$dN^j z!Fc{8+P2wwz6ZK*wM9IfF&!&O-zkONXXoTVi{Q;oL6LiioWm4|ZLq_@EDeY!bs)C^ zKW+#D_W=?Y)SD<2`1mA_hNJ=0l7b_rE&68uueaZnl0qI~6RIdGJ#h|gE7)jNpYO>K zj@74>f!^{ouCfGBX1PfYput}|7{~;@uzf2L46We@Av@`&s>Z8^d)H|W<#FW5c9cc= znz86Vc@2yC(lUl*=gt_xZ3#sVM68;v{2VTIgt<-QjT3=vHL zT3`wUTd}^23nd_(?--AQatQdb20uItn&bwrwuJE}rlP?E2P$X2nG~lAn%Ck^TE^-e zTqdh={hn%PqGIdj0lFRsapO1vHeu+@!QiCE6}T7Cjt`YDG3Z~q-`y07cIgpgn>H(= zuQd|wx34U+fXWQOQqcbb5ZTAo7UwN82j)>Xr@8hpCZZ3z)U5oYtc6&c35yvc0Kvp5i@s(b&b#qX!Sbx$_K4cwk}+mKc&q2VLXmNBo4AFUv=c0<{T9 zr04k6HXvFUNI}rM4BNQ21uCQZ3(SMP!;G;%Fm%wjI}UhrQ?mY;cH#hGC8W4t&U zl79hHMHP^A1XUzresg?^s40S6Rpk<)NrRBlo8T~gmm4y+h>dJl{tH6l`VR<+Y1ER5 zVLnC50G-KqMA+NG(vvNtp`*Xp${Fw7E zRlblzmfv4s`z{2dcZfg8W0XKj{pj_)O zu}^S2CJ0axSRR25=j|rZ`$gNYKSd@dsWwL{$%inP{exObq#sXU#;Z2_hk8CxZ*=A3 zojK~i9n2`bdQ9$r4?ML1@Ko1Z?J$MWS#OuC(#n)FS-sspzg~7urtF&mH_Tq}3NWj*4 zAwD*2orUPtmZ@^xxS7!7mk!Z4*lp-g&XiUH-5*OJJMPy(fC6=~wx&)=h)eT2@}1nw7}2Mm&d6?p#p(cfo>lhoPLH z{H9jmi-KqiSm{V)E^MZt#e!uHyFUcpZuKz{NYt*a75qA)S;?6va0`{=v1tPr z5%O^d2930`wYR~s>{F z4g*;0I^Phne>#IR-bYN$+_DDMs3esrdjEBt=t_6UB zh@Uz@rNEznWR9qHf0af2K;pai=x*Fc+bnVv>|!UodR%xP_!=}ts;D;k$KW2^s~SrE zj)wyTi7tah1DhWB%D~(B>&lS~*sKbp(LPs`=tFrK=2swDYSxsxlf(^zy5Lv_&Pg4c zE)vnJT|8`YkzqbV%7=B+nS~)S@CwsOq`+Y!{oSH>aIqqi^Us%sH)74kTrfU4GtxbpTHA&&kGM=)QWO8!p|HDtwD&3g5?MzRsQfHzpIt z;9@AM{7SRwm4X=l%(ae=1wb&r7M!{e2?bigqS671P_p0MN}?gMOj)wHeWd{CynaFI zi()FVu(+&`_nNPRl31ds`&x?Iq1s=;ONB4~L0G=1;1qj@hpo0xH##h3({|%>z{#|g z2jw>TUh1n8hp0wDLbQf)QU&|Er`uxb^q7+%C_3zaeZL4@S3DfO@VecMxjfV=x7T^M z2e>N-cwX&=j*G+vo*rV+_R66zK}I)xErdtSI(q|%e8=HV zUwI(7O#B4g5-h{uRh(J?)X?CM`uguI)yBLx@Lc=!&^j$cV2fx%S9vFZmw|0NU2>KO z$+JP8Dd0I@3(6+CH6OX&%(uv0$?IOd{rEl>WM?<&5@<%wg*O@C^m~-r#meX2hk9O} za@R=#PrJKvRGlzG(g7iPx*mHIm~l4^N!v1MG?JtrYZMcjQ@psHKT-Bc)CB*L(#OKm z@^t1mPMxw(dLUpp@%y`j09Z)^jt0x@H5oCuV?rQX617Qp0JFY@3_%nGl7J50-0l;j zRu(+%T&j)4mfVSZr>-~f4W^+*w_OSl6t1-lV%>lP5>@2`8uz3D`9~W!{73ZopuPs0 z^R;{A>%L523pPz+mzks4mXW8V8|E&0RiGA3JLA2jgI>T;6OO!#NHGOY7$FY{8F@i_ zxY1pT^^|_kYaW%H-(YX}Q(giQns|e|vrY)V9Hfo_>R!8c?Sf=C&+LtI7>~ly2bLR)#XGq8}8urH{5kH z_C~*CHD6uRpOd>w0xYH&OBuCPH}zb_WVKyJM2R=Q+8|5T0w9RTBv}o`Z|~^*pwtTY zHM?neoL;jRI6H1DpP)@a#?4?v0e0G}yEj2ojyDTlemxWepTh<53VEjA2WC}I=ow)Du*`Me{Jfn0(p=QU~+R|@rJEiPVc4|?^yj_hw#zl8Tl_ZCzsCch6oqMDC9-`Z| z!PAuS_DM}q7!81d5UcL!U82k&pteL%3a39UBjKr~$0k&BGxIdt>)Dikw!kA%SIXvj z> zF2BTKFlzsQkcybz_A9LCn&I$59`6BBc%YK%v%)7Ve4=0sK9n`Js zG!jUQpPR{Q?_E#;gJxLjAiV*DqhC~~IkaGB1$U&wC@2~527v9v$EzT{RFJ$3q*f_<%_rTCC!@Rs&x+N+!OYR zzX+8AxnT|fupurh0XD~g0D?0KOpaHfiici&pQ&2615*k3`S8}dlNcW~jhq{RCO}_@ zk55x#tJh%R440K#8ZLd9|3)Tz7@I%5y%#EH2!~8wFD%rkMlgDV-*z8nfVB*cVnirW z01=f}Vzk zr-CBXnEDkt1HU#PM0*#4gFP@_h2t_!N2gjEkASEs8FX{dVL-1xM@98@X%oPe16L#? zl_bwun3#J^b)dR#&B`{`#=_T5OJmYqV@>Vd_ZEvxr>3F3rY?^ z%RIqSly5jZDs+|iE*|gGATTTE>MDQ)IZ3y>{&-9R?e^#S`3!I83q#N5Kst&z%|eF= zZnh?mVFXrh(r2$}ys;Cqa*FH~V%Uyk;~EUX8P?d?7~PQ>vIRc}Aj2$;B*Z%wXgNUb zKwL1U(xnb}HiuJgS;MnK(k^;?lWzWqjHIquX<~ypwyL#`wg^u!XH$v;aBaXF(r3>V z|1(gXOiWEXy1Iy^gJARhJ?_s%GbNR}3NLe47C!Pm6nF7`|gxmK;x zfrk(04ZpBRhlTx9_Lj4k>U1lNxPW1j9_b2FEc*bHW{^NosE8Gf#TiQnF;ZN3yi~cl z<}WK5#0YhALTRZ+7eywcG~E1>Xf)v&UL6d0N$Ke5qMU&{0gc@WA@LtTx0oUTgaJ1Y za66Ft1lU9-k2Bvh9)-dhF|?6x2N6#64g1f(X@O@8bV(viJ4x|S%;{mGCrEURX=qH( z>O&~jVNJ;9XEtMbXh&uNr!Xk3pXgw}>w*D{0QCfo??d-+aYp6o;g|XO`Nsi9jpEU> zLavt|jB@PnE;&M{i+hghyX(4g!T1xpqUCm z(ljZc1^~T-(27PB_tIyTEvzh23E6k-$(-H)VV(U)a}GG$;9%N-4WN9(EwFYWDBvhi zU!t55F309f;lM;e(=0BvSr&$u;3>i_AdqY>^3gUm!G`qO{&Xbw!~o?{c*UM%oqa;# zoPIPx^_ayh4^X|3PF}I9XPcIe?rdf^017@y{+-X5|LNS#BoGgXe|7Ff^vZn7a67^^3l8kz3oG0Yr;Zgq82dc8J|)Zo;CLXZtr^u8 zJro(B&L%szbMMvcI#o(kFIx!1xa3gn=Q)$v4=24I+-q&Y4b-;0jTaPo+pC625WNsL zFNLQGqc#cXVijsrK;M03HFp5}O$q%x^R^R*avRbpYu(M$zPq1pEx-Ybdz zr^XbQWO4MyA_>l{@6pG+qC%r0@n?fMac3Mvc(vL>ggHWjsAhLM!o_|)Yo6W7*Uak4 zKFL~7ih@oKF-C?!bmrNFT@U}g`$yU9k00gxw!W$jKIpkyr!DoiqrCaLx)ox*r008Us%yA{69#OIF_lo1Fc=xWp5e7g6mvE#K;1cqb-SQehZ` z7tk^hjg&R~_>My4Za_4Fl;Whv>v1$^9KDZW3)u=4L~-I`vTpeGKBDJ+1&lp&8eqdu zvASTgHUdg8QH;XD0|<81Rp%!&smGCXS?9szdd52klO7NPN`wqOg9j9)dd#=Luxv zbg?vLRwz?;1TjdX6xH~@#^Bs=8ZzlpigNY-9aJBe4i z{~cmYleTtwNsgd3xH#f$?CH4VE69Mu_t&e#X^&p6vP|B{%-h~McJ{USSJFx3ewfop zRc^{@4{=~q5y{g^Tv5mj!2+FOJAdy)XuADarx!>Kv08WUKKG{7zgrr{codk;K%9EU z-aNkDeHU}y8UPt_+2h2*1y(RVdEKW7MF5Zxm;{Ga1edhuUjhrjo_7Z}s{}Aevw|TF z5SOww)CMq+@K#gvq(9vKMTlpN<1EV%%hPam_9Prp_OLiYLkznLgCg*^hBFL+I23XU zVy#mRrgr8(7M$!z4jf&Qf>BAEJ!=dkRw!sdv;XUyhlfYqa=I8&i<+BL%z**b{d_1> zpb}Z%@muf0?4D_;N=gioO{~*O9MB1rNmatB?Qhm>GY0kR8MNKxWH5@C_}vIyB`w+X z!jVynQq^kuA~*;-4k*%nl5i<;gbGb_OGZ#m@I9T3&eZ3hzvD9>u6=1vb9QyLe0IQM z>(P_FcDs5iHBA$mQUwKtwsfZ14~H-BGwpb@_eV-mk0IjwHEbk)aT$;7GLG?1tFb3y zNWG>%`hjUWNK-AQIKt49kBimaFDXQ^X3!?9)J0kn&v+EXrCg>i=R)KUF&S3yi8?K#%k=%8!rd#VFkajMWzj&u5 zS}%8h)=u%I;++beVLC?$ut28Ym`p6YLEUzuTDEZs;@cvUj=hmBidmJ(gbf4LXORTRU z(CBq2-#B!_w0@6WxrWvmin@dsW#yme;~<3rx!rJbj9PIIQHErr=D#}79zkUA4HI3X zXCh5fXhV6Uu!*W+2cMp;!oO|ly;dj49MW^B$yF2+xQwT;n>fT&Wr2L0Rq^aQk z5E`l0d+N5>{v(3=|3lLoJtH4BM}w*toLKJ?=!~dN!N@A#NLb+`G+*yj;{#8py~n*e zrzfU8eRL@vHEwvEYgfs-HJRc4{U@RFm+iTXYe&$0x#J6w5CC~fGUt!Ppmx91*WPt*c$(963yin1K2zgwG6w?$FeGj zd7n{Bq%l47={A57M$cQJ)UQMKxy`Q=%Mxfb(&e%NvKsp?64map3@`HWLIsadAlfr$MW)ge*3s1W}DV~B^dLIO6~35T$(@j z@f7t1kA%H}I&>pqyUT(0*rw1lG4FC{km1lqGI};g(PH4`z!YZPpF;#@2G0O@-mKab zX?1Z)30*d8iu2Joyj%}hG?njs}75CT-6&fni$Z5InfY+nEw+fwm? zz8uMT0#3xq)sVSn`2A!Yyi(6%v!t;7qv_dIen?m@SM?5$_p-m9@96!#rv;O1{OnC= zvJh?7c#oQ19TL{DFjQ;^Bvu26Ssi$lU`AmKXeUhPr#%rd96n9Lv3vyR9Pb#Vt5edt zo9qvq%%+595@1LW@#q7q7r!`Q^UM*>5j;hJ-vPoxJptkaaOrHC z$DtBP4V_@`g--1qsT-vdNtkTeyIux-2LsW-Tpg&w*Zdv9g2X>82yRz+y@@(&zbwbE}wmY?=9!W49t06+h6o7j;m83s#buz2Bj{>Z+yS`FAIdJQ} zL5O_8(BR;2xV`-fVgPgn>QhkHGg<1$l(@Lt!Q?QS;+?NhpNZz7d20Z^bg*6#bQYlJ z`FVDLS1(APf;L^(bGfbpXGVqH0WuCW@!5J-estJFd`elFJdV*-bx8DO95#OqFDbb!^&!@$<1h^YbEy(~5u9kOC^eFFxjMAU``_I95zRtnLD18TD zCTPYV^|uVba{T)GG+Y2!5I4nai1Z;DY)d07)C28eSE>zd9fFj9vIZmr3^bBv`fmLT zAGpcP0P}Ikgv1mxyg&$*cZY!T0viKN77e^0v8XEtN7Y`wK{rTQc=xK4M;T$2c5 zd~cJ-?!Tm2?vklqtgf1c31v4mIkW0o=V#DdsDtzu7s}C>F6AjmBdJEH0B0ZcW{d#u zX#GM#ShEW*g2mBf2D8X2P&vz>;UfIA>tXC~%B>d!3Fp-LJUkq25(8?XX|E`&y+KP{i)ih2_okHHfWImVN)o_gpzTII z0b)ZnZ**|iLI9VLyLnRR%%>F3EJ2XCm zgR2nkx;%$ye-S!?)ov{>uP(7V0Rw&=)=;#ndg_u=kwkKQRTVv$?A1ei_!CBKzz&0u zsc1@a89`Q#%kq%oh2R(b{C+!E3+m)jS+FdCQJy(A1Nt8FjV$$@> zz^~L>H9k77!Cm>+=vlt}f8TA!8(VPr`>>cFhdwFt!{!~a;kG~d+C}L=3<9kf%q5T+ z<-Q#sk_L9EzpK0ZAo|eusRywyFK-ibo;%@37WZ#8MKXFNX{Q9(Eii?BNhdvon@0vqE`jhVJ_7`!b8R)_=<^&I{Q1%KIiK zB_WNx&9@$MinQ9jnYFPQLK`G3L0FUA5+tVOx5y*68>gwy)r}>;vqYw6BWCNuEWvTp z1_rk$Rmwp$gLGEe*$S%#GLMV(%q?^qlVxX_ttIc$LAC6!AL6cKgl-`Er++uGd}v$g zuzmYd$~<#?8$^doYt8KWvez zt)&7*r34}ySl0nA&gZ4gFEAk#bQmplOg0fI1dw#b^epYYRMjR+K0k0&@tpyyCCe}s z&BSJni>!FpI+;?~>u0~gnL`h&>wJhzRWhjZxR9-c!oSWzv?*A|nQ#R4eRPyuBMfdX z;F^8!>G6U2I%F)vO*F6}{y=emd|r%<&!_%9K0p0)zem2X&A5{ouG^Wdxd{aReb z>48o7|1vsP)LmD+`qWp3#LXfuI#i1<)Dr7l?U~&VwHRMTd6IE@aj}}PdE2H@#&?EF zU~UY00_?iX`V<=NxkLYMw8u%ZR}8aCO9wWXBe6g9Zl zN{0!c8K_VXr`9=Gt7Ot9Q_G4FVm%gbD%$>6KcCMkC{@&qOwA*HoxT^dB$<68Rf?qC zeLc-YBT?D$Cfnv{s0C}EOk{eI+BOm?1R#QIy!kLa$If52fx|;Vu7giN_GlJ%QAkNV z75(H)C%ZRP(rbr4(Np%aYJ@@C^Bb6w{*Mv+IZITd>6;*1h0PEDrCQgTnbkll1O zNAVa8Qi0~*+g{WI&ZifXdSY^>6WUx80^C9^4Ov6e+(^DjHfgjSOO#1h7I1|>3Vn>!r3s9tzQWxgx&32 z<7P@>cptJFMWA4Z(xkxCRo+WT`J5m!NM9`ZhTN|D0X)bRs?*fygT z5B&yUcZ`dbo4UDK4&ETvq?+E0S@Nv^y|X{?PJ^d_dTk72poy3fw;zKSGx55nMeS91!~~d zRG|0TWh7cJm>Ixo^+Jo#Oq_mEMYl|8QB#w>H;hZ;&wd47K@J#$A-=tCv#H77J35}i z>=ZT7=1y1`-L-shl>n88;=#m0(zn4R^Jr9A?_BF(h{cB!=?ZCpc7@_o!P>D%v z)UzzHSe?!M0B&X{SnkQ{S}Y%?+w8+aqZYzr$R4N_2(HMhyI~z`K>KX{3sMDXkp)% z{c!Xz|M|wXJ^HgwX)jfQXGmU4>NazuDcB>}g+v&z7mfZ4)A0H7^AIKsJQCmtu$VgQ z@DP$m#HSD``b5`=!NuiZDC(M|1 z0J=6|FUs~@uiQsrwgzq$#p+eKDF#c;mVKJRwwg61=r`?Su{;j?SU|NEx z;ioHTi-E+{+Q2N?LYqvto$dm|eTc5;FjIc03o3`}G&;Yf{y69-Ra{N0gyXHneq((d z?27I^WGG_hsFbn{}U3`c)wJwkTaL)m~4 z`r=NUgkCA4#)J{)^%;nE0a7E3>S7;KHf083TdYKa{CCO?OwcZ&bG5a#(U!`4fYA4f z-K;DBElhxk?SG*zgUQEwszT|=ZY*}Sr4P-bR1+BYoLj?$5>y$XZ~@1BF6omm!UAC+ zlBrc}u|=+xfmpU`m{uNYxA?gDR4zvf_sh2Bb&JDrxK>tY2bNLSp*IkLLwo!6hUm4=yxh zCqE4o+jZ;c_~1)9zV4n2#!ldH$6-3j2?a7{=HFBX3KgokY_mOx2mVl00mAcNFec^O z|3qv^64J=BMA6Q@=~EcJXSf^h&8hhFO;g_vSJyqgo)%=>i#*PD5ykH7k0#cq_XJ2c z-|%VFulyxf7}3LB^!Jfw4;NM0(9-X$2d(epRRY^vAzMz*;mwK%B=ugA zGiy>crs$j5dJ>krIV;U%-KBJPRxK^8X6P%$TEBMQ410LqZzFj8 zwYO_hkvM1a8xM-_=SN}}&TOsnZk_A+)cC=*>ywQ@nAbZ~n6|-MnqFOL=9*LEh=Mv8 z-Wa8~BvcYr1A_NnHnUQ#JlD0EIhbPb-hLxwn`FOTZa%!zyEAe@p$er?^n4UxwR;39 zlh}!Bvp&W2p%|R&uJu^&?;UswhBO#FR(P@E{kP2bxaf|$44dTd7)(dK@0I(KyyijP zV)+?1u2=pPYD-+A0ILeDC!n6|>2Nb650y}+^JCm=x3APZou8AIoE?rtclABNVN7~6 z9l7Y&?xBrnAC%%pd|m|7jihOQMdNRDlofN{xPu* zLrlo{Ktj#Jn*p;+)u0couge{jT@a!&l2-{Y! zQ-XiM12AKOj1SN|-+_IqsK}xS@*Gc3pYpG>o9SeNUe~-=1L`4|(wM};sDiRwY=7P5 znP?P($7jhtoGxtlkRqQdl@Qq?pvzDGC552 zfrxpY3!}AZPU2st%%%7@aOc`vwUWJx#RGFqU*#6xb=-%+b+w&hEB(o&qC>4a626a` zSINS6ZzuIMo1%kB`>)bbioF{!{h|N1XQ6=~P8!6?Pfw4*NP}_}4E=z!0j}bQCTA>@ zwD(~$&~m{97|zT~=Mr_P22hl!`5%?%!ATv!4ko&livM~t9mRGUx%C&H2^AAqHqSB| zyREHB4MJq02pM;0&z&!c#J9FAuIM|iYDGS3v*+zfZcVHC+-PN7K9pcS?CC8!ZMexM z*Br^-e92C!(c2KIH`o6BxnYp2-X>*!GapPUdctmj#KB18B_MSG5<+k((AQIxn zs_8RH|GcsoQrdx4l9f#&@xuRBkaJkUn%h^9n_EhC{__La2T*}kK5$)c25Nm-g(D<8 za^{1S`AGhSe4_CAbRE=~+pS7{3f1C#3*MaZY7?z%mDzeFPU{;Rew5d6x|exq4y6r) zClGs5Aic;jOndldaQ5ZNe^Znxo+xTE^9cwH{m4uD*Hwm%+UiODiMAF@zBA@W zGGN#99bh+nUG)TXBrAs#j_nJubkTTcZUj7ya$Y-jv&}POJ{$aJVE5S?_`8Pd{X7;+ znT<>$*3K&280(Lq=J(vyTa#r=w<#xf9te0hZ3>6rY~Qsa1N27pE?tC}E$x_{Rl8@k zVJNX{n^CVu?$A?}SF<_e{!o38&}z}9l#f0wTOiBmpRQ?QrmJa^t8XpdF{YmnKo&RivBE+Z+poA+ISyDEfX61F17ankz3~Jbbcs;FZ~s5o67?2r+%5|3gn7klsM6Ik z(xXQqbGr`bH7zvCEi9H_uQ9*-4j+*1iDh&9^Yhg*xbOPL4ZhwxL9U9`L%d|C$pPW? zs@R!elid-g9_L@_Z^C@$*)$IoN)DXJ*fmMI*abcZ=QrAkQEag;`o>yQLO7MUHt}#0;ke7*{?Bl%$9=Gj3V_yf3 zZP1X?yZWjX*eE3N;YU*v(Zm@$qj{O8-iJPo5>26x-dIGHkm@HA2k@n>kCJq%J<&2; zjSzEn71j3=x-T1jVE8e|=@X5`I$M`7;FoG*7(Ur=6H14aQU;;=!S76q?ctiETbZMu z{m!x>_MV78)Mt?DEqBUtfmyRswMIt>-ew^05D*`G-+05#3!vI^FlI+QzZGc^cfhoy z2JB%;i-8N`cclgdpgGX)fs&%bK@x~5++bXU{__f5Gm0M>H4jUTyeTiVL4smH?{IrB zZF-YK-1g_u-G$L{q?z+RyMGXL<_<$5=~LX?VDA~k>A}=DLv!HglL^sH7ObOgg3;QiyOwL%HOZeSn3{_b(xty zfWhn4ETnkb%-kqCz*c==?DSYK3tq`EAhRZoK=diNCet3@*_0Bt#vW?W){bkBMB6G5 zVbsu~uF}sHUq+X21*mAoNXa)iJ5fu4Sy@ zRPwDAcKH^a7rsP|daJjT<6pla{P{_EI=WMD-dg78kZfAIsb&)@TJ5=>ox3f7dJ?$n zb)at-hgP#zr1$yczm2X)WhwXD+#S6z85zEof4pd-njpfzYXg!`li#}d@k3bIb_Z6K zGIohCU%tQ$0U3Qts8~a-13U*fYR{dRr3ya!mqz{VN9DR<0hqQ#ec-9^LXAfkF51xUKkDy-3kKdZ>EIG#LasYK4 z{yPhwb+vUc;<7e2GJnKnZvDvA_y(t)jVTKCgn)kiKwgUU7|k*GB2wvl;z}sg!4?#X znDfYC_zV_f`7jY8!H9^ZL%2tGb)cTd9+g(CZc{6|!8 zn*#rWkSHyFTiH!_e#Fa(wkL*pCwu4FLKuhP#Ib($ex6hJpT9r+#(I71=z`l(4lc4r z$x|(n^{0kBT=-5M^OlrP8WOgb$JEmfi`GV6rC2fB6u;e>MbZ_y?`QO}J{d>af3KZk#MvvoY%5WB>;9HkTWW&pNSRLYVx`K6^Y#xdiMqPF z;IT7j&n^>A-8pjc#mT)tc&rhP@Jk1#e|-Wz+eOV6LGjO*e2zQJ`_D&rZglHQ?)~$I zGmtI%7CcR~?;{_-O{sme|2_AB?z<)CqJb=+f-OXML=mUv-O$C zcPQ!iK1Xp9g?og?&H9BM$S2T5oFEP-35_d#&xMO*kql^Gnfo=$_N9huq3MA^(Ga1T zx+RKvtbM8SO;@$sEoyDMDfY5M0vRJu+eNqNcK;087~6d_78G;z^2AvUykyS(=yZBY z*Y&)XD>lg2m4K}t4|XMQDhf3c63efwwrXBqW)dW%X^Ih{JlD!ER;H{&+&3RngnzVbY2*>n!+^^jH88^x!bI z5E#)%o;}gOb_7bP{og;Ww6xUje9M$5QHwg2mlqE6w3(1He2E^j(M-7P_|_!f;)w=< zD8gRKs*Xv2{M_%l)sE^4&&GB+ZU@h9{Vb%Fp0f(?_@um4PEB;A zFhMM1oMo6&cWK?oBQM8?I?+@O3z?R0Fg#3k!<} zii$-3bq7$uz(Y*xkol@#JkPOtJ!B{1lKzqJfB@aq_kjHt)ilmk$VCqBP*-;@Zs|fmo1g(lR?$j1tPHiT*Mu* zTESFLn$363TyU5KH1jA4XYyu@La+v3%{<|ZmzVLmlJ0ZXSGRTuZx>Im#^1fu*nK_W zQrQ`u*{_-VCMZnI|1=|y6wiyn%*-oj zFa%*u)AsMCD~v*fmD^b>64_3x4>hQ`qTLRkR=b2k{p6~;yBce(4g+LMUqmeV>I>=# zkSKZ`ljGwLV6oUdH{55N;x8732T-~YI%Ry#uu$_+q2_AYPx3<{@|6X&qX~hce&Bq3 zp?AU_;13|PBHC4-JGU}^B6JCg;FEV&?$+{0=+Ga$M1CNg{&1CW#t9YIQh~uI&NE`f z!P6me-&+0d&s`+F7_za9eKeL|pFrx#%R256Nf9LLZ;L;Fp|{~9ZSN&M{(H@))1xMj zFfnK$&Ye~N>fpNe#Qawb^=8ut-mL$K?Qt?@6LWO-O#S5HX`*GAV zjD?M)YXFlc%Glhlyv{V#d9zCHp35%wxi;2ccV;R%kMkvA)AhIR7{TPHEb9lOq0^2k zYHGx}-+175xyLxDJ^%)tWo3eY@ZR%-g=~-7GqpXSv3RQfT&9Ygj%iRBzt#oU4z6HI zdiq`PRE1T?4J=w{%6mV0)YI*Jr@eEek>SvgeweMvFvW-Txz3&+mVE_8tFgpW_!ANK zyw~hmixw<2lJ_pGzOs1}wSs7%B(&%N5q_`2$uNwe3p^+@%~WH{!3xXJ&Hzyi5DTeU9To zZ!;VgEAOYI36JwF;#Zw7zLg%H79lyq5kPjoB1AuSq}0RE5v}95K|djVx+Y!vI?E29 zbq#y6gSGPHToa#K>oBSXM^rq1>e@2>Z!Gs%6IYK%a%&c`38C-Tr(KM-+Q25$SW|ip zyVUraMWe#&waNBKSCoW+WZ!*VRFx;kpj~p9YU8}1z6sy81Ytq#Gr1+f$=wVOJ~HdM zX$A&j*3djBS_WI_CbnxriC;4ee-zL&8768OJkV0Y%|ETfuE%&fn+W42wiP87>%^Gu z=u%Dx)X;-ZGIoz?Z`7b=SyGFoMH}_v|9WCd&b0;vrta| z2Kr~zAps)PCA6>~c_7|(pyCFy5Qo8l1W#P8nzmFIvv*`D)aYU$pv1U-GDS(>;Uj z$R3WfV(&5;>Jw(dqM0+woq1eF++s}boAt7r=R&vRSC?45I|Iqj5qevEVZChO;>gjkyY@|t(KDXIY3jJsyuuRhNq?b}wDIXIjhq90?AouX^RwDU z4qli!EmfGb3SD!KIy)>{kW;XxBpbO`>fWFwgJ@Q(9~ByuQ%-`X`3q8{ohpFKgiO&a zL*CQB1c{+PP%p4kw=ym+A!)f_QJ|=)$@;$1B<{aZyd=PcU05Tj7=5`J^2 z54>jD(bJf1+N#l_dzt_7+VGssm+>L`S`KpjVSk&}7V#&J+2wqkUVZE(M+GuGwD~Zh zd65`**x@i09w#v|VnlY1&r;t{@%im6r|>kqbLxt;^a+yxj!T|}BlhA>ovXSbwSlwd zAOEE>C^r~~ir$5YTtl|~)8T>5yG^e&!n#*Ur3Kf@+K;0npesD-ELZ^?ewYE;I(3kRun2Xadj!EQO%EmwF1 zCv);(h8C6ABpybZ87KJVP98)gOGQPhz0alH&wU2}o=3$)#`Y&IFAM^Ql9r4lL)B8- zkQ&9V84}0!g2t^<7YtDv0x}BiZG9^{3KEjd*yRek=}A2ga|%>6p!9PW8PSHHilKYv>#em?3`b%<=iZd$u7p=0Ug zwdPAXdJW;$Zl^x$iFdVZ8b#&*q6`^!V(fY=n=BI7c30~@1 z)#p*aYb_P>Vy#nGL{zv~b<6hlH2Tr=v;GP{>PWIhpZePpXph?rzQ`V4cg;BQ{##X7 zW4p3Q%b&Hs<$gLtuK;?LsrSO>^rr4NOgnw;*XfA}kCDuhjWG9?2xI9j>LeyTeSNfh zI|rYMAbEgI@}vZXv-66c-uCvkhVJ15 zM4+BKFfp;R!p6c<3(5kk7;#I%^_-!F{z@TDG*|}$;s`Eb2R^jEgq#v(Rn@$gqQ-$- zEMC4?|L(3fll-5c!Z59mP1%ibhKf_)?%GzOJCspC{cM8vjxjlJ@Qc;B`n>delu4>5 zCR&3UFZB3g<+nV!C}Aq=miJ-JgmZK+H*w^K=Ua$PSUeuqd7F)&SHChEPHwrgGTGB7 zd6ol9<0CJ5eu|S^va4C3vo+=2<}Wr))+PMLz^FvvlAjn2A%98gJf4$4{@9RuZOZ@X z6)y43%Y0vTmWkC2#Kc!b8uTyd)l~LAD7#whA%&r4v%@JD-OL!Ie`U*I=ma=)cTsWfaQNz6|v-Ly+&O)QLspAHuj=t+JE2frz*(nspeTeKF z7>Hcy<1;@}j~Vr#Hmoe3$i!ql`?Dlg`!)24N$Y951&+du+Y7;V<6I0-*fjo_Q?T86 z&U54PH#C93qV{ONwQF#v|4liv)i-~beWku{V5_uGm+p>9nAod{SI^}A3}4YS&^9X15$HTt1P@MUrTXclTCxOylC?a-NH z$YfsWM90C{`E?7<(Kx|&l}T+Yn>T0HCa3eNR~p0WibX!JJlNU9G<1I1?B5y#d^CB2 zf$|!9K#%n~AFm}^vyCM6XOF`dO*m1Xzy|4C9>7X{Kl%S5?!Duw{{Q#!qg^tJk`$4> zBb1EDCR=7!MmQ*=Y!xLd*(79VhwPP=?CeffcE?D@!7;!0C-r*2-rvvn`}_X+yWP&M zTiu9rp3leg@wnfw>$+bTvmXcmVK6s>$hJTy`&+5Kxn7cTPN^Q}G4YRaK#8z=5$S@) z$hEDbkpU#&M)-PLe=Yq3H%(0$K>Pz#khacpLsv>C*L#>daHK8*K|h;j!*!RdJG^@2 zl5d45x1aBRraDtC7xTrYxJByD`-=ymSNEFZk9tV9))DYX8HH*c^Z2s8^rg4Il*j&- z!}|Nl{!ZCnx%l#*JRMsd_CtOpM-^Fohz@*;9K1E=mou(!zqoGQbtQcHFskZj0NEfkCt6-U z-vc!mlQ$=7C5w0XSZ9b6GH_&jC3QJLInFj`_}xT|-h_GTqOf?Cow>tLYU9bvLkgY_ zGlz@fCIkd`yFxR}ZD`k_`wz>!dWBb1)M>t{q$9Dk|E_!De75JZV2pjJK{B-tuUXr) z@ytCF*yw!MSpaf@71i~TH=z+eYv z2>c`%jKMwqfI;Wa+hYf>hZ|H834%}i>l!A_C6cleK6OqT7WSTB>AXpKO$%$#OL`6^ zROEF$DWg$jjbrhxQP#WyDEq3%g%3@Z#2xOB)Hh|`lI!M65+A;0Ar;qFqw`U@l<^Ah zGJ}D;^SP3h2M*U}>a8?g-<}ydXMHoK9IlQo=Qg%jv=%esWD-dEl8EX$+>5st246x? z(>V0*w|9oLHc3K|hd}bl_H^X>N8IqJpE?y1cXAbO(p)6YEwlq~QO~qMT^~QRAQ|e-V4**%3VuXNG*h`pz_> z?U@dxWb0LKzNCo3#S!j%5r+-jmN?rh)^Atfx;2h%y9tYn8pPXmE$B5(Z(X(ATI=-3 zWH}%3&iJXuGo??#;iGV}k-n5~sH6J2zfBDvq2>X`m9Az6i~Fh6W)o>ydG5VaB?&Lx zY#Sf%;;I{n;-?}d>Yf*VEf?i&_YtisqDu2zL?IWtVtej9a?QI~SXgKJrW=@pbP>DZvZ@Gb8kvCrkvko#jBz*;#8QTO!r<{i0ean2k@ zBK23UhQ2U+BkOYDJw_=94JupEou}sokg6)5y8puVCQE$#jA+%Zh7c8c$_);@uC7W-vcJM&MtnuDzT)a ztBZ`z>sej30IHv46j5uVzxXHH01TA z^RKSs6xNn)@s;!F;cN+<>*@jgTM;N3!#edX#us)7U4^T!uGYsLO15|WA<}OLs1o&^)ry4|2>aO9tHH!~FY3VpX z)+GA)>LinBg`YLUp!sEDooj(mSeHuElkoD$g9%C~F3b>^A+1Zkyjp#}K032Ns+wW1^o&W`B^qgLZi}!&~8MYOl&N~7DavW(M zTBFXmjwu~5n`BaViDp?g-D)Mc#4oWJ;p5?zfAok+|55BUvd$2$uce}wr|V#M3rO)Y zxZ}2MUSfmuG#YS!q#{!MF3uqbl0;y5?G~srz-j}IYoj~~DvwHiE~{iXWMII5!af(9 zfZLP@NR`y;iE2!MC}nz0hhiW~)F=d>6WDw2lDG@z&W5&!c0@Nhfzj9~!=k;xA^`uVzOkW*09O)uxI zd_(iYxmB=XKk;&-zDws-hfVQ4uu4WB`vrzpKmveyC0S&(*#seMwAufndQvc+Y_;pK_i9ul|5 zN0y(bO|FN8FYsAwt~I8S9a)n6ZomUAG=&mtzm)89g{%^-M%h-!r%6xsoWPl%KMku( zUWh}ywj>o}>s9kkj*`iQ5yQ{ygn*MD8Qpbvq+4@1t`TI6F#vQc^cWctCf#ydqEwDi zWpKY=?Xm4aU{Z%(4DK3~tA?l`FD!)4J3wF23Ph1iWoC}KlbtRS6OX6W#9X^uP_H?hc2uB41w_VvG zo{c^ny7XKVX%iN$gP1iBvf>{t3m6y^dCHAd?;e7#d|9@XOBmlo6Th@gYV}?h7ifUW zW4aL!Ekyor6AQ^Xi}-|id$M8H`%M%-zX9GtV9~=%pKhhoje#sot9sdFAYttfY6yOG z^E`Ek04Q51Xkc1V4sY^+!DEMw&vF5~j90$fM#QD8EYCY~E?njksm!;V@0ns<=x-Dm za(}74K#YG)h+ThHbBUFWO{1Ig8THhtGsPi$H zfim5vb8aIO8*}aK2zO{e#MWCOird}c5X7fXdB>JK#BFu^ejT-FsW909jc8AoOx5p? z7vl9@mP|ii%+t5a8_lR_T1#rgH;B^Pu;0qk_gr>m3XIIA6I1@$)RAzF)tEUXl3 zrbCKus_H@kNWs9KqNO&O+N>wZpHw`AH^oPrlbqwQnQZ@#Jmi4B(CSF-@H{u$dzB<= z@+C20ctkYIi<_rISP)!XdpR+*)??kQm?!G~)z~Hb_C(zW$g@WlV{olAZP5F5)*HoX z1Ab+X5Ht);qU~ISDlyIwz4SN|b_li}!u^xU)yte7P>_)1cN4vHIVNUis|CGKfp_%p} zEgP+tDXyepCVCo$$z?7IX3?}bw>cxT;(@#eqvD9k9ZWB)93i_HM#9d_#nl+XAdiT+ z_^YEjrt2V&0)1n~2&jIng^dkoz>xxp&^)G?C$XgBFCY0bvZj6J5kJOq693Imsx@+M z6kM?n!PIhweZPtqC9QXI+K(3|if;orBX=p+(KnD%LMC;OD29bjvu(qSHTel_|B^gR znDRy3KG@uZs~hD8|0Te2kVVvs{B+vyu^&eq_ z__0xCFUsh%XnraH`N7s=Kd3r;@DK>=pwEI?XwWnIyXpxvk$^`43}eEJKjt>2khMxx zl>btM1aYhQTE~Dqb z*ql6jhyZ8}SQpLjyz|{-l~r3h#QA%ddHWk9J67Cs{v$w$cHfD{ImfiPL745_H5r@e zyP||om)^Y1TrlrG-piX|?qcB~mkM&t#&MOLBgR1jBF;<#R_C8smgrzinVIsZ{-*KHo9DY>GSI);|wa zE{wde3iyV$&3daQZuPj_3!isWG|PbFE{H4IA?S))zf)yA8>YR(VKvf5UoNCK5-mTy z&co1&H}}?#FD`ARC?06Dc^4W>NMFFCB1$M$j}LyvRE0?+}vzpBvFo@OzXy)7(9b34~lKSr%#{4{)4*YM+YF+SFrvD@dfIAPEPxJ zZainC|4<@0gk#-{KrBtKtbJPCV#_E^9U;w#sz)NgKHPy1u7=JfH|Fy z_fNSZHuBn9beJ;5_p)K}&(ZrI%zzTF*YwE4%+(VU(K0SSy|qv9Kkln8pXM=;`PCd7 zskr^t&kn!MPSY%W(JVD|hO(FUGPFjr1{KMs0Kmd?h58EC9v{m+X(`X2KVWQd4H7(n zkWE5QP9si%m->zsJzjv-CPy9Q!g4T?p;lB;3rEW9Qo-!WEME+XDC-Z>;8A)(|qVuyQHG&0r2 zV`^qyRd#Kir{>GF9LOMIw^?Ryoy802%rf(pQ7ng&YCTFXLl(nF@oQj@F(GU_?a zcSCHp>8F|pAgv$2O~Xbm(0)V8v3$V*)&o z$Ljw=#}hV@;PpkDdf>@FXZpTIp$$S#07pj|V!KkB4aM_~Vd#7g2bxn;Bc0z55&Mef z+SYvIh7+rAstGP>V$l5jA7w1->BG3plD4i=Oq&1tI(;HxIcS16Bc7h>ecUNEsF{_P zS^zc;<%FD@P)H+>6>?d*uNU9C~RLN~b#LwStxE;H#(FmR<<1tx-_ zLBgxwCfwJC1h8wHOhKQUjlkF-zhrIbaXdShzVl>wlX<-RqptBqg^2L*hIx4%yWn`y zZLAXJQ8Rb8e14h5=a8Af9~7qgau%vn9ZH@X-A$Xn%DFf_w6!r4@Y_rVHnw+~;60^B zBm{35Pt`8yQZjOMEh1_KPEpm~NWBUF#&O>P1JJ(L&i6(*EJ^Bo?BQkEZfd8&UQin- z{zy}33geFt2Rby<2euLiBMesbBYVzr;IA9pienAB-Qt|NwjQ=QK`~jhRZA1Zur3)xGu~s5qd&E=-9B5PV#ci!dl~VS$}{4-Y8Bcni?z@M8!+3HF9C8lk5x} zcAAzsV9zSj(sFz!F|CsGZ508Xd%^KVjwIM96h(IBY5z;del9FXL(4t5laZO3 z_jqCWV>?4OKCz)QNtz(XgXQ?MPdT@9D_7s}T+3`N<22PhGFEKSW z4TWbJ4Iw`HSCPj}R<|LAAr#NT(e+RS*Q55(NGH;vJLXqn;bHxGx6u(~&^JF~2Oimg z=Zn>c9zA-Lk3*g|Z4bHXW7HZf_Lp%DSq!^Y*Hf(8E!;i)x#a2G+qv)1nk19N4(WW_Ke$ zPEET7I%f=u3@!I%jmry&rdY4&J;pP@p34<2BvOU$>4G76EvNitv6e-P+f2K|SoZYQ z_@*s&-L4^;>ibPfldBFp=?elwv%K+&9P!1l{M~|wW0cG@mReTVX7PGe7Xsa@Z*VthvsV)`{H_D93Y`f8JO!~NMGPyvk zoj|^5Rl6u$Fm~$dlG7_vvALIHo1f1_yEU!3MCAI9QkyBJsTILIkui9*ux~X`7=&5d z_CPYR9|)bmb>9$%%7GaayF`jI=E(B>8JHFsu|4FuyFG`4iGe1@dTJ^vF9=Hkb$e-c z8yT?=Otdh@JSm4WD3N)r_Bmm`HA9z)Zx;|q$AcQ{s8;}4{_t>I%Jgd8$B8shm_&wU zroJs-sG+GV%)&noV+T&8+Ww~tW;J6Lr2$)l# zK#`t2(*1^f9d`YErnkf-Bn1W$VW0!U*DiE`9|PR!?M0H?r@EZJc=x$=8Ut<6CRqwb zFs9s_4D#d+ADI?A$P04hqg$2uuy2dG7uP@X*bPY%FR8I{G&03@qqKrlhwnZfI&AXO z?Q2N89&4C`9B6HQB06+=Q`9_4H#`EhDo48oV15S@;rM67oG)I0vJcr&y8#tM^0Gn3%n}A-5oNeN|GfQLvGMKj0DBiVt-c@;oy#~j#niLQ>l8TATY6lCaN|-|u5a^?`jnfMnDy@5VH{{b zsk2l-3Ux`{=z|Z>7vdI{@g_!a{@1a@{Y>m@)AB0^JD;12q~72UW2Y;1@w%ONWqTT7 zJTQp{OR&K~vGVaPFQ%Qs!*h#5`}poxat~bwVSHjQV4_dHQBY6%RQ4FA-dVGgU2tTf z!lx%kJZ@gTBfGGOHJT`k@!#r^n_ZM0(A3my9C891;I^CR>t3(O2*jFs=KSU6roJvY zTctWQb48Bi_BJaaoR{-y)PwOr0ah={%Q*H}w=1;)$QGn#i^p#3rwIU7J7e0n_Qin` z6W=4Fp{;!!{KKKgEpZ8DQSS)2qWViM81PgH;NsPJBk3Rg8oWN4QE{($09c@>po50s z+=3Tfpb4H#b-ve|q0y}I2D(v_ADmlw##p9z;Rs6vd7=5-sB4er&bh{BlN30}RK`Gi zt|59Up!reN?@g);7bGr`zSK=NnDF!WFO9N`Zl_?5BQ1vtcGaaNf@7}myN`QO^uxQ& zQXZ@+_QSQwu!$U9Ja?ZJ!T2p<6cTBedz^4%Rd`Q`Ta&aSHNP zKaD;+DqC&$=ydJI4B>R<*3Tl><$D8PRbL272O2Vzzs?aUKjUW$BEJ+tP z$%8ujF3J}YCkUaDqdEZxKqay2_FMgbaY7%y{5L1m#+_kakA}7F)a?IWCzDxL!7{8# z^N-#N3P5l(QUN*%_8Eu{Lo(x<+w3_zbDdmd7J{?yY%BcmbYy2?34TuS>Eh$T8vSW` zB|)sL!svr&C_!t})80|kq?=XyNjWnpbw zJ^RC&;+gb#off5q zHNnb7dY$g{NXn7Rh1~Z%VTvD!e$av5Bf8&p9ZvRpE=tfIevVf8rRHlvWcK6pFl2!GHkBlzwc!&BLRW6 z`b?Sr>CgZc=vzWaxAGB+0B99rE2n(=Wx-*NdQ}jU>xJ1PDFUuW-eEMkVF2wXCb=3) zoQ!EV9;v10qIWFlI@F;F`*w5{TFh?`T4m@%DC&L%>PH4lnDd_#hC8_iESHm0R4fT> z4DjZ*--TG7JIx~0y|~4D@4UZTaLYnJxJ?w>ojD|i(AMwH<-I0>^;tSwNo<)N@ABuG ztfYpfO>RbaE(BLNG!M0I)Ak*^3Xv3<2ifjqy|zvYQxs%S?>OBu?Gmw|V9l+z$}_GT zfEUlm&VJr9387KmEt9p7QPEz@e%2q$(S}#pNICNI6)r;Lg7qG@kW>Ck&}McpSn&-~ zpe+F=3U~tI6Ut0}z_>bY@)kZ2nqv}&!nCav`LZZUy5InjCWdYa8>d((*QBM70(K8( zIj{%Ri0GZZ%@}PBZq_&1hoaF@O_Ei=l}Kmj=buA^PPQd@Pl+nRqfmn(PYC6y~LwZuymKy6So>FV)DOwcnkGZS+`#5lxA zW~;QVgA+V<5yz{6XDIup@S3pL_Ar}prFRG`AKoqk%#L>Cu%^RP<;Ga_NzFCy+2k)N=uF}yc8A+Ve`A`gp)-bS3x?muRv9=f@%%_nhO|gF11323&JeVyOD>s9uKe=t<_?FB zyQbg6b($uF_ik5gO8ksn@9?G>q6LF?E*3L6MLShj}*r5t_o0#_6 z#_&E1*|gwo2A6n~#`kWblOfDtZNB>p>X8zcHm>Hi9$@_NiF&zur^83K$m0vaw#-zh z(tpJR)tqgUi{n^|H8`7)<}0H7z)V`oRpPddR%&1r~z^#$CoM>+-MR@&{kV z)fPQyUn=x%b?(X?7JD))s2}OY&`?>_x=sf z0JWWt_td9Em+du|fI5fXe#UxAQpK@Kh<6EM`*?qyzu+rDdgA%%cG=IOg6H#AQf@GL zdy6iFHClNG&xTOqqb;OfaSfcH6>YoZXRJrkVp8{7W)#p`B-*sdb|mgMuk6fWjr$?) z)$J4RjSxkyi>x_kqlfMKT`u~z#~4`En{J#$eV&QgmMqoCV+AxCm^*z zygd@!wV}I25A2;bKNT>qyIHmScr|dEFkNCxYHia^U*U%FI z=M1Oe&)L~~f%aFxkT(@*-d)t*h!d~ zTG{@ESc~1ST4`o>fU65z`X>ley&!NseaNN}3VFooHHyFHMyC|(!NR5J_7f}yFm4pV zp?gG(^uzs5Iz$RNt5vl%qc4NkJ_aEAiDys1Njotsi#vDT?IrpnVFSVIe zj+fModBPyg89Bi;o(yw*XnT5UX=(KN)+K`73BnXuhk%-Wj9U>qMq3*3(wI}UM`u%eVg4`;zo zg1DtofaiJ!d`VTglSv$~PpKR(UVg4nrV=)@3~8K5#)GOmqj!Du43Qb8ig^Djg}!GC zTVe}yP~`jHpmD}+ravQp0IW+q{U1pAzvlnmS_~OZ*_DzWI|MIadBbWB+h)+v{PP_$ zj0&?y3f{aqkGjcy?rn%tMAS^9bRyI!@Vr9eJ-~_@y?8nmUAndT@`Ypr}xwj*p&v zEen?|35K9Bv-$Zv+UNdxTOZlpAK}gHJ(1bl?4pP@EgpacPJlud6i>+y(~b33Rv!hp zGUvfw8-A!y9b}LI`LQ>wh+LASXO`T&Er<;z3h+>vQf>b6OrSR?=nHOT<)7t?3{d!j zo1VEnvv1D@B*=qoyLXS zenOwRr?4iV8({OIi&O{I2V5>NB)eieEEKVZ z7|>Et-GclssH~XDYmxD8%){^VkHCr|W}61~Qeq#((;lJzj=TD^%%11MT!GkL6g}TkIxzMHB_VJGDMSaIi)D(;u`>2BcuJ0>eIn-t% z@j*nJgh#xbpf7|ZidTT}&|HClxl#`=)Hn+Muekl3qUy1g3zBP=ToL;pHcKL!# zae-$fgv&2v7iCA9wwIPL^q;r3ob9@Y%Bj0$L`;;T%N42^w_!Yx~-{(~_qU zgJUG^J|yJypkx1C_S}O{QHXLQQdO|Aam^B>fMCd43x^l*Ul0+bqOIL@p7}KHPL~ex za~*>&S{%2(1l_kP_ZvBtt2zpu#1@|f*X$L7iGSZ z)(Cu=`<8c$e?_^>NR!xHA+qzx>%A%1*LJ&7T}=W}>3j!Rg5acsi(hM;2R`&kFgBL+ zRAL4f`rb$;qK;=2LaOr zVCd<$3s%jac~CIIT4EPY3l}@T4dxcH4z*Z^F-UYpyK8Glqi^tDfNapx6W|rAn!_XU z->WKg>PP{sqE*C^2F`JtU>D{y6cqMXo`PTM%fw@_JYhEgt1@uOPlTrkY=nWgEUJu# z$QP1&#vx5B=CSB-#j4TEr$j`9F5X}y>=tiXB=`g=Bw?96iORF`{D8zjNc1JrV}_v3 zfgTZFS|56d2N)LXQn)-Pq?$(y~)ZU)#ogb_yB8sbuj~fSJp_vYBUTDPN10!@7 zg29oYN1&A7ym_M#;~*Nj(q~n<_;*7l<+ZTkrh-&L`qKm`chh6bJLJkB8m5KmVwFZr80F+-)I3W73kc5~pJ;^!HGxSK!SpojT7|P1mc& zLtuzsntcEM{U@YK0eg~n0E2@EtE98@b=YyBBg}JJ94onA0YouB`rDJkgl9ikx3sh@ z&f9uYfhXi=*f|JL9U`0Th@G*v9;yVotcP_(0)|Z?49MSAR8^k=v{BT7m_l+6L)0>i zEmmCsd=o}JBA+uD)5pSGEn+!|`tf_T?xFKLspbw$f%!P3(VT~a<2Sf0KEF5sXyWsu z)J`brR~J;IdfnU#%-W2x1q1UC^Fs1_*ma=>W=G0LwmE@r{kAB)+{)|sO+}|A;q{FF z9mLiVLRK7o5Ep2Q@wQOPZQTZbL^0A@;n>w%?!GsF*{><)o8UM$# zz)TBxAV0vrnDF7)t5F2Vq2%aTXvaskSMSp6M$S#?fC2CiT=Vze=PZVquq-ZeaO8nA zlgTdOvnSxWrNtgm^1);!nOEZi*aSJ0m}G28%pH#rs%(rz8^DwBoMnxvQ-@e4PiaEd zy%W+sfK=Cr@L(BI-AT7r1Jf&bK7YmLz&?!3vQt_5e;afFK>Fz(Egx(hf70NEYFYBy z7&OX7)e@7$h!u2}bdpXqOA6~_dXQZ?{oSZus=&Jb?qebcdKpNmNAo=LW%hfhWb_e( zhRO(Cat0>Xz&@jPdYJ*ebrXGh9lS)D2Gf=*?{ZSWcp#X)$9e8J12t`ytQKT~hXfjU z4#!^1V6ThRuY#90Q;ePnY_ge|{zMoBDXZ(38Wtzz-J4S3Da7cZU#74?R(*E%?ILRt zXFztqMG;;hs}iB~Y-wrX)`4om~0)gf`h2GVy=Jm- z|Ff7rcRu9$Jw9ditg7vaw9u$)IePLk8$C1q_3?~X@U9rvzng(KY4KeZ=t;|!`oi2vYDWZ1*rue zk`evCNiD*paJ;4BvS}(hx#isq275Ph=@eLrBwiZ&D&GQk57sm4T|$CU^U2Fph#Z25 z67uMwR4=gXWnFb~a3D`)Zm#8R_>UGrze!Ti%F+^r+8aP8g74mUWc(fW-d;y}TOmie z%Ob3A_LoBw1#)LzyGan4c{9rhtRUo%FM>Gz_Mvu>DRNJkfzU!Y`ydFb;L&oNESy+~ z9U9Dyf}j2iLJCf10a|Sn&?ns9fq_#YhMA~S>Y~R}e>g@W%gBT^32aolU! zPSxp;KbW?auAVBrs%;NhkZ{<&dpW&~AEQ3;?yNNIX-~8&pe96P@}IZa0LnJzU(l7VnPKmhmKD>5hpalTOk0T9 zK?0oNqt-UOQcPWd#shqTwLJinBZtfXP}q6B3~4j=N4p{R-3X073CJ60tH9+>bfYuq zbc7FFyEfT0YMn0Mt21`_vlaO^%RR4}vfMh=Joc9P1nZlX_}4-&K`2fDic@NSAK0oc8FFNv3kLk3FG>ktKaS^AYB zI6Cq1@nsp^ao&-2ynNc%5H#u~tKY4(-REa#>p(8k+1uLyviFX=yvo~Q@S`hNlDoe+ z>az^Z=15po{p1kc+aKv70dFB|ObVpMa*`{#!`ioNg1tThimJK6($MGC>tp`;^MM;H z-AWLq;|+%uP>IOI>>Q)-0`0qGfjE7g)yKQrYX$vC`9mcRl;;t!(17G`dtD_ORW6cx zndA&Ub^<53CAmmq(h*Qfw~yIer{JnEdA)Y_;^^H!0g_`=UWf( z2fTW9ATQ3?QmL58rw&@>P@=`K7Y`I0W$|ecT^HssW0btL{}fkb!S23BD}- z&C2qUj5TY&k}NDolFNUWBcj%{n3*>#H)R;%00yn4YDJ%)2{&W2G})5=7z(xuQ&qed6Z((N?ff(8$c zVAM@{d4J2{`?P!N!v~OUUu4tZcP8IDtV`BB0SRTq%tF#zQ!VNTIMN&C;(fQ%k{p`d z%lec!u&uq0B0xD8Z2g^F+Oyr_$!@KfdSg|w<(HU9w=Un^1MCtE)e;FZ|hBO$^P@qtVYomz~jmJ8zO(pIQ(i;F3d3o4roX8CtD)4v{-8lz? zA}FK+Z;j?X3Jjlc73kdHJSTwT4miT`>h0A5c4`!oBy=-N6D4~6ItmRrPiM}Yc_Qbs z@>xypv99oD44>867Nzo`S0p0qD7Z(|fJq=pH-NF8gs_TN{^s7@SP6Jny3kd7Jtw3X z19e0c{3qRA=u`?~E8X*!MkotcG24EE;zHfa&2kiRs${~wzvQ!F&7 zS`|(Y2QTP@ZWltMK0P~3Xwr!P(?pr!5s6mwD#BnVt=rEaeNbbPl+{}q`r7u}lMfL$ zNihwLGo-HT^&K9oyk~8JL)XeTIo=*G8iZsk{g^3Lr|B)a@@&T3qen>?d(|#hQ_i7H zVSg)&gP1=+6kiV5loLUqhKbTG#N+H(v|?Y*UH?R&*R+eNIf%>(sx`eS+5EW|ePs1^ zj67C}>D2xy-Uy8CqgX&ph>R>E;t@>I%OO2De;(*c1Cj`rFF)oH3U*~QbASU`ZEooA zg>;3WIH#?}6tAF6bI+H;lzC-kIz@`7Pae^Z;)&D84)=iPK$z0~9qYx$a5`$4Dux!61Z3bQ0uxY3tk;luJV8FCV>fVhV&Iz_Lvbwc zvp*bm{3iCS01VfF0Na%8rM!fS`aA~&xWi1+>CY-6cYDkVAaW$VN)z5D_c=uCUEVZ} zl{KdYbQg*ZxMcZdjDB=m9-eylFb+xZUM;t;f@J3IG;YGbZ%NJNpKhmX=J4L(&7%?B zRd+J}AldXpr90#9%Zk`gKk^saM3T&9`c*ccyXXvw6&8%7^5tg;{^4x?_7EYwQ#kIJ zgRNSW)Z5g)j4yQ#vqqly7Uao)&FDL^ZG4sPaS${8-DzqQhv*&%RgESR7fr* z()8xmf)$SGZX}pNK(DV=Y;!RtsBsSeX79j3{zw9mLLg$mCqvi7*#Am`4(4;jm6U@1 z1I;(FEWa>0E&lSE9nTEZ4q!PC3T^aARqXkezdvPyq$#+sg0U2ls zAR$r`kbHPRst(giNGd6jAU%uCklZxU{_;#Vgi#4v6yU^|S$ME|sa#da^PtG-<^HEv zRk(;Sllj^`{(gSzo}q8HE9qLbw7q>r6}sqm>DhQRrjHywN_*@8XpxZrY5G71ST;Z` zP~c2%cd)Ntbg&kJ1`x7v^0$OwfDBG%5b~uCUU-S) zc0+uK9k8k}m;pHQms*%;m5|(>>Rm9%u!>jbN_J_H*YJS+6V*!nJ#Omt zprwrs%x`-id-m!4+x9r&%#-csppzLMeg$_NQ~wjFl+@HCpwEz4VvYVg+VCfNuE-Qj#H?$mbO2|29M4OST|@5usc(f;#^#nSCgLMY_JcS6*Se74w%FFi24E0ui6nRz~7^X;i^H6u6AcS zFiu)$NirYlj!5tQr#qs_>}8!T^ka`S;QT2Z6fh;9AfxW#o72D7*U_)N_n~BAj(%^S z?n06*Kf4JJoI2uw7sm?-WDS^;18=BxPa>b15U#9k6`pbSDGUYtF816kX1CGQjLe$$ zy>tu-&)KZ5sJPG)$t4ZSpnV_=7PyA%_|3*e%7(S%0RT97t`Wz_L+NN+v3LY*%~hb8 z1d6TBIPWGb#@^nhG2Gf(LCiOh=7GmPv^IK0w`FBz^=vQu_BqU?tgWwaoCY53_Ybjv zprE9T3^{&Vnvu{}vjU@$D)*b{t{v#^&mKX690-CXQAmt@J#Kw$v9eJWw>ml-1+y~H zXKHOvtzr868(k)&9#W^oaH%RK0@DWw)cAL+T+>Q^5LSMGCLSi_#9rcf#@C|Tmc~sp zJ!XW1z#@m9sxflrd!!->Z~d#FYJE}2MQ88m8Qw!2yeAYP34Zl_YqE7W4xdDZYVha1 zJI0Wh(w@SGX?s0n6Equ*g90m)pOoH}hCz&!8^9#`lIX}6<+P6ymu`h5Q*jqMeZ5i< zMw5|_HW|0LF2Z}i^wiX{nzifBxo{$(E{_l*3VA7cL3ms1J?Ae1|72CK%5Nr1bjdRC zit4tys}e&ck(uQ}cB>U{`{lxI`eus47pJi9$@4u$O_F%oFy{ysm-fq-@pPSbZmO7M z`Q~o*evv|+NP0F=Sf7xV?B9^cGP1Y9%S?5?RuETpnh2KuvFK#daydvSaZrJ`Dy_f9 z>!_&_>cPa9Q>%iV50ZJ!+nF^9P>^Itgo?WHIEUwD-oX6+urtIrkn4i1hjAcB`8PhD zpXTfSOvk%#XY;7EQSY;|ya6%*F06~zt<-R0oxY1(et1vo8wKWY1gv#Dhi$G+0l3rJ z7jt{Q@*?q3*hsC;T^SeZdc);1k+_wAyTjQ;s*3zX;S!nTDTr`5gc<~15XME2&k%A? z+lqU$OVpRm9s8&M$;aaQ4a{>>UOZ9Q>$<}0B2Y-?^PFQwjA9DJBeQA}vl$@~83Cj5RziuH{SJMB#-9j?#olIkb$Gfe-`fd(~DD7q;)agUNWuUs*D3u*q z(}c50s$Q5u5%zjeek>P^w4QVmG59XeFSBg6WFNB!@4nu8U3jFxcl=Xiu3~DXvJlR+ ztiFX2NF85^C*LGCDiC+c%!Y-vmf9tdt!9R0{$YOlE;0Mjfyy6Vvo%-lf)4IoP7X2R z%X!2rD@_4Pc({O2za1?e$PVD2gn|~C#zL(9bh&yHKt0`?CK!TECv8sx8VcIow936> z^fi_PEQGu#3Is;n$X@atp+8?{%2>6g0K_x_wL2=#mMty0352jelhW< zY)or}qnY4A9S?pq3O0Se>;f*e>0peVhDYhUNuf82_>h+?6JN1Ab%&8NRYoKc&^~e{ z9xDrrPChq(n(r*aIF3^ux*jDl6ML;)FJqaa3G}tV`JsMLS@goSBtOq2Uw0tJdE? z`Vut;NYG{2E1K%2wqGuo%-C{@&WiU<77S`_jYMCJ&!@<5Gn1bcJMeGOBg=I)H(Uaxs9*i34cZ>F68=h93A;xUGJu#C%(H$CmHJ9qBHE%6k}_%i--My z97a%iryoy`f`RW-#BU$qL8QmrS>oC~Eg!*-i0lyyd6tm_uw(Boitb3m zl6{EZ3Of4TwglbVB7~s%426OEV5?&nFRFdv|3xl+%d3Pqw)Nk2B-(nuaFy#~9}oLd z*_6<7dj6}U>?(zcK6Q2VFU__iTGpu=b7_Ezq;$nI1>4R^gJ(&24M?7xa$_ zgLISt1>JTOuQ_o}^^S`hF4F)o&x5WZOR#T!!)efk8>pz9LLP}n?u8h%6GZPJO*HD0 z=E)C;;WPU}GCiVt7lNL?K32_xs4b6bv{vxmaFNnA3}92x zEb0y_=c^0I?=_ZL7cT&~0FZ5Pb|?aIih3ZxAv%^P`!q+RdDzv3H$C?hE%q(w@*UHg zm9pnlCz(!9q)RBlfn(=pISF;NcIk}^#M)8y`z{mtj$oGO_=*W)w82kXFZT7v4t3Ml z_dU5sg}OqxVD1;F>U_6|Vd?LR)>N`N&0JKVVWp&Na!zJ4podv=kz8kBM}b$(Kl=H% zDZ`{Z_c;ku(-ccVn11bAPEAU>)wdqQ%(ZtOy>x~i%R+{cRtSg>K;L3M>Od_3$&{fY zlpmn(hJrr71lD_kGP6xPlL~ofU*D1H5w4hODRN0D4trzV=%Z#C$xqNi!WzWA;$Ap_ zVGDmGX^HI6o*t4hX>vW_{C<0@(IP%U-SbKQjt7t%Q;xUL*EE>PRo%JLE8IomE#u@? z0AS8;G=C8s91Nod?-yAqR^p^fUuQ10IBZ{ap#~Eb^aUCed@iP6-vt1tMO2w{vXTFc z+nYLGy+90EpiI{62cP`oZa{COB))fU8H=d7M2*@Tj=ilY?wFu?%H}?`hNA0k%z6Ra z0+)fnJ&RM^M=58KS#j?u25l0t{nM_x73?sNM&uO5f;}9os)` z1*tuyC3lPy+4@qrY_I9q?a4mx`?qi~c)xdzK}BbHA1*Oc7^+C$;@N`?PJ}xD`P=^Y zMAJdRxc4ple_ZLQ`?nAMVatDWb>8>x%S<8gd)~%sXuP8FScx46EmkelfXwqv{ltQ4 z=AYjSzfl2jNw6>8LoDy5Y^;)bM%7Q!G!MTaz5U+fAJUV4;65?0xQrO+)xc3t>Jq78 z3rswq`D5(V#Ph*`^ly{OLpQLw&l#GO@uc`r;#O%fAy`4Ce@M4IXKR?!UY)}v4ThQc z{-vx@pgm*OyzLA!Z)hgqz~emx(F6ZQ=jB@ikWI14x=+|Zp`d*M1rzT1yK0Mgw}+eA z>FHvUl82!LMuD0JNeKnz+f!ygJ@oiyOA53rkFx)=fMjZ>uAW%tiKYFwDnW!0o$#1l zfjnFgQ9Zi#h6tV__@FOZCRLA2hU+DFgFxnUJ|GB?zzUT*ynxYT5r|)cVi4LtkgsZk zqbm4BsB36|Ng#BCO=I+Q#}2?3pu7Hoa|y3012P5*9O$j#d%)-#g`6Mz+=}VL6L+CV zaJ>sP8)1t#dciiMxdFF-d&$oNJaIyfarVN9J*3}}12E@x=@{jL5hJes%Zf)pv!ozk z*wB(ns7Sz2rCvuewjL-{=#^gLm|f=!`vFWNKp_f<3TIxpIM89jyfj$OXZc`&XGFZM zhGFpX#M-bcmsQW0D;rpf61tI0p^44h{(;9r>142%XBKYput(+S*s3WigSAQd=V<_x*s6Z1MeQI z`vXYtsuwcjP7i%yW#z?%>fNdtW#x2&tK=sV($i%rTVX&oB>qIgR=&IN{#}0l9FU+| z?A%bkEegm>>C`(MRAj$;MT(zZ_d(oQDw+|g48~$u9Dk2h>pL6pTeld09-z!yy;PfE^ z@b80xh;Up_Fyq4!@Eo3dRL)LpED+$j_6t;1AS? z!tMl2l!V=m;v%}7m7ea~qGS=`;^8_x)0>G2ENn0q1KB*JDtuc76-wU?1rmU)He-cd z?@*#4x3mRhUuIC3-^?OlB>Nb)3H2Vvs=gVqf+{LH3kE(J)35hLj$N8iY5{)m39!U-?#WvhE?#T^>IzX+0@l%Y^W9r}`3x#JRFV90032Fe*r0?kg2o0mFgdHL z9QBozzTjlCNYNdMO3N(5HtJD6|vDp_yTD7;M&oj z7K7zd8)tU9>Zn%Y*1^X+GQt>QYN3~4xQlE_>rGR^p(b{oZn@?E8_gO#Nu)ZbE04e3 zJG*}bv7oi*jrccRC8L&C0zOjsrBx|JI!VO#%pS<25(fHlpQl7{IT$E59$;}bl@W-9 z>$X2$DFKNTKVgdF1>&Gun*8E){gU(s+oU;^z3}vDRY3wT5%eB6v(lp4%l^(SB`ZPjL%4f}Bobh5Ft*6`2 z!rUBU)H?mT!MNsgAQfiS48PB%*3V$T!9#1DXr!*0{ofpFWpp zJE#v$bIz)I>C(sJr7Z)m0mfsL8!j-1{}C)r{S@ZDIf8_iUfS_q>ib`ey$3v%{r^9H zq|zWtr6N=|A(99uS;^irN=BI_lw(yA8QBh5Wp9z4nZ1%tR^cQo^Wd2O*L8HS@BjP% z|9+2$&;96g-{D->^}gQk*X#Lu4tH-G?IlbZh}K!BFM&Z+sERx&Cv~!ut}_NaROH}K zQ2Tw$x%$G+v)4ci?{_c-fHB4P1o!Qd1bve;7dP}u?AWO+L^L_$i9V*M`9ibJh=5bV zJj^9$TiXj$&H9!mu9(cW_j|b->F8aETYJ zZg6_(rdRjiie|M~B&5=P*@psR$i3{x_WW=_WPj|H{uuyLvQU8xA-W?y+C9C!*jy4- z9)LDs+PWu)-Vp-B^K7Zr64ictC8mQl8x+UL{zEuVK^y%ANC(OKp-2C|*Idx|QrAD{ zF^raX+8syAvG`HJ``}@r# zxL3YZMsy=irf`*ys*e+x+vh+2Yr3jPBy5`M;#x==r|Z*t(T+aQK;d5l7oXhVKJ0r!_c%d+-JVdiaFUtsYe%;61lu+^JBcTU0F z0CrTM6T%|VS%*snR;S4$6i~UqS+D+V^1!UAA9#|3M5SR=HZSQT)y?W-?``$rr&u-^ z#L0gBXaed-P-j0WCtv(gH?rI1Abi`5FHq(IL4YM|7?)dq)9*4wvYAc-f#zS*!wFaJ z&Il{#YD*`lYZf9v7la8&(eIM!qRdwD-d$UzplpOCX+kXnE#$%uivY55<>VgNEp1{qKI0X-x-eGXuO?cRsL{OeJpJ(Zjx+~Y-;v;%7d*RlPUi{<&z z%4`Qu!z<1URPxZR^}n2pWBUx+Gl4LiYOXD|X#tcuxp<~w#A!W?^I;hUv1d~h1UCSH z4|7Wo68ZN=5?5NoG7LKZTDsosqAl}6ypI%z2h0}zD?`N|JD~z6O*AErdS_v?BXjZ& zguDALBmGKHco#JnR5IZ8{s%m~kwm_P32!Aa&G5YvBkPm<-kmDbJIg;?D?EPC8vv&y zpbmkSX%9*KQ_OFaP@xqF@=hq3aF-=s#OO>gLgULcr;21p)V9Y6EzvW*%T^&#YL>cB~4A zlGeqLA3L30@do~D#m+{B3`m^-3;(&gv4H$2&}vJ{${q~k(s=>A7o+V>!c*u>`*J3S z48;&kFD)$%=Dt6c2z)VFmUBF3J+xg%wthd!0SGTj)afL`$=@IO0=;vDLCjYzS&?0^ zQBQYT=gJ~=j)9ZE*gBhOTMPx#RXts(r9tLYQ9_URgT|m1Soug*jo{AgozZi7VeSU~ z9J|arqc;Hio$Lva&wn4vuSxnb@}2~zVRTJs`P%zX^M!!A0Qp85C548VmYWiMk6G31 zAv0wI;EN$HIA)aphK*npUPU_W$@p(V&~bMF(1*OKSg7W~~Z-X?>C zxBV|cSuVXlT&)(BH3>cvc{(zdol`Ei?aKJN!e0xoUD<$daa(HRw z<5EycD+q8MRXM%@5$UAf@6i}|MPaiC<3UsgS>)-B^DpIjfTT?EQRLzGXXb!m0DLCX zX&c6yrHcTo_!BR+0Qo8KYP#8zmO=WBsQP$dR7bRud&cyv&@PoCU55T z@bfRK$q`5p_5>_URRi>~_TX6QFBdZF$`J9E+eO_ai4^ezw%qM$D;g15?;GDKy7 zb%gWNZc~@L8hFzlL^m*7W#6UuFZP7|x*H~|Iw$aU)6JZE?{j?GRi~8YfQ5YS&u}|3v9wf#+vVSc zVE+66gALrucwi6slBnqEaSkLeD|-X|H!aMgbF$`N+C#EG+QWSkXNL8GXpQCRsODc= z=W28yOv3#@ibd2_$HfFo7vbff$&%BK2@`9`DYG0?cr26fTKhtzx<6q|ZD3;8>I%hV z`D`!3rF%BLMO2Zh-aJ(98DV+k<6hC)uF-RT^ro?BVc3^x?)C_J^B#Q*aHCA&dwSF2 z;Mc-t3D{f0k6gqUB2&;RJTL5qSx@W^Y-?CvKr5$i{rNr^GnM@rDcb-Q-va=<>+PM_ zcZGaqGBA+|<1u{G)O2gtwH1uR(pI2G6TNW^$50e=LRF~%taglmzYBC}F4tVgai)kt zhLLG*qB!I*1eZhZ07w9h9o>&_^r*TzZ~bqgN2&Z(88cKiS`aaU8TK2sj@nB=X7>sRGACfy}9CBlOtG9@L~8xxLu zqwAAdhHQCD(3xQRR@x2G0Ae@Q4#MQ;5HmA#(R9`)ZQVW3QBG6*ZnIj~c3>C+wrG1$ za0Ys2knte}!_9KeB6?Fe;2XnqMz}s(_I69GEebq7qx{rxdw^-)ahW|zD#B@0q)`oC zG{jPhX+y)X(Yi1`H}{y=kfdqQZg7bR+hP9Bp(i7d*;xJR=e)$DHF_qHyF+n)5gZlZ zMC{AAprM+6Q~(0of-)Vktz|XD9Olj)6sRpgNkRGP^|+H>xK#7@Ke;J_0sm>IRokd) zth)fn1z$unR?frXRHIY*AuBKs&F*a;U66~bd+uC6A-4+!^etEo6!=A_1E~XSQEHnV z2&V&E7dsC7u_vD&uYjEUj4~l)k_#H%A`;7b^fG!phIMoA34l(}(qEC?0jXl!%oSzRj(PuBvoEgF9W!05Ec1L0B_ zFOoLk*hA4jBUB|Q5(5*wOdd}DiWBSqkfB2#0kkqT{cV5C~f>dkwY z0bg;(j-1Si)c?<{-rnOSS~I^62lVp~z0U5E1%}r4!20#-OUQ&lwGw3LT!EaV!E1|JKUcYf zogJBk6CO)53xq33WmjwqJ6RbYi9=)r)o-hR+z~tn75}LoUVVAcFYh+>!z6#2`UMJu z#{Xz7xeOET#&`Drkz$sXMwQY3W!N;#0t734c?7hDUc)E{Zqqw(SA{u0nC*G?PAl}O zrBKZi;^;KVb`ge9KU9Me;q?3D%>?OF54daWPPZzFF4M&x?H`N{T+*a;b#vB990O}K z`92b??$P&Cy2qXy8lv?Hc1if}JcyKpfk7m}VV6loM$iH1Z!Bw`MkI zuYvJ~fY@;QnOqKHT@D$6dZ+J-seq-4cgz?+Oh=kSEwalri#bvu%nyDS>Q1$A6@}r+ z2Um&npF`T>0^tiRZj|z+r(HxJyBwu*DZ-W*-vz&S<*e7N3ct}3Ze&bB^~28mx)E?V z9XoRR7D9}#69AkE(2;(9LsR4^g!GA}Ea>#!_8sLbGs_W*C#b5boYSQ$;UlfV@@8e>p#2ggD2D|C&pQKSk?TOJs3~`IxUtw0Roo{ zFm6FE{=AQ=i_iraybq0xUaCI>n!fi=N};$dM-k%o7`dsR`6d%_aB2!bI0Q}|zL<;u zFXDOLk-1=)`^P~gPraV3w#Kgp7c8L&qf*W(c3!+z6|nQU@cj@h-*e%uSMo7NfFJhR{7!yC@JEAdEiFCx*=**B*5zFWFq!i-yjbV#Zf3a zG3TqZgt8DoCE(2lCL+SNy`CY{P_aE-VFY;`KnlQK1afj609Pr_hp$&}GwBvS0m3=7 z)IYyO`@q-B8n+JV$3SXGobP z66qZ)c{(mhpFiqQ&e^$&P%kK$k9gcz>u+V5BBUOO&ix+8Z#TiZHAz zN~ZzklWoh<|q zaAR!D*vajtNGTwL@rzocyDrz+Fv@CZ-|CFn%sKD+}Q9Wxv3iSI8 zXLLdy1fOC@xN?xlf%(kFeCB5q7Y}u4Y@ppOh|&yPd|a##11E(do*Il6$T)L$@IMg( zN!!9{b~4e8u?H8?2`LsgA6mnsNXS7md3kCRR##S<-tg$S?m`EjfS>=FKImYrz9~OP zJjMMWAc4`KBP!vxC3G#3xA#)p)O{|h%Z1U+PGc92NWV7@7M6o)DqIT;QP}j9Mk&b2 z8-A{ktWv#%4y(W550gKZ%Le`yfE#@Q=#R|ly>V=HqWE?qYd`0%uE?Ya;FYpAuVjd^ z0xK^!)SYY^0mJo#b#;^JSTA)Q*RG<|6+9SX)`3z{cXm8w^W7!1Uzj1FDB#+3q+-Nh znT_c%jAxU_TM#l1`(P=||`_ea3AF47_fd;p)+-NQCSjLV~@l-~4l+>#UI@coV49rWxyrurT1|3TJ8f&#Xn z`k!y+2deZE0sK#P?Q6gf38~3m+)MH7Z`trsw#&4&>H;7NTMyQh=SE1I2o}bvA=(K7UW+JL7xPQXMovq?cs-zq1Q-6Z(o#4(k=VKlDZ(#II*O1 z!All2XC=h0y`hKMf_Ds$H`YcH8nyE(t^KzGjz3Ik(u-%g@%F9o>G?8>p9jq1OhTGlsg3CYAnxApRub)7*6 zR9T$A{e)i&?FT6Q%qBT;JpC^_|GEg6P#bk7Da`b}06YJ=E2bPTH(T~KVJG1iimd;N zF9J1JznMxdee9&Gv|NJ4O$nuw)E#GA`#<|Qzg zMxlVFOMdIu$2KvwtQRnu^fU2t2?xzm|CPyq5JiMB!Vjbb(6AQ^-&>w(o5p_zSGbar zw=VA_9{B=u>!hdgNkbXLO8KrNEsQMBG{ZUrwIsX?@b$H~H+eA1IG_gu6cm6Ke<$>6 zC#GUVfui{WX6MyE*cz5;_GIGu2NGW0sX0^jkJ-Q1P!uIO`8%lc0NqD{q9c>w^n#uc z6I&c7V7RPLc9+rMeZ%S)q?IG|`05`4E+;|eyPM;HY_p$RG(pd?5}MiOXE)~`vJ56N z0jct8?)^ZqL)FswNhV0=LlwZ`4y@PbeSRRz-h%=Th-{9A$mD)KU3jO!G697G4!A7s z5U7giacM6@HUdES-1s@DJr@mLPUX*lZU7^l?t7&Rb+r8J?-j=rav_gl*Q1Ie;l14M zUI&A5SmJf_^YeMldi+^D()Tlap$XJX7nrr6yxrz?RCn5}z-3x*WRFpAE?^oQ?rMwR z?ea(=leMIOQ55}H%Ay?1{%$STaULhQI)gZ5#+B(5d6Sq=!qNxuAAxxQ*O48esHw-_ zCYF96+~gm}#6+kLd}u2Jk!V8focUa~eivJz?CSPS;u=uAED+ATu}_0tC;r>T!ofYL zt02Sp5+y`hBlbZx(E39nxbG2X-9IQrmjf)F3;W;nl&2MAN9M?k%JdT$sQ8&KM+1~%sccXdxZ?O%f z!QWz=MZ|=r&tlt$H3IVsBb}6s^sBuat8-Y72UYce)mCsEzVI@94v4}{8mMQYPAd&( zn)#%B@mW}m4LifBpo5cx45{siK~)lm0);8U5OumZv^QHjXAf{I0eZh%5yG-j+XWuK zzuUoo5#)1s85l7WO97*-i_M1H;BE{-0Gz}RXkAV)W$gKIe_Ud~x^N7V*}95{`zssMe6p zc}&NH{jll6cNO^C{bYOqgHld(Y+Ld-W$Tch^*Ao%;AC`A_%Q+}_JtpWz}B%&iCfqn zOi~N2t4ejRs4EPy-xjSkt@2_-J9U2_D_gLAv~BV+#^jpea!sc$nz`eOZX4MzdDfzs zc`)5ocF%?rUD#VAU6b9N0?vQ0Ez}e|V%S1R$&S?2DqW_NKMZa^!%9;o)f1oW=;x;s zED9F;^7JP{9}%SdhNs^RKfR@!P2J&u`9gNF<*Ha&FSBUE2I8pNBhdE_l78Xa_{3V!uvu>ohCs zT*lY0j6PuL52T&21kq#oI{q|hXb_Lx4UKd%Fk}Mq52*ARn;(Or8Q_84wCDfP{ValC zC!+fqvsU!|s%Q8V@KQkd9^q6pG&}&pX&N=#So7`BZOg6wg_pM}p7z7eF7XWDxdsSv zf-x#^lP^q0LCbNcqcx#xOsZKTgbx(@MsveuFbsnkb=o8{8Mh5J1(>TzBi9-0>U$gH zNqWgEL)2%ompnfIvcEA+2(SVxRE#WzI_zSwuV(~a(Q;k=G(wWIB&IUP3RTZ$Txyjs zt3U`iV1Ysmh{F9ebgvhi+kyE+@~rjt{;aB*l>r;G>5keq_Rkf`9Weh#`2{b~27cLI z`*GG>LM#8fjdEgg#7`fP)O_TThGhk0p%-9=P_GX+4k&atPS(Tw@VXjim<}_2VDI%D z8Z|&52EngAca|A5cCJUj5VI!+j_IYA1Pegzffg~dwzdZ9u|O*erf6vmb#+E?$pI(G zSv8Ks`K*NtI2usuF31K~>vEk0+tQrb(2DCz{yeK-$X{`tv!Ze;l}EK-mckW?7b9zR zg2wgiqJEvAQfrc!UckK#!|Xqc$AD_CpsWT&6+N8Qegc&hgJ7HG5)YvvuTybnbP=%Ee$qD8N&ElYF|aDXr5B*$kH! z*{jAM90_g%!v*kK0M}PP)=QY^ueM^p?*3dzbh^!lJU`2)@4L-C>c<)%RG;3t9UYr> z1FV99R&=_M$%09NX=vjDGvcacoYpaThXqg0$<4jrXef$DzXQ@0hoOE510zry5$euiH<(133@jmvJMep6G2WCuJroOCq-(>*4B`KTc@vqT zLKn6;ytRk=3#@4WBvJ$HnS+7C*o0vBx3|NFha(sOz~p_RE!OOZNZCTm{A6-$96(oR zM}W!H7qAznpu*jG8s=z_k#KLNuP@wEk-a<+%Y&{Mp#((tjejhiChvnq^D?|tE1>%3gQ2xu$Z2!dxcb&#`ecAse50m+WyFEoevv#52ycv^PCeZKMbnlA%= zlVB8cQ@uVo=)>)Pt*7cor{Y?k-9DX4j+Owc#_}S~oUviDJlq|U@!&p@uMaR%mj1p|US_Xg^24FUT${EDirZYHJ$eu=l#w_4VU*^6svye9pI2*pPpjeKB6r_3eH-TB*buT9&%6Vhk)vz5!lz@QRt-<`(_vTkblpK=qzhHNcG ziuZ*Wauw$68`)HjgFd%?aL^alxob-yG2c2lb))G7Zo;BuRR%EwYp^5MMur<$E57h%3UB3sRg zLNtFsVInfRy33=I*K?!1hM)F_DNU+7c^@})-IUm~CbizJ+s}WUG9K@N{X#AD(ZyLU zB5)e91Ey69$|P=?TCimr6hJ2TX%MQ91pA7JRLX;56X?UR0RUJAJ2q&OCE@S{Ki~(V zu9XyWV-+&qab6&1t7(l6OUq%6o_2oUuws9Hz!&_{p?Giyr5YrALMKBz+O(R|B#_T= z0t9Ku1T`{=`R{j>1K!a(H^)HNP=wy6qcw$;6g=WhF4gjvH}(`@sP4Gc(gFMj&)sUx1dsuEO)2k_lj#sx+Fg1 z>|pvpk^jL>4x_S#*0_J_OF@Xjii25OOU$drozKaoDnWg-D|zL{NgGuH5MzM|#)^QW z-3eje(T!`ZA?9u!A6fwcW^cc!|2mr%21SsUP&D@DTAxL};kU%dLz&BU;H)p3T-nOg zuv-4TS?Mj~wRWSrd75~8^VEBV8?Dc#`?YY z8>cR?bCI0A>n2TS_$SNI9mIM~xRt`RgUpxlzzpe6aGMWP802vwS%*P-+o%hOdiEJH zylA3yeGn+dNHDw;G&ws9BOOabah~BF>x-sUo#m-iko*QrxL>4**DP$eP2EDZl%1-P zv2Sc{jx(I)p`grzQ`zynsFjASO{rybp`A78{fFLdLS1Pz4dt&+_PKaG|5?nmk3qj! z`He6|O35$U@z*`iBYWy9ZH~7dtmfP z0O6jazW7Rv z2jTpv!zUR1`T*hI7tO2|L22$Id0fiD8Hx~KxXknk3kz#Y#MvG*-LCsoRu+xd%=w!= zr+rlkxp`Q-TR6s{c{1RK@}zYnG&ygLH<#q+3(PK^J5h$O^|GXAkS{McCfyn5D;G&9 z{gM3%lWhk1P&Oj`U(gVbIWb&fKdmhabuk1OKQg(W7sEd7`R#loa<*Wtq&pkkdUYyX z%lNKZson#E@}=3vU}>nPHEjq|>(=MM0_BPE!t!^|xQn`ZUigB*Ma`yI`IXP@x0>w( zk_BSgrzHzOu8Z7S#8Sm&W92;4ciI!sTlT`H{QRbcH^k9 zWcsGav%S_)HSzq9Zp@dN=>O{Hq7%l{9E@bgh7dAYV*)cM=*03-s#PE(k zT4Z;;&L}>l$=Q*bl;9N@bTw$6M;zr9A=(H~0; zUzG@mwA9*_rD-r!U;M|walVDW4QVX?GSzQh6h~xf+b<2@{E20K1lS8;v+A+G*xG4% z26Cen^My}=hwFj8Uk3-HmZjNrwvJ1+cR4ozoFMk8NwsYqC9gqHoLgGpU}&qbzAtR^ryL&(^+ik>IQOwn3+xMaYTu;-weSK)e6Al-Rl#KW zAdwKlclF%=@}nTKfz2DdB{%`n(vHW1i>%8!%ZN4$KCU1BB@&SexaDyasG1hT)Gif2 zKnNSa3xfc*90Fh0&)~1{7{FZQrMwhGZ*@lUKs~C*(t(JBAaoE(bA8mQ2*YMB{UWNl z(xo@-)DgS5@eFvXChg}Bst8h;r+qC6{+0>Ct1qF7ft)oaV~)5brRZVkCw6~0a!pKy zDOJAX`OqIX#FT>l7I5Gmg4Z{g(DDAr#v{M>eL`?Rs@E5^J!VM?c0+JgKSqS82NBgX z5q%~8Jm1d}8H%yQ|G zPebprtTO1jG5e^V=D^;|ZU+N9g4t~RLO%7ZKN!cR+ADF z3|GJ}!rKKYs+w8*i*k9FoEg6v=T`yq&v|EK1l-v}ZK+x4jv(y6rta(BO$jGq+Q$UM zTtr*9@i2^OUG<$nv{!4+2jC_axy*NQQ*GAzVcD#BFBx{27`tKKw0YT06?~)ND#Mw* z?7^5O&@J=DZ|23g9m`zE`^jn-=JY-U#D9I|E>53C0ug_4hsVLcNJRr-^6wQFj(>Xd z<7NnjWe}WK2VPKNJzt+`@eTVFv!1%Tdk<30j)~S|6$$KZXlQ)YVY-g`-mPV*k*P+A z12;G;a~^&jwj}P?vo%@k7Bl&Pu|ZS#unuxZ zaLDZK{{8zXquv}akN5%fLDVzgw57&irQ68j6o8d(YVf#Kz`)fcjcE5xRDUDpvowaz zM}ak}ScLGLQ1h#9JF7DiszlW=GF5pB0|La_8Ai8=mOLEoCT8F?E6^_n?8^mww%b`R zmnsGjLR4%GRQz~rkyVAvc@a}Js7mWWO{GnDVMAhuv;^KQi=K=s_*^6?07cedMC{|X zp6vD+Xc(^`?n?QjjFK?PcZTgB3=e^mx+jMcnz~(@EDVJ|*x~@{1ovf-hO+tee*KD! zl2JK%tx-bMz+(ZY4fR1WCg~+9&I=ozkDaCG?LUEqI2k3qZ+aWJ`)~j!NmNIq43~_J z*?pXxGG>UzPa980hZr)fht`IrXhP!Ygjh|O)0)d zk4yfRKOgck3?mC+fAmCP3}!#+pLK9y!FT=llL{aBJ>TITJ*p2!wT*KWf{KZrqHj%!p-FJ1Lhn3$NChVR;F(jSezIG6Qtl0*fWh7=KcmhF8%MR{0NIZc-`qnom zu$WxY6{>h1vqe$~GQa2Ao01G)Z(%xxL_}JwTM(HWrV&8mzG-68a}pLR0nUMqF_@#! zU44iGtF&iB)>`{nnTgCLkhSx9T}t2VP)HueyrRRppM-bzmv6El+AjolV2SgP@&K7+ z*DZ&sFIrb@IWYF{268ckw7e+rznkG9z zeEkd{25XK0Ig0eqMLfU$>|zjNafLA{LgF!it=sPaY&e7!&|yW`y#}xV z=MKCLRTS0d#?JI1yk0{?9=YJ4yx~c^I8df@mp9{rtZbIomLY}~1`#qcGM*Wm9qdkJ z3-5k7-DF8v*KgDiGL}oj+%`1K_T5v1*~dQpFs_80`U_d+Mle)W{RQxguEW6qA}$m3 zod_u<;ZVn5OIgV_ye@z|O0y-AzbUPC-`G5S7*OVR9=gZ_HB+QYN`!@F1G0QN!*iEx zw^O^Z#<87%az8Q&vt&Q6W5p{cUGiLI_$An(eC+MW9(=+0Vb4jaKo^o2K^yvkE1{Ab zFq53oO2Znup58S8hYLmo_`iO6ZQR)JK1l}-*eE1z1hf?pegt3e2eA3NKGVBuYdMfH#5uvkHP79>Po?3Z(v(WBg4#Lw1nV+Gt!t*O9oo z*-6`W)Ag;&?^^`bBd zmSuz_Oe0WbhE~w6MzwA zv4A2F5hZ*`j=^|)w8o;{I>0bthrwwkrx9#% zl*IlW^gTALXC+$z9$xy~z2ivk@b(0jxLL+e%I9T61l{vmrTm+76|n0UeXt8NCIr2f zn%-Wf8-YP)pI8i}d>7424%~9g=At$!mh)*}loO1Wo{sny2kIK`$&lSGB%Tm)-*7VQ zTE)1U(B{renVThDciHMr(&Yz`$DA^;*y-_gZ*wA3i7dsio?X9){x^Gi)zqL_&9u?D z%w=tdi|3gWKczNxYyJ$%FK5yy^6^Wg<;=H9XfTLLAu-vSm*5IZr7^#**s-n?_&M{6 zhgU7*x|da=UgHS*cg*9qsObJ^Ld(+`rdX_90zQhcwh1zZiIynC_*XIvE_Rb(P9fLX z*_lfZ2u#}VbegV`;k*ZG8l|_jj}%LuKR1@KoUk6B_C!M_M?2%z7ptME9kt$1G#d$d zX6oqNLyA+M$%*p9(FWeNMso04%DtAnv2JuFGBZf6 z8S;gdWQ7E@`?C&MFUcN6Ce*24YWeS3SWK%5M~C4e)B8NmsJkR~PA0;6$etcHl;yXW z?wr2~eoR|eB3qj#X0*9Xa}RC_J?Ai6MVk+qorqfdbOr!5HcU!)(%R{+l{|y~8W*@C zInZ`tUDKz=K^mrEi&38I9m{x8)SEiI{QYtnP%b0By^}nDcM(51Ihp3FOJP0L!Ev=U z`S#Q0cA3P|&}fwb3xTH#Xx1vZPjc*r78Vw*!fMlb!$r^4wp0ehZxPtU^Y5`%ed!rm zC#~H{Sy=>eM?^;&&2nj0-F)9Y|BH^gcYDCkQ1ZOWiNQC%>HIyTEytdhpZy61OGw4s zr#UgTC^hrMqlsw^Y&%!rU17a|846IB4~eAr4@?VBf}R#%*7AxOA%RfmqqMh!f(!s@TaCt`QRv1Y^-~~24wX0hPz~a=+OzU$Z zAPm`@_0Ywv8pVdJ_)f{FmxfT%Z~Al495>RW4ehpmExCu9nmYXL+Z)2sbs)_wRf;`* zlD~@JOy6j%+LZd-b|oY>#q2PU9D{AI{r>!U&#~8gPtwqYe#&ImC?NLxgot8>7UwG;M(#XFD-Tr_`DZL69Pe-v(skoH)qEU%ZNdDgAV z0jpc?;jxd9ako3xXTnc4)!32AT`y&pG2a<`?D$Unx8|9d4E9XS57ny9XAulA1tn;_ z(dX8(FZ!^ayT^*ww@-8I6=pJ39~uf6?$m(i0oZW{%CjUs5_e);3Tjj5V8)8P@$)+u zdJDb~zkS2)MJ#E*(f1^SO~!`hj!8QWGP7Fn*nrLvgzfh+RavWyzp>j<ei0gup~=b-%{|%7T=?O`GXMz* zPr!K;RDSK?R|)g$sH=}|)TLgDi*dqvzL}aF@^Mie;D1hwjvGyMMwZ6D*TTFO`Jz@1 z4w~g&DXTYxrXRp+yvY&_Guxo@54u#Oi5@rJH~l2}4!ooFM2;E31`~?fpO_W7Z}@& zm}w+MINMjw0zZM219Oa$&YX(Uf>zL4^75HXA>|*EbLtxzp0}NnM(g}jemOemilyzS z;9f|d4L?D~IxXIpf;mEmFsDx?svZQ`3|NsMk3htf;M^J1n1IvaVK7c%tt_dFdeFR|i<7_H4U#Sj=^WZ+I&yTuU&*E%r;XR>|lsJCKLz(Mj4hRpL z+rNX4i~jaWO3XT+*n(;t+ByBIf>N!yWcLwsvD)fS+T5w}C>QE2)M~)X?7ucD%w2O^*h`~Nya)CCIL8u&7{4EN_9{38Tc-cX*k6{?P6WGUVx};3qwYwLa1hzH@Vtkr_I+0*s95_JX|^ zS-onF3iS4EYUhti!@5Z*fMYuD#zltiofKQ~_@3tHq-1HD3+Zct> z6pt>1clFq>XF6Bu>KYon^sT|#VA{a}%qJEzPHbHlW|I?X#@Tk48{fjfyeVvR3rO$v z$32^Nj`Ju?@kyDBbrF;=PvhPXVv%X3>t5AV-1#nV_{QzWKEt|QfLIekk6#f_FT9xRLx7} zUypNSt~JV;5;?r}LzdFWUKLJe$!l&e;+~`-+h-bs%KI??DX4hR#Sr_2m(xhWcl7p} zUHdf|0(;}@*9RSA$WaIgnB-&^H6HA(eGw)&mO2I5Zd=o^ha#M`U)VdR_5)+`5Nd5a zSZVn_wPLf(f*xnoa%9@@HDO`8ie(jC#>1dCT%2abD3<<|MbtX!zMRZ$*o8IHUw=MH z&8(TS!l-$OhX%kJY0yeG;rM{Ek+zsIX695DwJn3(ej6jB8|#zchtF1cFRU4tX2!*{ z`w$Keu93o$Sg{3^*)i``@WaLru3PKO0>5HAy4=^<)fS3-bj2G|V_BmkBlUza%lEs> z&U>}J^m*sXqUmfcbkgVISxD0}=taMtVJ@@0!ino@Yd%6w9xhgu)~vFa>gw<&MrC>cKY%Lv-2W^rjv#^<%%+JevhXZ>6 zzpU8G%52Bh7HBP|HS+cAowQ&(;fqU&2xvZPC_h@1K~z zW&6>Uoojg5TDm`Cb+?vrCn2jlCovuJ4J#6kyt{4RtuYm=AsMgIoc*%zXez(~e)>?* z>#jTn58wfQsf;iaCt041DE~Y4!1cj36b(4v*f7pHt2rq&v=MnLar_n;5B^4#942IO zGG(3Z_#pxGP4Vt)8g3lciw9psJGQ%fM>?zGWx7cAA>8jl?hT>T|QA)80umvU=#6G*2w?DT+uO zk|Zn=M3Atb2>i{Z%G}()G+rg^Jh0_7}cR@1q~zYrQyG^X0o5@&Nb7MQ3SAA5xr_ z>U9tJ*yM&(&vWWRg7`f?mvel#QOu(GhCS>jeIPhG>Vf@xjkE7^*PDL3Q>Bp$#9EJM ztmk01wc3UU*BC-|9qAy@#w9*9vnxm{S_C#W82x;;YfU@yLzVJFgGHJ1=NtQvjGKr0 zd+Ta`TK`a;ihjCKp=+<29$P*L6lw|wxg;9MbM=vDxhIT`)8;xK*=hvQJQ|dp20%Zm z1tFON+0Pq7r5_3ldspth4Qf-tSy*8(OG`^P6IBZ)FXan?au{|H_yUbVl1wBOrVVGy zvK88FGQQ*pz*MCwrR%RY1u^kFe)Pjgr9(*R_*^^I^_Po4aJI9kYN9^(Jy%zHU>+9W z<8BPmpME)r@QT!zMbKz}c@}p#G?C{`)JQ8kCE;Sh%M>`v zn@i))$J@|aL8{by})P}OkU=T@MVwvoV6u#*yq;8osZ_(_AllubZ4`!=2jlz!9_+#XLeLXR-F^OA>McOXbE4|DO*@wVM5vWOzAmgQp#4iLA z%Su1{K9b-%`Em0zEyqAm*u4z__;5H>{_aA-k?<40`xMJn^RsUZxUqqa#$06lPb+=o(&Ib5`zx-uQ&RJs^nv%a!I%$>_jXP!P<=r@Yx;@b$vdIfsiwL>Vg{!v3`mIWtS;t@9Wu*6qs6;t`quuM?{U_aL zQd8_v#P9;?bim8q-Bb&eqfHKxUACwvpr~JXs$b!UT+^Q~?EJHS6G=6-;LS4N?WqkW zwtx{iaK)*7YL`be&~?T?JfSkvmxn_XWNPa@0F*F7TO9 z_b)eoU~Mf8BPH_cukl$o0RTnpW>IjfcAHfyr~{ujRi4hieD9wRAuMvo;WTs=-q6QE z@$^oeW3s))_H~N>`!jfwwVe8S>^$9(=1_-z zOHKbe&g{}XNLDwrQkcvCy75Pf%pspaF6pq=?Zj=JV33_VFGY^jfU_|Sb)Pk*VbPmE zD4{8H^?(kzb8ECYC{_Is40Fyp%ne^`yTftP=PABhE4|>Dbi4|@lgc4;!Z8^A2};FSlr?Fb42HQsi)Ax!m+-=CD4deVZSm4KEWsz$ip z@X@_MeXN4NtjvwgvFPP?6s7lXF8$SqQ>e|{-fP_?Bi>z1EvcgiPH!S2Be_nQD@!H{gc1x09e?^UGYXcy;%Vt?jWC@)U|AN8XO^u3qA8 zPgMYqP8Sayj?f!0qlMqhLZ+wec-O|lq8ez+NZZMWM103*XJ=1UF2WK>P-X8d;}qwM z$P-Z(h5ZIdA4yIcm&&Z zU?a|wP%c;?-6F7a0(>6e*fOyM1c-F*^ZnqPNyiKFDkPW&ZU-Pq#ro18_BMKFVkdkQFb&`$7=QQ{XSPqnEo4yzpdP+ZxqwzvgN9&bA3 zLdA5mOFWJ>*7?}Sj~}ly`vVf?Wo4tXiZ=zB>`uQ|*FaN5N{*lTn4CGYH&{&NNA(f}GLgcZR@s4!FU4=8qeDsWO_ibMr_!_coE+ezFuC><=D;l3 zPS4ZOJh;PW?i#}Lv`vruo^H6sp?vQei~cvWulPkc@ZN5_!hSzK2P5OJ$Ls&~p(x-p zfg&=j0*-zQ@1-Ufi=x!`ztKEX=uT-~Caa*s?YEUb=iNLXZ>0K_wVlp3)nUH4#Pk=Y?beyO=*lA zej^7AvdUavpHt=t9q+$8RUn*GV(IESY?~jc#jUl4-1GJ014{2L3p4Z2^0UF7o3_8l zd|wq{cie71-CpYj3YXX#NdKIp$j_Fj#y{%^tR82Ro-K?KcYdyFc~?(%`D5JF`yQV) zE)UUiKsj$s8}{;$=$65%)oJQ6f?g|zbp>LQn`Xmtw*8BiTpa3Kju&GY zko>fDW<6Mo@}4^|7-`51#24HnE-~ebpwy&7NbJU;w`q*zY>M@>ggxKMz%M0MMKL$!N!A4Oe zC63w8>31=i+j?j0JdC*G!QlcqH=$}ELG5g~?bs9_LUe?|>81#ehR0=6t_TEi3;sK6 z{$M*@2ZM4H%8k%$IPbPIw^Lis;KtP%KVQq^dKAfvR@SL{3<=%(MGr~f7OgcZxxT2_ z_*4;WrUKC`Elivs@p(mosB3aenoojE_6YJ3!TR^8pEMGj9j8;KX6#X9-I8}?Txv5IJ!_irK!sJiF{}E?d`c8dch$(YPf_2gsXvAEE*fc zx3g`Jzy*b2Dv-~gpY1|XIOG6{wPzmIpM3&n?GHdH)-zG=$3xN*6# zj|C>8IUtuKhrRXrV95bgzh%!-Pob^}EiEmQ&U;5i ztSe9oHysHw!*!E)eAl0VWmsoE0Gqb8u>d z-KH0h>w?c(2e#aJea{`JTS0>3SzC)dQE>wr&ZiK69!Mn`T3zvFu{h&1RRG2Z!ETM}3$Yq6cqV$;nyp2SC@KbgH(vRgXL#YyHUoW%!rCcB4XV6I z+3gF80zTvCqPo{a)jFDZLATgpR8EZh4Ps>h(-Os@thVvWIn$8s?IT;5ZKk{{yfoWS zqQ1ipGn#lejQO~}{krv=uU6vx=bHMSzT@~R`H=I35)Fk4LjgY(UOlp5pw?w!K^X)Y z=}WLI@{xCiHeR1DTF)H3QjasuDgKoLb>_<*k1q{b)Fbr5`jsnvNMM~jm3aF&m)W#;uaUvXjC zI@+Wh_dFafqx}^ofU@m5w(2tcH7yeQwC3$h>$lQ1L z1HZd-w9i9h=LBm-cve>3pG|Z$V&hsax0N-DbpYcm#yxL7z`}2e6#17>2yK^r^R;gu zN=GjRJQmai)}M+9=E^rKp4GhqZ7(a_ zX*&v3nX+HLc<{WwNdI9d6<-n%u*v|VB}mcW;yK=tI!F4W*#~Mv&ZWILCa~835EC#%WO=@nF!*cXR=Ns0MJ@KX9=gpnPeSoXqST%Ngr08V;Qta3(Vti^`m1$GXfU zUt-pSDnG5aEyBAAh2I+#&sJCr*&ruIySH5a_|iI>FJ$)V^h?r24M##RSzIuyxxI90 zQmzeeS#7PnzVV*jyLsL8N@3~Tn;$DS@NU=E6<%1SAh!>K+pnnbUAvAeq77%Qw;VeQ zw_W3murk#_2IDB!Gq%iX%vJ#C*3Rwf&tPf1&KGifQ#q<~wCDLSJd-|sk&9__fFu+yc?1AQ0G9jV>kmk*vb3eM#jDZl}PMjT?;O`YdO z=Is~fj~5C3a!%(_8AcN}kIgQ1j&*E9clT|7l62>WHpaaot}>TZ?mz%w6sq~2xFVI& z(J!a5dg+&-2Y;y8x6rg*3cYSp7jNDzu0e^a7;nS{Nq3M|3I8@g3Nd>AT|G`Fukz%d zwzxG{ipHx!s9g4E0DV|3JQ}fc|9D8zeZ-2u8vJfHO3cGdHgkH@Z17%P zH}oXTvlM8@9vz8sy2LBhcTt#`xpsWX%Y-=vbeyCMqA7dRFR$|*)JwP_xWa4qcmy}#42kk0TW~KF0%P_ zR9m-5CcQb?-xm(A*5@f7VqG4|4PBSaZiega$h3E%C}H+I!4F zHV`uVG5c-h)U49vkS@B;IZsPq)ht^!T`$|F zoT1O<5pSzU-29!~%R*+K4W$%<@{F~XKDLq!76mwHoT47lZ*>_~09|%-a93;h+mO8l z;|Q%4QVJo>Wh%J(9NDs6l7VVm`(iH_ZAM9MZZ7`_l5U;}?{PA2&7jCds-`4_1ah!&w zn12!=)?1ZWWP^u|<(;NyBC%8(&z_edJ>aj+=HI;gT#<()92*`Uo~H?1PX|S}KKa$r z))wP=ap|73wVN>-=gy9Naj55ZJ_-*vMq5`v=W%3|Mk47Z64Nz=@ujw}lXS}NaMpL% zYmaR~p{sy+D_`e$;Aw%)+26{A5`FlT-&uWamxD zCFA%%f2XJpJyWvD@oLm`w&>sXBEnnZna$X6UnEp=k(r)`nSIjoj-yL1f5=*`(NLdu z%4gfHjvlQ%If?Nj-m8O})fZ8^k6h8C#&G%_5W4f%+kFC)sMXFUY1ZA$H_^1V=+v4F zeI{wwuD!K8Aat<(p-{H3a=j((4YgZHvshvAsfsv`-4Dx8ZSzrz_BTncR+E)|qqgzR z?OY6BiEpm3CKo_WX_3|wfy;FcYA+6Oh#!v|z(AvuxOm2~>FF1?1uGTxzt`-NSrnIB z*3N*4Io?;Z$fD}-h-R*yQzvhXQ{ej({!6c4sR%G%HhO-hX7P9Dif1MK`}&6T@*v$Q z<*77RA6saCNl)0jDVKa_TwDdZ<^}8i<^p~Z@tC1bD=q1Ih04Hk2JXvob2t2~f~x4~ z==jl7yPw7?#5evKYWt;BmWS4_H7IvBY0dIWHl63S@=l8|!#-yDs3`AM8sPgruaGBZ zJnDW4c;mBAG2^b|6MyLPOMPXQae48=hS$F&zrnV`De$gh)oH5}*QFZR2MDUvfSw?9 zTk*E^GIg^(8I}#V;8Vi^2&xaTUco9E)DuN{IBGJr#CMx-8>fQ*P!;t z!UIl?U^pKnC6(Cha01MW(P4NDj+D&P5Ls7v{+iLgL}fbm(Lw?+N;>V_y_-beH1pRL zRA}=0uZ~nJsF)L3ehS1!ji0;B2K%+o%{Q&^LQAqHCM|w6Gn!I+nzPWaxhQ!0X?^+> zq{fEO-O*7|y^x{JSV10Lp7AXQ%<|dZ0gXYw<60PEI)2WWX*#=h&&uS)*Nd2C>ImOF z{h_AjI^KFAJNw3fBk~8X4oZFF4F!TAlnk~P_&8wr7YM4(9cvM0OUZw(m@_PWyB*Tk zIQC*sOkzbpaTGEg#%04E>3ZRDk@wN-@BD@KB5Zn`7pNvQLD+?IEdu*})-!MIuj{v| zAI{>L70k^k+4b%ww}zjBg|V@;)4XN08DRu%K%xy$LO!LS+g9dvkBB0$`{Mwc@c8_6 z@ny4&|K33#u)otgy9?|m%`K!ON2T~$E!>iK3pN8peJGr;;+52?aFv)8dltfLJH>it zW{Qk2m5*Hd*>T8_xITEKZM1IrfHKWHAHHruHI6M1THYI%H^`Do} zr00eKqS;>e(xr~tC;y>+L>hem0@(E9<>&n!}dy7EdDy}L)N`J+x&|z&?TB* zH;k`uV`Mkw7uUB*n~Jnx2-+R_$lE2YKT(~c7BXGB{6^URM%%ix88bM4aB_^9FBVSx zDft_D-tr3jgS>cG?JnB-F7=Ys@_yAi!#INvF|4U*px)}PJt}xcL!6DBg$yA;5sC8$ z*95K$^s*#Qyx@4sH{%!5>Xs_LF9-*q^RDBE`!agugN{${=WE5)o4Y{%G`eYv9j-hX z#$X`ifmFebSY2SetgY4P7b?T*zJVZP=(Bitr+f0SF}+eH66d^Mb?I`~-ke&mDkq<_ zZm&HT-MfAj%Mjg*30z@*@?*HyEtbZQn`UUQr^qurEIFgwV|8ve)=IyRwzCt~^b_ce z5#FlA{^G)dNJ{o%8*0&e`A%iN!1pt0E!|&ME709eL^6b)v;>QskFuazjD6z3>91U0 z8?Z(^MlhqkqGCDf2+)rROmsuzfIC2l%~|}c7I8*ingUI?HN~%a1YsY zwW;IzBX1)^u(m?2<*AEI_nTcgb#-T%Yc`LrvUOT-(>3Jvbj{Qnx#<*REl^-xX_hl_Z60?Yv+{o> z`irU>tS!+@ZgRFWbhn9IdXU~;Nf~R~o3FY?`p6B6(5(GOSFn4aPTuIR2&?e3;AXUx zVyE*vr2ew{3l_FV`$JvthCIs{5^Wf{wT+!Nd8%@~>Okxs-`|9Odt9c4qV0^Kp$IzN zh<--qlP&XKg?b=Dcy$?{qcm=Nh?Q#;e(ch*i~s&#B^0I zzF(#;8a|W@ct2;)b*wQ#5Dw1)N(_QS!pGr0BItGWpZO&QZQQIS>8cpNI?^ z&8&jUyPWU#1rd4Tcr~pjvN&k{-}#T)*NAwP&^uZdhvq9rmq&GWMBmS;Sioy1r|f-E zO+q|of9z1f3B#VI$#`mLVBoHna<7S)5izH+Ve;?WQ{P-a_=2H(Jia87O*d2^aR_;7 zxmOFpIFQgcs zCDveLMLFKj_d~eD#|bz_%Bo;H^Dk>p+ZOsPL65K9M(_xZYv%a33^Zjx zHm4ikP|?Fl)gD;sl^-{j=N0s6GR9vyXH04AlY--DCC~NAmTyjbrMJ744@Jz$f0StR zu;Y+-xm)0+H3p+@n&<9@#PpynFPzLcRUQ>d{5YqmRK3^n6K6ONHvB0rel||I^mF@A zJPK?OS$_C9*(Hw_CC}HIh^~`K6!T26PnJ{l#@Erba&l*n?0%%loz&%$5EysVbbtNXEhNi4idz6R}$ zk(8}=a^7kZ>oj@XWo8h%VEfXz&#|NGR`*Mwl)xnUPJhMf8r|E>%ra_EM+cE(uwZOx z%s2T>_Ep$6?b zBDIYt{ZnM2eIzC4#Lj@Yq-9ZRvG?P*CDb0}qrL?h7BPRnuY+2Q<`YP}zgD#VwY3E* zKca>(mFUhr!G?R-kufuRJSt4<(_9ACl}*Go&|!b_u3^SlySDS@#od@^qjpYD_!;+= zAlkb8uU46!w?-+m|AV%V7Dlvv{I9>iWdB{;SM<5U>3Z4gxPDt^E1UT`-AY%_z|kuw zLZyGWEu*J*>?QbPSES!NNG8)}#4~1D)Y^Ze|2JyBTl?jri6(&<2HYl9bVCA4T0sz< z{;@-`pf#pHeUEs~$cQ}@EOm@~weWq+#EyX%K&|o!#ko1H)*?O=5comh$i8;p(O5P~ zM(dVPpgrRn_)`#gf9%w#zODPzG9<*W9+By|k$+{_Y*9}8pBN`UG? z4Tg^i*KCabqp)y>K*iu0-Yy@tDRgMS(=g}3-6z!#!l7#w9PsqKk$W}`oU^-3UcWP^ z*C8Cd4}FDi>q$d)2?!qRi9eWQ(ISyimwZ%Sp7`}lULx@QM~74M;~e-ce!SPC3i}tK z%J9>#iLsuUF|u%}hu8WV@%y3;3wa1AOxP0rNT71KDS*40&yE6vjSC!5GNm_j? zJd{vjC7-Bk$@zyDG;&0$q;8P7%JFKc+!h~AkK+(TVeCR*$NUqT8zlCGaZea+UET{& zT5HkAxmj9u4G+9D{T9C5*ZN;e`(A}zUfuPCXXiV4HGUdt#{gy)NGCzU8aKmetL;rs zgT%_}g)i+qwC@pe4lmawmAPuqfQ|tTZi1Je#=3f&*Dh;F?!I1yB`zyHQGbE#{xlkY=^=kol>Y-2x!7o##uwoQgl$#&l63qR z>w5mDp7+G~P8OJCVDFHnzatl6sl?0awN!tc?{fp}LvI6=022*A-Po+@tbzaBc<0)-9P7**$eOD-BK&sr zpbIr=x7V3`G=pdsm_;rM&e0vg{IdCH9p~5APGMj3z_ochj~ydw3C$Kz{SacQJ?z}{ z&-7p)+cZ7cyD4t%dDEtROJTF@3WeJfk2ckw;_K!)b~t-dn)sBFh$H&3?9k9 zDXBvC{Lp3Nx+6<8iv=(riNyuH(95zYtp)z_60uLWg=*7t4EpqhrAMj|fJN~>2alV9`diQQ2Q9vX)cAsV;ata6?3Z5cdLLne@f)Sna zg`Xym5Ofj&fqZ2*I49Xb@Q_CdqLN-@pvjAne6BYcxydBrujiQx1xy5a=g)Hy6WAdJ5F*`AFI9wSjQBu{9|DtU<+jsEm8 z-beHaYFOE-VC>?c`-bZpCax0xjD=`f(4v)Xu0ozJ(rv=-X2d7uuEt?bNT2w!#de8Y z8B-^*Fmb6*X>{y+sbCZyZujbay~eT;v#7U-#7%4!8|!^JjJ%BnCz&F!)oazI_*FbhwE1a3 zWlni6+jR&hBPK%Mt;%`Gb~f@Ap#>cWYn~ZZhBuQ(9TqUDmJRCz{=8{)|3o*laxHvC z!=TriZ`}XXF^4mW*|b8qUA3@vcBNn9lV$o}4mlGVcj3#&39O=`2cDD5*ZF?sw2*)C zW8K}&9~)w+(S`P|YN6|=pXhv(KWr{uWTjmXvDtt2uKL#tmsIPnLniMNTwO^n5JO=7 z6SJ8a6D%CIhqv)mddFBhor_I)_ZzQx!=E~@tf4!m%5ulx)4ovJ6%#Y;ky^8;ZO+hS zKqo0Es?B2UN9o$d1w3UXEp>iMO~N*I77AhnS){fu#}-Meifs??PuWaafvNxo0{tOV z*ZU<9))`I!rR&cC)k~T9D0wqjDSF*@N$oy0;pOD%Hu_*!%0mN2RmVny3UP6a;NB|2 zz6E}!k9S)ZH4qMd+pVS^jU4rsY^g*%+?5afxXr(01f7gmjpYf(zs!v55-=L^+@^Hq z06%b(?t@a(M3bka%Sp3}e%0}7{BH~bRKT};;CcQbdfrqfot)32GuP}m5b2Zt6xaPJdMY4qC)s=mBC~=kLp8=!X|E$qZ-{KCvf!euU zeJ!VXjtYo6lC}@x<|`Ry;PH}bqqy*;6iyBW1ut&iJ_Tn=|2A}RBs57yB-AugS1zZM z?7&hjd3q&WpmlLe6Sw>evsC%`z`{9rqQNZ5$Xc*1INefLjE2&aQ>k{>;&Ipckg!QU zC{%?~<*jD!Rz59L7AdlOKI2X*hj1FwbNdD`CTg)7isDbw($)tYF+t87=$}XO0yO!8 zOXG!n?l|!I>!3&mB6q~TY)KWgZ}@Fo^ReW6?M_K85yOc*u*-ZQBCv6hQgKaoq0j?c znM>2?Nt-Rkwdz|d9zoJ9CB~g3FU;*7xWyx(*F;!rq^qN&%rgGP&0y8ATC!=bb7j9* zDU<_!#7%Uxhw$_Dj;`JKINqPuI}{cUUs0mr8d`TUVrezo(=#%@ zLZ6u8bRoOC2;9{6ff_UQ?z60{K44yuxo8IW zB-iS&=aft?*8ztq{0>CYD0G!)tdXt1Wv_n`@+7Wp zm+RdSy8OkLY0nS|j@i6a8fQLsH6m3oeCT=W7CTxGupaWd#sX8y>Pl-`l3xRL1D*i( zijw0yiR@brGI4us#1bsX3jEo0AK8mlQR@TEuk08`&pJ!?fw`}>lTBF3xDNWfec zV%Id!XQ9Jm|3v+ajWBd0hynB>Q3%R(LqbpgbcM%OYk@F1oQ~zkt=yq*q*K8rqVwko z%$5opb8K)N3TO&a6A-^};ym1^IvE!0QfdV8LJixsm~pG>=pp?{p5!R`@U~H99&HkQl5np_M^x6L!-u|H-Y|Gubliw(*D!FVPg* zdZTbYhsGRR;IOc%>;Sidqy@l3_My2m4?5~9Z}9;?XHjy0U=5RZ3~R)f^)p=AQ1a^- z7-)Qd5~WeIQ&8(RM2APwv#BLn%%N|s=-20mf;0~U_^#n80{s`8ipCHaEvoN;kYzE} z`+Zv%2kS9Y)F!x>Q^vF!Uq7m3z=wk4N}#+h>tfQ{KTpGpcN)ywVxz8 zxPLGFcm>f4A`8Tje~g2tD7Uz+?#r(rhqRWw&-+QrJ^VW$8ro-PePmtWP!U@0Fdv0~ zkSk$(!c|<1SGIL_rW)){T|HN=Cnj~f@|&CVmu{$@1l)OSzXA66w+M%pCw|NLl-}-3 zKdW6*()ZG~bLx)NhRmyn7g)F@Vib4euL@UP$GTmZGL#<{O%?V{jU;1OfR008(7qsk zcAtzs><4GKS#Tys8^xJeSSZo2fHU|{0S-z>#QrV7q5J$b`5db5JZFxj!cGF`j#YI` zeqiI9(FmE5KfQ9se#x%<>)sznBr)$(323qV7GTEEk6WD!BCJN$ zGvN9fi>G=0ly!XVv0_N#8RZ3_QRw!d^1Kw|Cj+zU!7m18h21X(M#M?vg4b^p49EVs z&)RMiD^{#%E=pdxcCi^XVT+bbj|uMq0Lis}(O+LrAI)>JtEaew=z0ir)MA%oJHW!h z3(wdouLWr{GDvD%OtmGF7Unnc9*5fp$MSAA{ve|Aik%K!;Q1N4z%w1X5R8+lxal0# zIHJQ|oaG#}j=+Pj$dJe2o$gUylHxnu9 z8)*$KYG{&Hw(*@=x}?H{D2e7weQo)va01J)pr3<6b%lo54@vGI(C zv2S}_3ta-^`2NUhz&S_qY+e-|!O@7N-OWF9nqzs6-i$cnWi>Jtt|Lr?|LK0x#s-4L^A@rZN&-*rs~xjQg>_6DUT{UIhsLPN2k0NVy|uW9ptKa#^j< zHC&k{(q<5JsG_E38`}p@nx;F>#V4bSmX0TEkX*cfg(-8vUMGb^m04@XRa(YC=FaNw zk^bt@P-|>jWln{=E_iG^na|v8smUjBN?2OzXEnQS2jCe@g}GB_*FWO$zL=AyJHzT@ zA)Z#|N&9=p$a=O^mg@l^ahFaX2=2Mpq%soP6e;&fN$P#{vv;)}i(Ag^6Qu8Jt{R*z zpJNAcnGG=`HZ&v|?FqNY8vQ(!Yvy^tRKbPzdAuUj$Km6FAfB1;{l;JYD?5TxU2Ba+ z*SqW733K(}@_hBxYN|is@=@(xyLvoQmcxYRHas(Qe}v*slIb7UXsj6{4J(8-!#^q* z8HIL-MXLiH!2vBUSf-9!}U$(@K9gX@_7%dA)8kLUVlZw*PWF!te1}>j7^;5;g z4S8h?%pfwk5f~CeS-o=Q?GgFSOLyjgTZ!9WS!C7UEEizL9F0;8bcp4W@4Xws-Sg0a zs&b(pDOj5M@zVbqdw@X@Q<(qQ-yY?E0T<`C&V;A zoVZ%U@MFhb<|X&fm11C(_?>$T9Uq32u zPrISst(&MEZ>`sYT)Mfd;`cF~JFIMnDpPg|yPBB6hTeFT^FJgL_`!QHewTMii-6vg z4FAP)OIZ?6@W_G06Qs^d@N=DtCwN2R33MtuSQqzSFI=%cIWCVqo)Of6e5L{#`!5M) zxhfMxJ(Y>U9(1V-=ab@CG1D){v!LjA2e&TsbZZyB&Z^XXaT&d?Eu&mSA= zeM{mS<_8T1Ue@~+PoOl=o=&;UxF~Q?)G+=AB_^6J-X_-e?8-Ti3Y@uQ;9}_bLR!Mu zSa@To7WL+vuhz^=q3EWH;%u2@(p(1vf0#C$sr&fMNR{QbKS)1bkbLY-a}QjyUtz`C zn_owi#pSKHRdr43ASMTHq!kjN4p%{7!`K{BvKLv-rj%u!`!e&h(~q+RtyVA^y*qYa z5tsy|wSB^q{laAEtM6oQ!_)VCEJUPCrsw7k=L>5^5)J!WQQL)R0iWGFCjRc-ySL)w zf8UxBK-#?=lf4|nC6i;yV&jbRr{}(0cJeydmIEJ>wvl6cg%LbVa9+Bbh1kFD_qwDp z9*a~rW=lZGAKFQj&HH(L)nayY2vX<~&~IoR1-79U!ZKAV+}ID2#$o{2F1A zBM=Z*JPX!YAR4WlS=2+k-4HI2Z@nb|F_SerlVrX@%(!dOZAdSVLYj>Z2QvGc*|+2! zg#-l~8z#pa`Yb00Eu;2vVl`wF{r3y5To+PEJsiLjP6C6fQ%o0xiOb3)6A^V2a%;(8 z!T2-Zh*3-KFwvo--8@C#785xH+!=TV2h6gXlOFth=k6;fF_%81>u;!SM~j%<`(gn# z&CI+#uh8;K-XlaQoH%h}%9@9FQH#>xH>Ofo!y>{PC>^dhl9tMKM_C{Ts)q3=PS~CP zUPlP;yRG$$y3iIfiM*w5p15CiiGUHPCfn{>Q_uMH7w*P3cMFbJzTt(F2+hCWg$q9V z)sJA8(&B>`DUW-!@Gp14p3chxVe)RA*-X8U4&i%das6y+`C);TI9 z`}4zO^ZHDiz2+?+f8LEYMJAk}#TaU?&+a!(&&*nswBvUhm+`&%2x9onB6ijI&z~U+ zf4k6@+bL|BqIS@j_;aFU#t7schCjaYOkTqRDc?Qy)h^TT7W}*l?@ZtPvShJ9jN*bi zZ5&SWg)jg5&2F_aYD=a8J6;GtAdv}~XEr4609`Q3s{q!)X&&}R)GcJWApc)wBxo3N zgB?gDP>X%&^%oFK+wb+)L3~KvLE2!2B@IkIVv3-SznS&@*@B<0FCyE8{vO_)dxYB^ z0}Jws2wzuTok3y@hG7tMF`68$g1D;k5DTv*(JclYLJZID^2^H1ra9(R;CraE(H6o! zX}jw#5~QG-`GYtwW9iep2nj|rH<#zyaNs>3u>R%FDpnoNNPqiSZ2n$PA57&{V2C3b zb&PBoMd4|$RV}Y^l~(ssY=3^shPyL1O4?pfILjv8yGeK=tf^wM;?k$uRdZY1Je+(8 zvS`;=SF}o)to+U?X@`w3eK-~EZj8L#ytQCvUT%Z^tvQC09fitb8{JR zN@Od&s|sl_P!zvlf#sP~aS^>6rdX0BC#C@ErU=YYjQSrOPDC7jSDM!ndmVm~?O0jc zKP*mI4rFvQ##kO1UEnSQPt<{pcccVY>?$zWrZOg-pE#*H_eF6Ctmh-5*BBaG+gDw6 zMWc%Hyi#ahmV{mJdVnEv#oNtup5}3g2s??2h_svgeNqse%QJS(bIoK#b$|;9Te=)= zpb)DH#Kf7i=IQ5R5mpu0Z_lhN0>q7LvrBsmgkt1mh==k!P(yg1uCXl)zu1?Lf({@P zvZ`_ice6-s@yUbP=y*Lyh{$)!&ZFwk&B0j|4n+;IkG>1oMO?ZcFz0d6nmu9ItSxg# zqI_9hAdzw)!!!AJHNizP6^WjHx~eHE(1J{?K8Jh(VrcQ-Nhv!Jxr`h5BFvMIPrP5v z@*If$XUzB$gCm6mwD&>s7kGT@$SfCpeDQvOyD6ns|&^ST5FEvB6WO zXpK-`Y!b8h?X%!VNOl^h21z>S@-K3&qFQ+oC&j=ESWt;uhLxQ0WW3~G`YXQN-23@C zlHHG3D={+QXeR0Kz@)b5&aZG#Yl%@ApK)+9!q3~pEbC&=Va$p`;|AG#d|3dTJDPmi5@?BkAwV(@XZxcF13^XNZ5 z^&Ag+>rKg^)Jmg0nR>@x9LlI-T#&XCoJe8G$xBF@gQl+5vY*Ttu?hmdm;RB(`5>5L zVDJiYw_&oob}x1`%CU8f3?Gyw_y~p=rEL%3?|9-33v!lw{*HwtSde(f1v9Qsv2qKG zc*{0ELi#8Oyybj;TK+|*fOr)sLwC8;5)VNfU#^a|?~ZaOoL*{Zc~L3R+Dsq>D3GP@^BV&Nin4X})T#^Z8h`q0EYEam1F=tny%7%KL9ouRLlqRahS-&VX($WiIN3kkmTCYxLGP8Y!b zwsiO=IkX5+8Kw&=d88PU!h#;bjtHRlqcgkRitbRh$Fw6uvQ|RJ39fpnmikqDE8_h1 zhRJu$I9oSIqj>*$bN>q44_?Tq8|BF_j%kS)4Ac<$R(Pc&!(S9jsh2amQqj zvYv>sNwI|h*|}7B1`CEUZgAFrzdGkXSajHCt}~Z_2)fy;1`kHdg*?oj-RMk5sDIQx z{t9aNQX48iHZ5XPflbFSG~@WnIiR{>7-noO2)_7vp2m|E)f?t;l29XY`IP#Rw9EXh zv*ILO+MKln%|<$i7{* zWfu8%?U2jS$4chmD8Zdv>wfZ2PlFaOYZJ{mEYLXzKI-h-56TX3zWKFMtfWuluOxFNe*gQY*8ujs3K;l$d_-OmH6tL58~tFSwJw$??Y+QteLClmNcw5cDTi}>nb(`IQ3V?Wva7u`C4z%CwZ1OK zVXp-i>i8P5@ zH{(qaYi_>$8pkb~hY~)<pJol*sBtp=mzd)hS*jg_5Z2y3x2!fIR zw&!T4=idWZL+*jG8FB1$AA0?0-NVr(*CrbCC$|C|PH%Yl&-BU7!VRrR4^45Xxp6px zxO-ub11X`%ma<@vMSYCX{Gj7IPucfg0$cJa&q!mA5=QjIHD_gIDY9NJx*ps>h-f z`5qQvL@@Vd%i~R+Lp3sDZ9Ukfs~+_Elr1u9Je>O)Ei#rqzXg|!0$p;nZBxyX-Fx?9 z5Z@|T-cq}sW;dV5R46bGK&%fUp#l3hJft_^Srrr#!r=9Hwx@kV@yNBTzs8UlZe$(N z&)!@YE}=1%a|zeb;mrrA2_`jGNrh*#G_~ zVd7VBv8FLnp3o|qztbw4SUCU@uC26O)Y9^Oe5j!+LaxFb``4@o2!4~C53f`A-dJmk z00b{%vY7PGJf1w{ytNb&oB(9?qUOj?&>h|zQr0Cy-DX9hqrw0 zfQ}}$q4~g`j8X#u(q71}KmfB8SL}m`2v%%jsKnK@s!9HBeOZP7W?+OsD|Xyqhap|dTjw4ANftrr%x7h7KWG9QffQM7Bi&Ua*I zZytp})DA2xE|bphma`PIuuL-K4oYiCcte;B7!{0}NOdMm`Qx_BoQQnm$n6Br8M~P7 zha7La9f}_LpUp7r(mhYJzomwUOHcIB9>D0tCjD?PJgBJJk&riXISHz(p8*SygQM*` z3XAq`9-RsYFRlVRF(GZ!n0PUXafbSczFFo#`=MJiD|uojJY0&j7i67v@#mi+SR&>B zCRh%A4F6bU{Z@5c>yz3;I<8A4yW05NgU-E8&dW(+HV$S3m6bG3?wEyL(56E@d=y+) zyUl8dS^Fg%XI~pl2|;vp&}yyGGd%wdbHa}_o&OVaVi9tnD+Ux+OON!Ie9PGuU3!X1 zDqD|Yeo=S*AX7Y^`K% z0<}AgOR^tM%5m?@{!^!g!EHDxMiGsxrR6ED%%o^go~r83xO-#^Dso;&a+q4;wR<~8w$l)D^Y;36;0n3sVLU7VJmcnw z#ae6ab7W6`K$&_B5YfQ-H2E~sxy{Xog#@M)HFU59^9(k1Cy z7xhhtRbO}vx-TVS`L1RE$+E(BD8ALD?Ud+yGe|gg9RzFlCn+5Sdtn)LkUn+#^!YLC zjujcqCR$kefSX$EW*!^HfD!)6zZg-DD6|+4G=1Nj-mkkLE@+(wx~#9x%-$>q+iSFC zFjAXib1I%rvVJ01d@GhMo#Ll{t0{v7>!_T{cu;y$bFuJ-e?(P3Tvy@f)+DR7ua8Y~ zyo6o61fjNMqmqtbf2X9RRCpl0ay^_YT=V}M=gN2K&(dm^x365T{;p|BTfoUTg{lW~ zCLnFsiWIPyKA?A_>eQs){~S6wW;UW9R(rXYD{;;xwA-1!I>|G3m1bU**#OxR33{~l z`-%Z|5HGlev~8XJVABWDW5kuxmU=8b?3=pSERZ+lIWw8F^9!V(T7*yK2n%l|v0;C0 z1_tPcCL1(A2+KymTU%7@tuo4bTIN^sXw(lkm`k5w6+8d!ovCq^ElH@hetmua`LpC2 z=@%z9c-D-gk%sd%ZawA1be_o>?IrupvW@p{;+G!gNq49~Cwg()AEKgOB;9VsoyYO` z2iX6hwyFy$eN*u|wqZ06-A|H{^(@We1xlT~C!NhYQkM?CK!jv$@aH%I zH)ZQOcKrB3i?7JA+bycCaU%4Ut^h_#Rz$Rfhm*BR7w63ZLZL7*QG7A?nCF2OXHWkv z7er_54(-@^I{tv~OQ-{yZX4BcZDlsOhn1JZ$}Q{cOb4j@th?ha2=pT2mK=&nceHXp>H` zUToB;u6;mdx;;aErSY=_|F5#5vq`mmfAo+Bc?VA&@rwUtRCN7dAf{{FV%rsi9jAcr z+BB!gcyF*iMVsUDPP10((AQV$@N~wJ(gF=#Z&cJ&-o&Ai7XT95}vY(057OV;U$~xdc zbnb%-nes4)ggm+%(VUu|uK8L~Sy_0|!i8@gc$dx#_>2HG&ESp}LM6OQr8&r&$zkFO@)>)emEwa*aAtT<9(S_$cX`A$i+*HKS! z1vLxyJQWP(qHDUvDeN7O`jB=1jgFvJ3kRjSYeF{Bfg0<-e*BXV!KEf!_UN=rCvu3= zHBIZQFMw*XiW84#p3+o`hgX4X12(e>W%1(gxq7zWc-w5-(j=QmgDoyLSba=EJJE>l zB)3i~t0OMHLUMb-Gz?g%oi;5=-uC$xad{YwOu0Ok>1<~mA}$Z_-&`KF?NTP1ZY0OK z5)Hy#fpysx3uY7HV{}9ZnP5Pa*@t8*vkX`Lq&p%l1;|ZN%12Z%gUwtb*4n8WLM1Mg z_be<}ce}OsjNA!Xvz6ow{LFR^p3tbT@RDC_?KGTNJdJS1*Y3_4 zuc8Q)yu-a8esAeSu;ZeZ{F%b-hW`jP>x&e2lyiCyjGc@woSvD-C{rkO9=5Fiwv2^< zT?R=1F&b;)iZ2s&o)7~Rb`|-mhVPQO0n#@I%>MttlK_GZooElm=!QNrr;ezj=@Xsw zCZzcIjXv3U4e*vmee*I1c1Lk?IaSv{C4LU>)o^erW|BVwxX-@uX4Ff`0kS8Q4~|^f zW5*)0qyLR+`Tb^NkVGjV${C6e=j8DFh4D2t>A%R zhuRvix6w2T)NfAubWJOBGfHn64^iQRfxevfiz~>qULtTINbPjxbx)lUwZE+=oI(pD z5LFQ`Ayf(thRW8oGx3329~&ZkI6(>NuWsUu(4SlbNq6ga8GyP++xx^yZ8H3n1Xs?E z@4f|mZ{abvzj=z82PSL3V71~7Ttn!eHv37PVBuE+Rx9@+yreEk(@&mP)+}W9j5-n^ z-SIFZ!yoy>gp)A7*Z862oEqHDM2qIOO6>7-gIhm8%-@v8vaO?e-;rnqks2awi3MQo z&z!q?tN-8RM*AI^dTYG9kN{lEd(KoJeKrWvUx1xoglJa*-zC&)vI?e+!f&0hwPo zoUef)h=-_{g)|f%l4CRIkVBvdjQ3)Z|BMDhLd47O{fX02vqL-b7s3B%O$lrBj$`9@ zdf58$(I=JDT2-Ftr{H9{+oF<8JGe2t!Q_R-;TjInh>u`O7HM3%j^R27H%8gUV)X)d z_v>D&=`3CJ-x)|KUGL6jHRq$Ktwt7>)K(rb#uQ}u%Bz7=Y)1|wLs{_kwUV!I*iD|V zeU!AB(gjNFu+-LIf=a&Y%N`+ZjfDWMSWfZyG460(x>a$zR zA=c}1=FIH>*+3H0uG*3&`W%iK||r5@2X z2(WX;6J@lRWYjb6%2XPCus&D)y@U14<*DdtRZS0D))SQO8)ZVOW72PKBF5;ahlGi* zm;`E`h2^u|*F_5dobi@F{hT9=F<8O)`MY=$2pI$MXj2E+i7l00N^{;3=Lnv6Fd>bw zlML5Z<<4Qj0|B>)?+xtE*}fx~=E*Pm7FxUiH3`Bo{6CT)dF%dfB#6A34SHzuDUkrR z=KIsH>@3gMU95M-)$uU^zJ-`HSNYG6a!1WEo|zlzUrF)@8q+EDGK${(t+U}1q4ttw zDO*M3q3?t-IS8}}QiwClDjp*$nK)Sp=KnpjRjgpf^Iwz+jjdIiVjn3#_>(Vrctd+? zdET0tPvLeNHg-Uq+Uztcp+#^mdIefMn-}AIz|j5B&)aA5zp#*qMm$ki?!0&^ovT+x zf^Rgn1Dz&igk;cy<#*`KW79O%Rs0p)H#TVf8q$;Qf7-m?S4c4&nYAGOyvQAH!(Lpb zbuTt1wK`U#&~|~>|KAo8#BhlSt1#FeaA!C2ihCNW$Lw`m!|q{)5LhYw`~x(%k1KS~ zvSqx84_58;;g@wa7ydr$riDsIUghDcwvmH3MF^C8q`sl7C#E z-{_9{0=v%5YnkHs{^mdHRFfKxc3FLu6maMIfsoYFd5jN}Sha{I)7?X1X%@`cS0aU| zbYqQTe%PU+_OaN(PQ=23qZjE|l;|jr!lJu&y2y${2%P07|Jmjeh-tMQ5ez zBbVF;Pj%Vs?>;$_>FG}mM_?wutb3f~VrC;Ko8?&)q1=DCMjoxu5E0F;UX_m0Q6pxM zWSRYt8Usx>lXiBIMOa6PrO21gm_ObWp+?jO_}gBeUx^II z!`r9I_CK_@XwY-&p-cNPQ4BjtH@21!~>(5EM1sgHu2>CZYomm zTPz*v`+3D#kZsSWawp^TxeGBPLHF<96`l&#u9}@uZ|yZUj(FmLrWln@_xZcU=UQqc zGo630Yw>c&aXuV>$7QMScUQf=)M@Nbf#W9V0s$V6%l?TlHF&;BJX#77U?17S%2J(v znhWv-&`BT4$`E;R6?*=#oolq?S$h{`QFVo4+C^yp%z?b{nizY2N+MRJ`+gyajob2( z=P>L@yztMWgyMxS-PWdge8cqtr-|GXbRvYgIvmuy%0_f*r?N=@Cj#Wc$vxx{z0PT! ze&Sv}zreb`d644b>@@5{b897XkQ$1z^)k4J8z7S+B3>SkzG@tnn7HUa5Fy(+*ND!KG7^eqjITF`+*6u=O;!==O zEV)?^^WJ)AFaP5L*=;2pLEn7lS3Zg*wspibTCb?F2wW@nHr#}(qsJuq7hUo_&LJJ? zf}!vwY(}&hjUVJZnTmyb7FT@7l?^)`AA16ayNn;Jl$M*^2g-MnqauF);C9gga;|Tf zN}VE)!}%iU{vmW(c=^np)6X8gSuohsAZ$dmcpxh9;}DgK*%L4uJ;s1K!-bN=c^u6} zmKeHeZN6B)q49vkHax*2bny1D$h%CBTZC;ytoJ$Vv%fe2w~$24?rvJj3oQ~?>zCWY zZv-$*2Frh)PDsIVyaiaZ%^8~jK&t=vc1S7T8`Ra`H)~(|Y1jLo5lMMO`N&d;qrV_1 zb4S5*wo5gTz(SmkQFH*|@k5%AwxT&=)+r7fn`xNWz_Wd09q6nhj>f7y%|}a5fq8Q4 zAA%Fj%=5fFZd<_n?z#*j1j7bRUMkOzWc=ijTON&5-Rv9S;)1afwzq3ZK`*nMoVUCtbO;L*kUnV(P>fKSi zv9qE)bY0Pg=nnOTEF>i1zPw>D6H^Kb8#??p(z&0Ut|6Op6NhwNw10oz0oL|{5*|UT0ed~l?=v#yp9e*Jl80{0Xjs1Xb!r?Q?db(Xpv*$TOm( zQHrRLYE#NQlq{4XX)>!cXwob+PYR)7MHver%_C_pNpoqSk_t)lph=+#4U1YDe%E`2 zz4tlyxzBWee~<4U-^ZhUpS|~Kr}Y`%@7Hx*&r3`=ZR!@{5h9lD!QL(KX7~KUewDr4 zoja`lFzdr2c;AnnI58Io8XT^2}fAS#+IOc`3KJsSfzyQ9Mu?0{4iS^T=K!mk+c8CwAa z^YIi$bf&yUTXS1w7|%b+k*HKu#2Rxe!TWB|xH4&C?nM{JpW-cQK;vzl_8J>U{xPin z<&wQa1)TNhGyNAyB4^KW#%eL5R7j;09co-i(_JPhehAb-?dG8++o$J>Y1##bdIU9KU{|~gZI+wfiKrN=1Pfg3 zh0vRi^?2%839v`+VpQK4at1Q8ML zJ_MrVAn)M~2Vw6k=3d+}b24MC&-=f##mf%(5cUq^D7+tm0dnBEpeCtX>acc-)`j=z zFIv~yie>X1YB2V|)409es7Z|O-?)vYD?=<^G=-btW#^unlXbnKi{>)}x972BAz%Ne zI1(Q5ycbOmgBdlR0gg9szFYdz=50#Po6|+Y(|Pb|*NwxesCDkuO~4ep{dSN{*Lm`B zIq-3XRr(v1s}tq0noSq9_T2_-^X z0>ibgtcTBL>J-!Om5p8qB90yR=Un+|g=NUS;rY9LRQCj+0WVtPlr-Ps6W?Hb;(5R8 zG@GA$qfT>+0SP`Q_yBX!Q#ttN4FYdzt)X{X;Ei7C>iSW#bz||pL78F&@9dECfy$=? z(rT8LfmlqfEKcg54b0Ybva(e|7zb$*TKOt;t8k-3b7z{RNX%$+o{OYx%&yh;J?U9r zT)sF(ZBtJ_Rqq4eTQ&VbS>l!=i6w8!j$Z<(VO}aQ+VBf7%v_XJbO~{~f|p>CFx$Wk z>C>B8U{C6wUn4X{D}#XaTKV&nAtk`GEJ&$J1E=HmNH~r`m`=*0$32On&Z}X^FR@IM z3ziLC-+nOIvH>8pb$-uJUx{ZRf|%2p&^661)p)ULP<5=9ef4-$+;=P$OWYSS6ddlN zUx)Mbu{sN5Gt$Lz{&(YY#^%BipU2EXmGayqTouNts)m94l3x&WJG)&*9=x9IkQ}KV}JJ*a_SzG(q5i}L#mQ98bwxhO!dZZEUD4`U(W9?9a zK`S&6)F{p(L!sX(lBRdP>aHBSr9M49C>`6a+8-FGEOk|!v`gzHwy7&o!eZFcobh|+ zH3Wo)R^jxhx2852yO=C-;@tp(fLuBO5QTj^r}Mpa_h#_1GPU$cjlHmz@-Wz zO(GoN*iHnXlf3;G89U4DkZY5M=K#M>c`9NK#Pk_La@ZZ3Q@!N(8( z(s$`{^WvdA7=Ccy=zI_ht7xJJ5%aI^1=J}1W@1%2J0=o2ioH^)PB?lH3{+^(F!|pOZjj;W4ir*AXCrfVRERLsm7L1Qx00j8!(YonSnVLGAUtAmP`WxX&CBxY!91rRBW`jOS`{FPmfcN$o z99*|$GAq%pV;|=7ZK2A*7fdMN`Nwb%i@s`6tv4!-E8}DmF1j50c`TFTwbbA8>ON4% zoo{TQdb=_s4B{2IM29Dm5C*{UyF0jNr^uSo2Y5`tzM>g9gssv(p+_UbRmubSORLm0 zr6G_~F6X0MY@M&I07U&l45f`c#R?cis|PSzC$XdrAL+^vw&~Bt6A$5Ntm0P%FlL9A zl7}yEA3b%HqVD#Xgmm(kB>u`8Jk%?aUmRVnbNc==%{+WSIC0AtXjxvGUHEB*cWIoL z0!GjvRf(4OtP-}W!<0PUTfyR2N#n`U`G`VK`}igA$3wwa8y;q&$^m;mD|XGM#iX<5 zIp^;|JvhBi* z5B3+QzU$eqP8xsIOxFFiW-`C)$W(2l;+wC(m|SFdkFVH*U;UR;Qp6v7(aiYsZ>PRx z?}dy%Ia1p5IIkq*4PBK+N>vUYKDoVypTV*y#vCuP>O%6FYa{Q9zydUtm1 zY{_DSC$edlJ)Y8=rFgbZd|n~ta-LPNGe%_?M*w31+uqr3%xs+gg4^G59y#sc05I5$ z^=w{_Cwg5Cnr3D6&)+d}+k+o~A`+Cq`fl(H%=PdNGcj5QA`d3={xps!p%9{DDs+d4 z&+EtyNA|i2U=SLIwmn7@Ak2K%}R`Z)}I6NItza81u6JFB|`cLaqwK4a_$WA}y4 zmbFYwq^1kQ#Wo}m3WIF(t|6sgzXERrrm;c4{33XbxYV~t;89D$9Vm=wCAcXjCZZZ( zP!`yUK^5e}nd%@K)@U6=0SPp2XN^VE2@dJ!EIVWVsQABwK4rkn30YXDk%@6qdVhuq z*@U?YlAa8DSgGwMO_C?tpUvnRx!}LJ? z3Wym%aTg!l`i@1t=b-+jm3>J-5T8ApDQ^o{Ry|gywF%npc&?Lhh{!TFF30eZFh2*k zx9gv}3tjlcwLsGS%AiDLm`^_NME|6?g#eZsiA~^lZA)P6g-<_v(t1T^(MEA&KKH<~ zuaRAh=QOXRk9@-xX~^iHxQw8K=eR_#s7YU_nsq zW=;tHk_Ffy3s^tNmcK@%oR(t@K-H6y*Frt7nt!6H-jwmB2J=a&kh@yW+)s9R`2}6q z^K6n4cS-qzgiPiFO_R3yWvh9%65^?!m_g6V{jM1i2Ki6x-#>TNoL@F|XI~Jc8v@ZAW!Gs7#~AU{4&$X^702L2J#EMv zGx`L1#jY;^b-K>RHQHr6UR96eN9Ev29;|rD1{4n_SGkYe=bKqk&ePi%$v*X|*$v)C zjO0AxQSZ?(1uf#CJiSaSZ&?z-PSyN?M9@{ZipiNIC5(Su=l`3`fJi|lbMa(MKTDea z;Phf4*;FyO*d~;I_M2qwN<`#5B+Fj==10EEMi2G=h1{uocvP#+x>C5!g)UC;Mklr< z>{E!L?6-{Y ztA(B_qCw1f`0%0dC~#ESt^ebs1V+%qO>qD-Q{Q*trt%l@WAv0592xezg|&Hk>rvmP z4Rac1VK~!p+u-D@&4VqD^^tX?MyQw|UK-}zKsgmw#3&a|CI)^n?@}fvgiL|Sx(u-e zE%HkM_=lZ6=H>GmN7;e@STL|?KO=o_hd>_IIF8kEZWeVtg3t)5eP^2+-3aT4Xa8H1@Xwk7rH)!MXfn1ALpabcUZTb%1bdKZl zV}PP^iweHMj~l=aakWLo(=WCM^;B zS1fa1f&Pu- zc8pFKy8)~yepWOfJ3$N&>ksV#@eH$l_;ex@9{GT&nH6c9Ri;&Ln~O!xLAD;#O$+fy z;NmImzj{yQMRne}+iG@nfkU4EOWi=pZfFcv)?+1qvZqc3{!iFb3{|JCZ3*Hen*tPH zalVb8NFb+3chT^b%9@$Q4;B8AYT&+nX1D6NeogcrvKRo_Z@!tG{jo|}#wQ=Zqf$~g zq7(ZT3X{Ny;h&vmj9q&_Jo#hzcfAwX@+5sSeOWhRWekVj+L2iN<>Z`V8QYObE~ zRK;XfPh_+`M9^^1!!YAVv+-_xlEs3lgxsbUyC7Rcmm8F}&UIgZHii7y9$|Y0v>Rq9 z=NKqipL>+y!8fj*tT(D&@#VyzjMn@(eXlS_M?n1n84wVcuAX%w7S>(~#rCM&Ys;9dv0QzM|3uY# z)$#pp&xK-;`690qIYw6qf8g(~5Z^z$LKNdUw3v^!Jy2Pr-mrrySn`ZgzIt(Ub1-!l z>3z-nv7V_7I(kinOGIevp@!sRSDcewCq_TUv(l%bRPMe-fj0ruc@{07fSxo;ReJE@2FfOmn%t_=+ zQmKk4lR!JS2>jrTdH-mBrCF-*)hpo$pDkN-<6*t$Tl)s|q2A#Nj+-PlSJ!=A7qGkic8*>~RGf)#KS(;d$ZPBxcr${u(2z#n7v? zKRbIg6#q&@2uA%(M3`>$3kvUVgm-i#hh(Bq;|_Q{5e{HjfasSu`pR~?&out{F_kXO z+2Gy91l!$zt=F_PeP5`=$>%;Y>oS6MrAE3C-Jg};xRQN=>xtO_Y`R95>B~cOzKuGddM&S%K2ijj(?CtS=;5Vr{`T)K?)IH=_{2pWBFvr zPW&XZhO4k#=gBOSSL2s0>PXb0s4?+%@}La%Nv=tIpcHWRcPMhL#>WbridMwvJ|n~w zsYc)ME=+^ku1sc`uO{*q8IoLc{SSx;ZzU1qc5vbbls zqjkqwuTi6Xgk~=Gd`!ReXO{=HEow?8Dzxoq$V3=AC{6G-pRAA?%!u>$N%gDQn@bkC zf3QgXF6nssVQ4fkL_|16x#PbsBAl-7_&bh@tlX7|9|cJjupZRMm$o_gaOpeFO-bmO zeMB=Q*}(3Oa-4pAQ)}M$+%KiAZf4>Z-LBZ>0QLK)aKM!w!TZtw6AsmUJ0*_P?Wsrc z3_pM4OEol?)sAn*4|1iAdA(IqX<8X4G}_Ij6wbI)B>vTbi{Rm)xE3_U3vijJbR|Hi zol6oqjOYr~6u;uf0(wyyHUHx1GhYA);(Hj0M#B$;N&858y!fzY$Btm{hyR zdH*Vai>B*dbFO7aBqHObDRZU4Fe=Rq`gFd_ERCkO;_)4euD6QVd|QYFEoX*5sj4k% zuyW`|ZX_;_k78dP-jiN-koAw#LcT z7$;KI^0hwczdE*#H-3r29|1++X4r#TRwQ6V;{ru{{IfGsReEfm5ZOH*O*|B({css|3O_ zd@3MP47b28g=P0RFSq7IcvVf!B^U*m(3y7YJ&&Q>3(+frePb2tMMX|&&uv_J_K{_r zz)x6F2yop0%g9CdR@G=57RTHI_h1;Ya%;&E2Eg0Wr*bxJ_=wN?8tifl+6%8(d(D0n zue|NW>UDj4I;-T9@UVQ?^0rGoxN#v*KH~;rBAsVslvX=j-3fmC?R|ok6m5I>xkjCS zb_E|CZ~(!hWmw|4y6@br;^JgNia2s$(D&m|WqIHE+j0DNl#Hh1n;FuY@! z0;B#mr*G>fx(p{~pzHU}pwI+=1~?)F&Q1U)U?>&G4I^LY1;Q}+j)(Y8CR~B@_ngUv zUuS2i!8EL0Xrf){YuMyy*jajav>j$yIOaCAj6IHyb zve-1hkMX)ZSNzaG6=^%8X&5!!Jn*5*&oKRE{H=3{`ztaZ$3);KGYFHl+=lqKpCSeWpGt(2}tTfh~b@C77!(-Lo?!nf64x*mBG4BX}!NJMdlOg}) z!D^h&oHJjZV=s9Lt_B`tG03z5HPKWN*_Wn(eNcg->WDWS7oJnd#Dwt?>N{Kul3d3v zu`=vgcK%MrkIy;EukRZ}oil$L?P$kLJP&+p=qbBi+H9}9rrS&^0igupAvmBsCqAo@5WyE_7rK_>;3G|w{F-gW+s9Dfe{?B>!6n9 z#5qqXEFPy^dPZ~nzP(Hg8SVOFLziLHb<7>pmnBn2(iJ|%^gm7BL*vI@Ui~f;-A}i8 zU?795B4X!%KW^dt$K3kOsWwB`$`;PXtpBXWB4JI+8e}m50gJMXxxIDdFhz-sBZt`c zxH4^!keF4tA+Cp-wcSKsdff4X1M|wT1v#vUW^rc1*A{3n-EEx^kGvzsRd}GL8*~3m zT_je7ceTAS7@oET)8p@rp{ji^xm?dwO)^TFP}tEu!O3NBuyEnRX-rShEAP7{P3KpS z0lmM=V=9f;Hcf21-`n#6O;o_yo_qX65jH+>tVF~PW%nBaav}jMl7aitSjWVKU{SPE z$Q?U`7W_8JjCU&=vuO+afQ4b#Q@xhRTA;2jM!tb4fZ})~yxQ7&*PiJOAp5p1y}Ghp z(_G;!A!w*%hkl*rQ&k5^wTI1gxA|y@mlm;C8#&o5R4De-0W5m%89OLzFml$1fv?lQ0ku++^Q8b*e5>e^RG< zj{UpB4y7_yeQDvl!@n~a;*&vt&0y5$WwU$ggvpgZy_fsf8V`)v{k~mCW^F9wEl%nj z!LpCLU9SrqI2F93-p ziQk<%$3#6@XaebRQ;hr3|FC&}0l*5#;x9KH|C04l&|83s*l}eek9`JjyQqkO$REML z2{&Gq6cUxw3b>VbEcNrKsHlj*s)1C=V|GQRfccMhm6FH8^|H5vcn$$p(Mt(XM1Ssd zpXV|HoI%F{0rAAZ59u%1Lixg+2d}MdVgHM^#l|G*(03~z-)8Bd%NaAO9^mi4VKi>0 zQik|8)YN(CI}I#XPjBx&y%94Y%oh}2o8h+zI_`$7@0#v|pnh0c&1tZz?R3RQjvBOX zc)W*i##P$F2YT2Jegg|JuFn|XL3ENwP|Vh{-OqP2uZVGo>Zom9I}VtW>^jEO<)sg> z5>q%Yy5z~Oo!!yU>-&~)_^U#2CZ5=S-gz786v4MMFqU+EHDirq1+>7*o_KJ?U5(CWpTkd@VRNt&!CEYK50=0 z!2S|Ji06D5ID7BXJc~+rXV8$kGc?+tCqKSX+U%N{Mq!uFib`KXE}|u!#DA0ga-C2s8RVnus6iyUt9GN z)17tu2PjINwgY#pW@xzf6t?{Ml21v>G0qygCT~#FT6|Qsg@1}#jUw7{x?k@iMIby(}#1-LQPGa(j}O-LfI-~*98d2TbJ$|@0>PP zJA2@4^Qmy^DsMS}&anM$zKL14Vt){y1P{(t8i6GJ7kWO*#QFd-12|5guexHq8r_H% z$pYjNrM2Z>7|$3yVk-&5ZSRrFuGg!Dx%QBzk21N>$kjf@rmjc&mg?6#%9N4reGW~d zkKDR+XMvfrzDnK)ET^$g`()xIJrmU8r7e>M>@9MZvYto2S?^%v!`F0p(YkE&$Nudp zL1D{Zn>{X$o!GTKoPm~R(-Oo!{?R}t}OFC1^^0jTX-&crhhd9qdOdw^^c6O>+ zO@W7u%O4$%O+`{4BjN+O*$c<+tCZWu{&9IJ+iYyWAyF!_^{oCED=XTfz7b(2zm=7# zB6h17>b3~_7C?2mW9BnV7&s$r=eZ0muSRr8|6ISYy?5%^>px~ z+K~l(oFqnf_&LD6tpGwJn^U|BEjl#1eFY`$@N4J$n|3t;351_h#k4b#ZIiIU;DKUg znYgqNE)n%SACejQ_koP+cxB0d)5D&X*u_v@YSO|cINACG_#}sV$Yuaq*FM^rIMAsm z&i`^cl@Fz*hdGp(pm$uUS50C_!N_;BH`k}pWrm*~U^^dUSz8*CN z2nq-IvUcUP6zl;@gj@w;UfPp61zl8QaUYsa`qmV`EaKSOeB@lxP48iK6sT#B&&PA3 zORlm|u_Pc>40qRq>Zx=RH!RTKgKRCPCIv2Ddcw4lT{30)PYhMHhNPNi_m3ExqM`TTK9&ZH~DzO_T4eyvI0 zd&eE~TgAQ@zTYCnLlNUvmqI)w@=$U!iH9^J9>U|HKStubA9*cCuQlYsY40D^Ft@1i zAhLmqRi}bAPR-V+cr;75BE0u@{p&MbWjwkvvn5@FZa7PGRab0NHGLRjW~{urR@a6~ z6^t_f6`YE$I;^i>OhQuO(rr<`<|?#z)=ufSsUOIK>XlGho?2J>d<6vsoEC4>d9(6? z9zvU03C`d9g)WLGn`@hvWP(geQ+OC&Hu^MC|JlPYik4fR8P*1c)5%)r>E${8Fpab! zerMJIeWxZK!QUJ{F`B^Q=)%Rh4@~Rs^01P1i5Lf+S-eoM&2 zd}un}DVDrVv*JSGvv_@C`>Uvi(arb|)ZG&H_3%bP0Y2!(x@C!LPyirtpZwNkCPyze zDxZZKTIcThrjKX3#e6Yn6pIT}?UhXX{h^-iO(W{g+Ut^2>JC00vso8C?it4CC4(H? z90>R0yB1-Hmm>#pt_(>eHs(Q-Ef)sOjOiN2+K z=?Z_aR4T*L8gmgH?7(_%-n=qn07LPp5Ro2+yaR$RBO_vw*7%5IM9?TH?CScSzx7ek zy$P8;#}aH-hxf#;Pf@BDxG#VgKi?I%#yr_%Pay(oI0aAShE1F98>Y*O%8W1F&vmp` z&G~k@jf485P{+8YvOHR%Ergxj%Zz&K91@}#2M9b6Hl0tr4ij7WJAxOPSw}6l=`-I~ zZ;xfFkG#>5BTO;*{+~X+`9#y@detKt35Nbcobw+t!qsCqUMM{RZ$NPT5ObV9@SKa; zq~4(|4^_9Gd&)$Xb>-?GWV*_+nA(h+$`M` z^=A6Q8M%hFyHXfiSYPBW>#E^hz(?i~d~qj2dT1p-^lyrD%hv-`7!lZVMLWHdv)E zRpK!EP@xKrLt5)M)O1c9E;r3lQQ@~#rx1%(~qSy z$EHL#HJ9&Ip{nE8by`nLWQ>P}a%E8WmNM>Dx0x2y*P&8PDoLlD;vBROAC3vxaqjKW zeFIM-qs0&(F7=z4SQZkudL=o8;_LOZSM=&=X=#ZDz0w)3Hr5D&A2wDQOW;Ur`BmN= zkF5oVK1R31$%!gZ#-rAp(I`2-sX;|(?1jX*mx&`iS;3$7ZY|hZ2PeBW*Za6za78=D zmz^Exwb~-`Ufor*-`r1GQtI6Zgd9&I(;#7 zD9&)q7EkwtrDL`*en->}{UXq(qs(2|M$+bo zU6|ZLV{E6*k6#r0NTI@70`4IR_l}bF3mCUc^&s9V@Of0*Cd;nPH$nOpDD9Nkn==n6 z%TPsU?o#$JEZiq`2z0s!4ND?}H2Nr`I?hGV{$q7@JX%J}ScNiL6FaVrcQv%H+IrrD3g(B3~PAx1`*V&6w@{Vpelc zcinoU+B1^7CEu;{qMB)NI&>czZ`?2^SlXr0j{m;5RQpcl`Vr4JRS)&lPz_JwAepoj z+Ot@)mPkHkX11nTG=NGbyX*Z@`dU%$jbphDZ9LYdvc~4-vxx90;O|LKzYO^U)0h&a zfcqa)8SUmiK0G*lRFhu<}vd+}cM*WR7?j@*vdj7N9jHoK19 z3No{IDGSS0FHdS3efH9@enGFHmwK#b{9$cvl>c6jA3H}5VwRUQeP)bbU0E4BXh2YD zaTk|6U*agPh@rhxp2UHdv2ckl8!aw<7!xg;zCYGK$~<1AXA}Kf3WnBjJp9PVbRJW; zO=Wzu`oljRcq)Dnz!mYkqnJg@QW)SUrD%SDqbe&e>|gPCX?#%dk$jwq zhG47MUuE4d(-bg(VhbiTZ%6UMvmF>&%EGd|vfla(W5&PwchNWZB6=MsDF()@o{Zt_ zWtSg2*|IpP$mq!}#Z`;*HML{ax4hb-gd%by|5v5p5Oe*i0co%7d(VP1mZkUFVS_sx zufOHIsqdO)`%6jKxkpAXA1NPoVYnzs;4IzUcuJBmD*UCS{L~zxIDeC@e*PN{!N24+ z$iJKMTOQf)ax)wkg>Yl8t0OYmvD2q(?9uj#sae}!YR6xZxgA)Xq;?!B9C(KQLG8G? zJQNOm3H!Vywc`)9Q9I6l$&Fg-3t__$saD7zq>j;j|BC6=GOH)h6MYn&DL(Rh_-U6A$N4D4r!@N1aui-R|EO5Q^tr=|Cit~> z2xg2Zr0Epa+t1Wh zLj6bHN3nrUB6a_s&@G+ zucG$!r(GAy!PQKJjHvNr2@oktn(*&OR zx|h?#xgO0z9`gu7MI0flFUNfG@mP`tyU%t{uA#09@2ogD+K^_dE|HIC_B18~>Zq}9 zSmQb?9;w)~hT(D;Tje^*Ex@=fRwVJ z$T0u5-AD022j<6-Q%co&CF|yG4&-02#SsC)tS}^pz{KRTix<@F6Ulq!^xxNF( zuqMn|jOq+s-9id=V764Cx!b27I%&Q9H^L^oNYXJr-ZTA~;-1CD=#u52bWbnbfSLjG zF*H?q^DY{>=BA7BsyVa z$CoY%NxyEW!{{GJ+x=Ozk#t=TtU~{IzQX_mJtVZIi!G6}-g*~j;PFk+?ySn|$ueT^ zQ{_?Bz6Oiu+ju<*V}!{{lS8#EBv1yG7i;=iu2T1kUU}4oTs~sR=jpqZw*;lVh#knz zkC~7V7eC(37i`rV-Mn^U)={bXn9bwh4(_wAfCu-HhNj8S>pjfTN%VaQSqZGxL0txd zAr*T&q9+lrQ=LV7GbtbG2De0m!^2})fK{c)%$`U1q!KRrAPA?Wx zpTIrCM6>1)rjA|TS>ILXk>t{EIrts!w$-j`<$w9mwd~RMW_#Y)-+!r^;q`iWI2HV; ztO}Rc^H--Y)rhb09d|pg^I3gzm^pHixw0Z=Gy;Thi4Weh(pn69Rr=>UxDeIff5}Yi zf;lv$+G_;1Kwrt|GwHO=(T*USkBQm}weSaQOfU|z^b9I-v20hD`iMM>X9I99D9tdH z9$Gm116Ac|MXL82f8wL?X6h>Uvl|^WMu-6TC{Y<3)o)0Zz9pTLj8h!0tktuM&H zaGW}{FE&GtsI6kAuMd}19BhCxF(7gAC~l-5dppuvJbpCO(*SM3^q8YN^3dAy&RdJA zW5r4p=mZ*1XMHlFt&-V4N#4;jPZuOC?3l6Fbyh>3OU{_6Nft-ptq=?%ZZGae2yDd{ z>q}~mV3DDR9ECiOm!nVY88|?|R2*xv8?6-cE1$*yj-wS9uTy;CZy+y+OY_(DO$1L^!70kCH&dm|l)pFfr)RI+W{W_gd#%ty;P2^uI z@UYhPTJ~Q3Gj+`SkXOvO5RVMwqRV=;aSk)&(0xdF6)VH(gK3IV$ado&j|29vI012+ z;He!P8qq|4@*{8wpQfzvhX-r1$ZKzJ_k}*e6%35{))!oJ<{V~aspF1z?DbPzwY)#v z*D(Gu0wH74S7zdK&!b6M#n3d{YLafyztx9^hF%^f8$CR?%ncXIOgzSq*Oj_*<%%w6 z;EXQQSK-61SdJ-Kp^c$%LuYv@-Z)0rsw7|Gj5Um0V>=V=!=xmxK=L4{3M{`F15-Qc zr-mYFUDCC$0tOvYePC_P0mK0s-it6&!xQLHdirGu`z490a&KR*?**rbVoyTJW17rQx73wz2B}hojoh&lU4eujm}_d z+@|DT+9_8SD`(}hi^-7`%M7}bi0wAJ>F``2;ERXcS?mT*-)2P|M0Fi6irH5fUrItK zrd-se*C*eER?7wSpnW(MKOEN*HV_ahuI*meA0#BfQ+D#Il30$_ZWk_oi(H;e8i7fb zm3a`D)ag^FNQYYT9@c3i1k=DlewzmDdmm` zg?KiPjBH~dRD=6x=M^ZAY6$bdWkYJ#X|#5*y0NP>D`#8Gli?WGj0KM`4-2k_5j4`) zopP`h8PT5mP2H~1mo2R8^X~XD2{|8K+ci{^eJS>4&mCXHe&b;ViAMfm)FIPonX#|C zMs=sUHOw{tdb_xLeY-Bq$Gg|%5?c?mBqoRs4!k&RNY8`M;QhT@M7G+Gc51t5S9KdL z0p5ATXRZi*zL@rRMXb<}Mp2VVY>+(a5(wJ;K-_tSYZ5Z{xdULi-7n(p!HF zbU5_D`UaQ>;>;J|oODTC5RTr3dU$gazZ-F1jM}4H*q@A~1sb5^643!gTSO5#0}(3L zNngg=PO-GK+;C9sTOtX34ixiU;1$W3cPlK-?d>FdP}`E*nN1K~qP~k#StBm6J|Qnb zwuyCDK!BM~Xc?Yv7h%{0PYww^0Z^?R&c4~cUdGX6)51B`8LO~xf1%xmd&jv)EsXut z-qu+Zhgs7}G6bnhCnQ6{2GLCW9R3GPu0<&yUJ0J+I}s7>VIFADY7ch^f*6>Gp`O{H z=B4I9N_gKj-loB}l;-{u-ZGLg_C{?r6@A#@D<$|fES0$GpIIPplUenj#4zriEXv5 zlNZiqULAQT_UXrI#F-au+!e0z%%|i=JcG73&XJ*hV9g&F zf{&o=GN*s>(TzJMM&8T1_`V0h{jII}rz0gLCC2;Knyj+P>EF`ibh`OL^dO|NUcX1F zsVlfVG%Kc6Ryo9*qE;SN8`7gIueK-)X0DW{;O+~lj@jiQ5O zHy82Ph}q=+$s4s&qAq$*X}ckc^7^IX`bO0CkgBv7sfjT`F1 zJmrkWjaPD+2KVZVxk#wl24O6d<%W~$PI#v(2B?lb7!8oMD$lX+^00Q4^C1iUtQ*fu zD}ou9V@5OD{}ii=aABi6=C)FFa(1={R{iC&E(Hf^Gf-7tSvjproZszi0M5{5^T3<& zGn;*#R3;8QRlqzaP{zV(uTPWwI#JPkNlD5Gh>XP{9*RdamfbfI5P4;%YwONPCsDFc zQ^uCzFe@idEU{sxD`o}_7FI9LHy~9XiasOsKQifI=QfeRC%5 zYWy5{hdderMoiZxYY*}vu9M?~89J-@yD3JpjD=V*7Gmq*w?Hwc&3mBi4@zoPn}!t` z4E=KfVG1%8N7~9ie=R+{ZdLxa1=ZhX{FIKO#inqdzRj z-66`)rfrEI)-%_3e^bLm?6IptEs^o3{;C0BhA13Cwu9l{z zUdIjY!lqClJ+O|i()syk#grd`A5vqEwi*Z&_>7G0Px>hGNjkp6p*me zyR;G2%3U-R4;Q>G|2TALebB2~_{kD8GBUOxTmbDLhWr5N28%k^IFcf?%uAOT1ODoA z@q+;^gM;?&2WifF*{2Tl05p|mskOx69t+uMNyQl34|bMbJ{#@y&et&LM*hrPbysZz z15U+?*?jA(VNHBs_^sAu&)xhthCT!DO&3UwV;1|(3f7D7CMgYfqIOq!THV^y~PYWRnbaFTP-DFx$fqonap?NtxB>d*R0cN>p%3ThYsxiiKz_AKvXU`_|!_wu}0XHswpP z1M}ge_A$r^O8Z2;@U|di8Q$4<2Z!EMre`?rjZg)#qsLElTlWE)7*=5PwfRf>8n7Ef z7=#IHnBC<*dkuc?jxP$ys14|Fx^3tTF1!C*P$-Qqd=TTPoPjsHxU@S$G977gU1;T? zEJev!Ar>C%(8}Rg&L|WYkfG|Wr|^y(_=3X9r)V}OIoYvyt>Yo6FUPtB+no!pqkgVW z$vmk`W=zO{#&W*S&~-6F5`q)oY$wx~=EiF!b>NTm4+^R(*7HMk#Qhy91z!4V$WXT1 zdAM@l2s$d9#=cBWP(qyt50D;kx+w3OuGM3aAR*$1<=%yUaTa`)0py(|sIkgzn#zS6 zz-qV&1NR^BZ4WwPh)XKR+aZvzL}|xFsityVSZroZmGbBeql$=qUL9n`5LY8Sfie$O zFPnm2)6U3{ukTHT4a()_UIv$M>_k?{_CEL>}K?zH{V8h@(564E7WD?hi=q5CVOd7=_ErPBnQRd^AUttUv3e)P^t?`N)KC9Z9@6CZ3zNJYDB4C*An0sNSpGn2T;SRY}>lMyHy zq2iyKPK<_4HY|(iSm{dbO)Rj^Uu&lM8Daw1j9VbSE}G!dP{M)AcbJuu*Mur_EbK|2 z$D0V3ssw?)9#Q9!(>Q7%5s>~-tETjWWmtwFfs2h}J*V%zOQA{=%Z5`$>aGYnzDRC~ z!{bIq<%bMJgb=_k{;V<&BfzZ;aa{&*3nND0mh0!&SrOpYgR1V!UJAs2Hvd{eQ_m{) zJ_6hd^1gTP-ibd0Ba$fELSQ5@qvb6EBizIxjyp{BUc(kC{9%_X+ka^?ie{e-C7W}d zwxWZAK|-3wku&u9>`Kn}myU1h9!V>ck9=(Y`q9xTEeDzPf%8A37TkjLg-XKzsnAl7 zc9Z`nwEPQ%7KPExLAj~KY!7%N-FUvi@B$3gFrx7c(}>s5%d(G*4Letyq#r|@#*%~o z4?~k_6X)(vn;1=-&6DKF{)9ezvn$LY81sG4pLA(Pdke>?$O3l%urRF$y32>{9w&x< zTc?<>Ie@|4_?(MlPxnd6`5coD7>UiO+eGb~Z{kunuRk^Dn^*F(mdWPy-k=$j@jtNN zVLzv^%9xa(5?kOif`0uTgBu=2R_dUgVYUY@XDx-9)1F$z4R7s^v9E=LMzU?y+GlIH zX=1#nBpkRbuHLuk+dooT26U(fhV>wf2TJeGE{+dHb^(gr%}qpe$)tX=x-68V(#t&4 z;eywN8S>*p$M}R7NX1U|nT$=MVCLr9t_N7#mW#Ta{ zA0UL$@niP{buyMFwbgd;Cn20FYG!MT^|&Nl9hbo^+EX^I0IU5x)Ui*ev5f)0Hb$?@ ze~!n~RU%tJZYhYC88;`9jYH3s9b;tXLLVZb(9HJB{7g(DA|m7=NqQaIe6$Rn3Qdme z^PICsnP~*uDGw4{iaE2FbrVGV&7 zn8mI|)BmCXoSAx7gN=9$=|pt58iIh&CBHm5b~~b4M#kYD z9^E~Z>ak0rY<|>qklP42YWo`z#Py-Wg^ z(DO9==_co=&NEhTl~XMp0gBHDi~$FN_h1o1kCVqF33RKUK(`>FLFrow=oW55Ld7@u z?k$+=GoKt2$|f&enegf{g<%7p zGQGK0K0XU|_b9_+3ag7j`3S?+a!qrb_5F1}dP=1VeIt1{sc?-asSA@$w>6`f+;eCS z3!-v+#}nL~rERY{kA|YkSkdN;`Eb)p)tWe^F?$EUwG@wt4gi zpm8Zhs$U~MV)XHz`8Dnyw(z{g%jN|4?M&ptV)W9XNuBoN_}nx-F4ub>C}tAvA~8+- zOdTC{9t!_x{wQsFjl1bqT#ZFJRZiBrV#~86gj%3Q!O#~HUbZnzF^2X{=ZXdPqLM?U zmm0}ET|ck!{KB4)1a8-fp)i^P(|Pc9UbH%->?Pe>zrr7!f;(nbcZHDfhvVytigLb` zcR6Rud)+FWKZp%}hndadTEtE~X~}lR2TUl8@!t=2m18>Vn0ir%nwW`UGn7jGlrgIZ z}+I|CE8@)W|tfh$<+kWAYS7t9g{`O7=f?^T*`K|Z8Z)S$>Uja>}bxD}wD$(yh zUI|6Eu*!kbOtg-*GFy#NPk!a&+_^-pYTf^&R@W{}H(wKKLach+(p;ca0|Xuydga%5 zV<(W*T6>;PCStyc%<~N|)0AVFOWQ^}FMiJQpgsM(pztVKG+$$6TN2U`noxtL-J3|FjD>;#;f#6QIa?^lidAQn@fJl#2c3N;&qm3_92!ybRe0BB;jM12E`TgNkqA0U>n)@ zaAvcfC;V)`ovRrI1qF99;BbYmk|8j*$EEE!h=X#}BS#80Ey)bsR->Db&tcA%Hm2hv zg~jMMi3dEmTCT#GSq%$SPu*S@Co5%TdoPhk6nwoS|$h9HsWOQOExFn)%uiOU3 zRzLe(f)?9v4GVq}x3HZloF9EA5d4h6c0=*AN+Q#O&3r?%hkqak=(kuE-OKn*lL6ji zoCT~v$cVbJaS^5;#R*^uL~;Qc@uqHs+k*i8TCjr*Iw5lFGs?@+<{BBf5^UV2 z*S_1W*;0<>($o*E^0x1KT2R15D$b8@3b=eyZmg|j{arf#r>F42-{hFfbhKRPZ(*lQ zz1us&TlcRx`lHQit3OMB*XJ|&KSBf#-q&{GO*bSCPU_%3^VMT7&Kbk-i=aO%j1C!k zb?;}u_T@)$L3Kk}jHZ0|`)T(Hv=gW;_!9m9HqKgWtij@%x-OQX~I zJDy7T$d_uHojzTAMTok_HD1?a*iTOQ6?qW_HcqMzc+1RfoHY%iWf419HJbwlt$ls# zAgO_b4OGRdFI8ISV<^2)Bg$f*_G+J$i^#h;*|}QgHggMhgc}ZWL+>bi_f5S=H?c@Q z5?eo#+47C|LrupkjF+Jt9x=+&dk%LahBE8KpsfPvg(8shlhZYnO8QUk7Yl4Kj_7eZ zjOFcg3NV=0NlE#Vz%wBs*Co8bdJlL#43fQd`RIKY#4tC36(&?0wlBVYxrqB z>hR0soBoKgJqQ=)?UVxCFeBB`(mZ8~SNU>Sph|k^!6zYAuUo7<+E8gF>CbJ{Z5|jE zoE01pAQ!9dX7mS2ankjzxmVcq?Tp0>5zvwwn94xB(Ph;Pl$tsZ3*<(~d03f&i8X|q zQ3S}gVmRBG?(4}%Dce?)Q9X3BIaMmrtR~d@fuU3PWXwpZ5cciM`BwE5B;@rkr1|(9 zeWRAkN8_`p);bU67m|B<_9N~8wVLwkXqy=jWpvLo)uB*d}u%rl{0d3cp!|BYKj9x#rq_b(BhQu)23|sl?qB$6P zSl4rYV3V?zaRhX-%+F$D%#zpBwJACcmvn@NpU{&%!Ku8>mDzjlvob^>)-lF${!c^| zTi5y5cJD%mIiU+IyKS~zr9Dcj%4M@+{x@v3Do;BE$yneGKAZpa5eMv6W&{;>U-F9k zxN}+o$e60J>R4I<;z!ViSy>X`j|if3xJ$0Z=&c2#z`Vu+1?F3!B!h0DiQ#CO($?uUeARhoVj~KNFIHmJbFx3>ekhL!7d8jMrgYp_Puwao%97lJ6R z*EyV=#O-nVG!LW;zYFy9K|Q;UI^;tji89~9GO(!%jdNAvi?!zcf8@OhG?n}R{;g6f zQzMnoaEfF)4T?fi8k8|)+-V?XN~JOX&|X=a}tqR$QIi8U!P5# zbH2axJ-_d8{?B@z^{lnmIn}bZz3;v6`~H01*LA(FhgieCOUi-yk9cL~?#oPg3h5nb zuO{7X`AKLwAcsa;y6vozVAF<2QnG9pZ;kkqf;&8arQvXal2FrCt8_1 zGkY=l$u%tGE*|sSC$hqn8l*j?y8PRc6Nmgt_N*q$RC8&t#=M!m9SXDJwX;(v77HY3 zf_16OwPU;|mgH(KIJ2?UAaFaavQmicO%Ywo1-J3*&3r3|{k0XA6zy+XFPu%&7de(` zze*76_p|qZ%>GrXuygnzmhpa>BYSt=7>ix(**cL)jWOOT(#rNT}KAI`;E@iwT8a))epL^;G&PMG76w|vF?*CKT=`qruyvQ zuS11lq3IH8osDc{l^cZmG>{8g{lwIYORq!BpCbN6Ylgn+{1e^NQ)Lc7o9MDW#Cz;~ zXwCLI%Wnx*=aol!Fqx#F z*!0~$Zs!POkp0RH0uvjKoVG6utD{X^c8_o?Ishr2EYs23v5UkiG-k)UVXb2EbpYrW z^*q{Rzuf*6BXcnHfL~z}i=ac#MFEQ^7Nf%S3NDml>vcm6xi0Zt27Q3)k{IBn+10$+RheSZE{3q%Rqj|4>Ru|fdkNtB%&$uXSPPWn=lwt7~U z8naJDfVM{ZT;E~E?6Mro;!^R;Yl=-$I8Uf7<&c#yc&LUa_Zoe#|Lxa?4UMGB+$`gr zCbwm0W3-%FS9s-_o56bZetv#@j6L5JH_%JPAKv!}*;2lCx}(|Gn|XBKT8K6Y8Ah+k z`|>_57m~y;b1Ft5N{i4T5<-T|O7jPf8@{#}i*6}MU>f>_VCZ9AFOQ$Mh|zf@tCfXu zC=FF3X7|TS&H4CJnv^eTH>;F;&4F9x$Ra;+!&|2laEnJjt(aZ=gSfr8f77DlwqeAt@_-IkoNQow zg2<=Bq?GD=S2ag;)1nRKx`@4f>oakckr2DUvhyX3SE``{hkmwo+_SC2=Hz%gOAWZ_ z(W7R{VQOzAs^{qB$^l?`ARubR2Y0xjlMRN@dwbnb;{QFn2!+{b+EstxL+?JUlE6t zfstsB*9T0DoCyULdc|N?-uF$Ik-`#+-OQA+K8Z0W%1FBoip}M-+nFd&8Nz*HptsvX z(M~7Hs1UyruDfWDOmR2NssP%;c#5R%%7$7aJbNHld>gDE8=-6-YVtbU5_G0tUsSC~ zwHc11!A?_o52PxHxIl~G*R_&`dk&6XxzWKxkTZ%qFcu#JO3Q`Z+6JQ?hoh(c;luPE z{(v{tk(6gtgt1XJ09|L>W24RUQg_BW${1n95pJqRwmA#PxQhV4&?eZ6mr_qW-OR#t zbODQfZGMW*Ow6}FyjeM9QJUX>914B!+DR)MX|}Kv@Cs#x2M@qdW8^q;+T#^@tyS$J zhY9?#m_-*eDTKVSupnmb7acLRCvhhp8~rkN-SK;dW6IN5wf?(3q@#WCPTmmTdjw~F z*W_6@Oh!J@Rn~rhrRL->^W^P;()Tv2Z7y3nj~wHXUcy0)QVHG)zh>nT2<4EHQKo3l zR{UnGs*vMsyBePkMQ+)q{Z?AR8M&&5i#gJLfG zNZ_Mz;}%SRx?aB_Us47Or-n~+kc_~duOt1D`{eJWcl`5Vk{41OBYh-zJi89}0QW{m zG-mW4b?tphij0xhP(GTz7Zz++<5Pc;Qz_`q-elxnaCWKx9vN!F1C3bofVKCklJLw@ zLYT$!SDqR z=4}91hrYPx>DiI7o593w!Ib|5$axQ(-E;9#kx@fu=RM%IXaHq3X4Q{~%fS>4))x!R zxlc$h1p~R2=kMO#5}z#mNEW4)Haub>r5pCLPpU3tC%zx*^b|(SxLl%ahi`;VqJdd} zKskl@k|%_c0_E~DNe&0ZcAjG>UU|T8YuE)WRt@j2I%r$7s%r_O?D+Ss7mukXQ7g=) z>aSx{wMSLeO1h59}07aKlF)E4;R{183X?o9O*= z9Mc13*puwSsn7Dk9{zB{^-3K(h#*_RLTg?W(CW(4HC~aCcS)Bz4{M&@PtW;J&x12G zyrJP{qXacHH(8T@N|Q^4TL}X$*s7LtMo+pCI{xMkLf<`iUahO6LlO5VLoWSS6ydHp zX```cdsVN5%n%+Qtc&Az0>lJBBy50LThs>DV(2$)?T-u{N9a}P0!cK=sk!0$_{SvQExFh; zAI#p<4qxxov#!emZm+-Z+o2!jak~`Na}> zSAm3lzHCQ%*Lbgr>pM% zzSV_%fHUl21%@d@%KIA*@jqxZQro!nUr<2%O&h(~8ZuAb*^`mEN9b8Ls1lSXPJaSi5)Dc}l$P`Q^P|4UjC00P=IY zzHwCe>w~5kvorqF$w+oVvuc zt?=A5v`9~jplXu=1%e z%~nrfxn?9dF4}abqD8S7is7)O?ewyrATI(;q( z=J(9qttUgi2d+>tUyTr|SV!h#Pe+X!2qv8~1gRBQf96(9-H$81vC%KY_u-ac%;>J( zwa4JM5!bptba-n*My1R?;>%q4j4QfjGH!}OTlpjvbG%MBmO756P-n1Me|$V?sMfNg z)rHsSMjrj^JYg`=o;b5ze3V@_DK4Hv2h$<+oipt5FLh8FrNV6@c4A)))IG2q zqUU%NS|f}C9bzGdte(E+r-m}iQ*fi4P)1VBrGCq&IAG)VjfE~IpmO<1(5CK(1vZA> zH!GhA!faRLR{~!VzO_(S*j3hkZ(|k9&|Ghx(4gjFavoMuPXEIenWW(Vw$mv43u<}R3z z^8}%i1kW`(Iy!CW@EWtsdMoTDrKY1rhd>#XhcZ{8!hAC-%sc;Obwr;#Fqj<+3*vnI zzZ5~?RVwKIb8$rK5d&#!^Z`5t#>t5Xn&zd0cwS zW4`L})$}X-*GDYxcYqIvF1$u%be9E1iamkR2}cqp-W$(!2R6&3mQQWJ!eV~my*4g% zU2`HwUAjUrl_w{ZF$ib`bc>y{N5~@Aix`~;%A775II{NU&W{M0lP1s3&Q#f7&LitG z9QkHfblHLh3zC}>FdSlP7>??K8y5-)Sl$rCKXXh^6jGsAfU&1k1Cc=+;?&&1^IWqx zRv%g3ZeOJaA5T1mkKT13lN8Q2dVl}N{Q)OgzVTX;HN>07qxCo$H?XHXHo@|{9InYkFh0wBdE+ouq3%<360V%35bPhk%K z$WNoZ7T&Gg5Vf$lo;+_-(f@TmPX*Oe_l6G$izLQVCK@9Otu{Xw0Rt*r)(iRebAyzo-3dICjR>e@47_|~0 z0e?hd`cO{+H&{m=mq)EtBmEsUYW=SLA}k+a>AasiGXaaSniac8*lXoSnz-d^4C4&; zFQCsCh~e(Ud=%~vC}qptSXaW4315J&3C?E(zoCEmlD=}TX6bFw z{)SRUlD!f)zxOzMhh?H-AhOSaiBT_+Be}pv+CV@ue61# z6j^<85bOvHN3w^XFzSk%lL3)#vjnGV`gYDW^jrh89*`iE~SvHbhYnhUx^L($Jh^qWZDs=|( z`ajA>GVE5)nIWy%-{hqVI)L8m$+(OZ^LpcgTPYE3)O(CKj~GTXjaU2Cx3|YRNb_({ zYTV;l!jw3!D+EQa{gbT71C}m@HfC1j+jv$)v3VjZl6%&Ut}4-0Z1UJM#C`sX@?+&? zQkUu8wKGSzTKMTTM2)&Mggkm}`E~pz5W)2dcbKpPCpYHwsM8IES;&GBTYg-8AbEng zgBZ9QCtzzY1q*!MeTB zG6+)&UlE*rgRm-b&;-3{Hgi(auzi)>o3iw>TN#e|?WnemtM~LW4^csUh`YrsvJW-m z^}$%RlN=+bB^+aSk(lk)_|L#oQZqBgw_JQ{4oG2dBour${7B1877V+5^16&((N3NU zR91xLkWSO4R42^drwgX<9FYiAxlv&t+VL1fM$n;85B@_|tIzB^?1uc)&air$By;x?+O)$;yO zwqC=d1*xdNm`6%+DD!7Tdb&r9?L%zG_RG?6HNKMpGJpqUklg-Ai8eo=GjMZcXTjKmyZYMy3;G$+2*kxj9k|q-@~df%sxMZ$}^9~ zT;_iq>xxKv?&t|zEymr(HNIavOLn{^EpN|= z*g-NLMp0%({ZKvl%;Q=}s6xY(J`51lgw{*yr z)NMoTBd4Y7EG-$^xsCd19XAKZj>+m78CeR;#|3BEcHJ5q!+zR#-4B?QO!`dJG{lJ2 zdLxrd>uO9tA2!WUFk|5Eu)n~z;0M*_iUujkAIJttxJ%>CwGj8Q`nb_H8_N;RQa641 zh$iNF`hN>LRjfS{WtbT+v(oFO>+r`NuiG5gm6drK=HL6GqL5lT)Ju)I4n=!82vq9NcV=q;LiA#$z0Gk-W2>*NaXGA zzcnOc>>F}LUlz^4Kx@#8c>??I4tiYImu-j;P^X{j^T%pVGg@VRRRJgFGFV%g_xu{2 z+WF>4EcS=@qmdN(9h)kj{>pNJjmnXS)r_f6ZQa{JOkfVABGNHx%S(N5T-@*Y;4uRz zEiD_WjVq^Rt#aEMwuD=^GOTr1nC=IZ`d!rwSsvCz3S7ZuOSeN8+wz9ZiOoPqfUCYiJ@ju-?132jJNgza;9ZbBGd3ku-w zYeuz7CpaYRRkZ?f1Xj~uWuv5F)P>u1JF)~M=HEI^Dvpues(a6|Ts0Cia{L^zS=mjy z$Sb>DR%G5x9((P0t$7~X$(%3JFuMmeiIeDkdb+v@j~4)rw5QBqgS}c$S?;A>v#3gS z(`LJpt--9?6~{(ur;pto|0XjU59`p%NCP?YVCC~qFFgMpDglZS9*j`%Heh>N*z9r1 z7P`QJfRza8E-G?3crd@5O;irCX`+#E;>5}|R#RAU57@A_4R6EW$Cr!jj+}E)%JhPI zGpiB(E*#|b(*t~*4Rf@5+beN}!~I1OgOL|VNg&Vt{K=ZsHwZbY%_uHqHDd0FJO2ou zlKB_lQ*(;iS7szrK(jDcNvHj2_WoS6g2&_Kl-KnrL6HqG-V9>J8MNV*&FM_vv<6ek zt8=sSp(GFiU0q_jt-n~f-qztZo29AlPT=4(x#99)TN(w;bGx|i4<`^b?hFBo2?mxCu*OpVag7- zlYgc%@OsEY)9oKhHLgzkiMbm8k*kw`Lai2j4p@fJB#x->@jRfEkX1dF$t>Kk?a^5? zp=`Kjeg>n5^aN8EvS0%vxp89*1(+`51v5g7e{1Jhhuj$Hk@MNzrjB1!DGF@`2C}@v z%9{l$C6?aY35EyTc=l#ed&pcztb>2WN^y5&xW`W5Uf`6y36qRO)ygo(K2K(umZuVWp}SYpsjD0`5-TpT%xn9;Uv@qO&Y zNewAMYQJS7@^<}ICW3F5+GxJyDRBLGGSbU5_TwBFR! zB~Yd-eBQEHQnNb=VdQA>q-j zY06bLONOH#A0Kk*s$7yjSH@^q{^x*yWaYm!;7`$-_6h-0S7W*A|9>VTDpDAPzeXfp zOtX1(XOfl}h?A_eO$->~jVh#P8v))$qIVjfJ1Ao^%#y(XS(FT&zzOpSA_ZGJ^~-6k zYjfvi4X!$~duU>ukAOQo$L7hO{^$JO;boZRCkW=OEIlZknVE;$!bSaN+HN^p(c$;w za}~PW%$rVeu2PvUOu}>@mpfV(l_u8TEgEWOlOLQ!l3o};Ni`i=dta;nE{VELfYc2C zsLSq|yRE)5&anRI#K-)%2O_L!WoE^Lkw+?wC?&sQy8@Oh<@Jq?36^TT$H#jxhc0SR zp%OxDLF9hpoe~j^inGz>B1Y;RfpzJoLb%p&6eQC*qZ#Apv}RFK`JR*5Dx;fY(os8& z=1`w{K-ePyq=RESn3-RE>&2(17ZqLoV@BlFFrXn+ z=M&GAN2-OL2AE&>OLnb}=<=5ec`W(z7KawOY_r-tnKxgX^cx8 z?ag7>e~alvJ9M86DT`1%0(f5Uq!w<+!6|C>T(epCWvLYhAy_b2Q<=NcQ6Ah1--jma zyyn*KC*cR%71P1ss(48WsE%ByP^>;O_i10PIkk>s_ZVAvi2BzZT^nL;UJ1=TmXlKv zMm7hp9o85FJh1ZUpAY(ZKK@;UezoE+*z8Pd*Jlr$>)DHMDO=O6>ct&BbS(=c4+~+U zbhzMb+1?E=?e!bH0cG_`IVh1k7~Xjqfe6r*T>!kQB6H_g%{D9-P_DfkEi*u9;o-k0 z9Gx^VrO(#eZ<#SuDC-_C%CC#gf3K}q!ng95IX6uusuaZJ@4r(D*`+l*etkfLw(8NK z5x_hMm@r@^7VLx*rW*2H<#z4%GjC#QN`*UZ!`{_@+XKYOK`X+inIU4u8AHK+*6 zrVFICnKes5JrZkqE?*hJa2hy1t9Hf&@rayf<`AYq{(4(md%RZ@q2s!`YuE`?=bB?! z1tDN22pI&a>}G`DqM0`jrj13o0{CWwJ_L%)!{(0??GpJA0yz7~KTmo>e;WH?7}l_z z2zpe;b)pwTqI7AA^@Cz1+PCDe3jK>Zc~b4QhwQ<}eAj_CADC(*&!1)t7GrK@L0=3Z zLHa0po|E~DLD*6~8^LpqLr#YbZG$EW_zj!bo+@Av9}G&gm6k+?`n z5l(+h`w-yF2^|UtCT;gNlMA1#_VZ_Se9jebPx} zzAWHbyo9jwI_$DZsIu=!D)uB84#RtZpbdNyz?5M@SytE+^u=LEE`w+0)K$^b?$bFZ zweDJq<@A^qn#dZTk`yY|0NBtpHn<5^Y+r=Q0m2JUaq0D*pZl(^PAsE8L1z@hXnCw= ziZ2f1$?+>Ng0`{Og;_Y&irTayZZ(WM=+6u+%?I9iN)Q;v#Z(FDFe8gZY zuF=NEG@Y}da><9V>oPU{RbJlHT*?Dnj&mWoleeAMRgEr#IAc^Az`lsSf*~Y7;m zLqiv{0DSK>yVIjJ>eq;BizFl_Hdfs~^uQp8gQ|X1d!jZ2U5FU8B5-lU-#^M7;Rt}! z_6N_6v5_$fRt&_E_+PM7I{y?qm1&&vot#W=oPv@b0F)hH;j%TX!P1$AXewCB6TsZycz=6nve-fflMDFhK_RNm40*fJOge@p* z^}d72o95+L>ksoF77gt@mBUq=Z{1#=->Cn z#U`Bwe=)QTV5cNH@=X*t0=;)fP77hX06Uu|Y{-3m52!P+<+aM8^RRq;z3B@rVoHQo zPNWTVbm?Dv;cQ;d(LV)7rwN%LqjaflD)SO1*{f+a6>OIg!Qqd;Fgx4&GGw;!UdnqI z8&Lw%f@RfAvgN2SRwPlB1&C9h2$d@)T!*%db*GXcjXY?WyLK+R#P4+F$XLG9Jcnc| zU3tHr-YvH6@hulx&9cg&yuMf%j?$Gm$adNAoP}7B8wy@r7FC*lMJH>J*`UEm>r)(g zBJA}tbiZi$K9Mjy)P(o@78NuU${t08&tb{&(<&uINQmWm=N-abE`(ALY0QHS>Bl};C5i+=>%pUD0I-cUX_$I02aA3x=f6_ zkIPHj`ImW5ZIiU)4PqXSvxpuz`H1ksfDUz@n@^seKHnDEpKr)k?;7Qa$qgOYe&-3L z!VCHwZq58s=8yNBO2lOl6A)YpQ-YcY2S0f>TlIztEt0#~I=%n({z3~ok>Sq$Yh&!L zDCKCx?(d!-BOz?bl~12pC{87I(v$3I3|2td5Ltrn7fkkT=)Hk2GZ)65DRka5S$%b) zO&uX;q3rtl@5~!a^Qg7?rcaeQxTzixW%II=;XZ-4`MX1C-ERuJv@3;i{imHt-?)2TUdB!gAWyuNe*=}_5AV_}0D=3I0%p$Yea~JifC772ySj%I&X>bLi zVE((K%U9cuwz-VQB?=P^v%T$aG)G$+kCUi&ynm5U8RvHGu1_9J=cL9WV90ba?&TPWZ~h7Ya7h=hQA1koMNoi{fZP3{LU(;E_T+Tk_29bH`s zXuT{al@;U(0Bo8jeI*zSt2LHrwg8=KC4J3a%_wwayg#5RD@Z~HGVzsXx@zWMHWX&| z$8#U^58Qn2!O{GJs-LY17qzaSo?s3Q-|5~gX2Z$<(Ac2l+ecPlL( zR=&4f_%NjZV9_}a6)+~(-IHTcP^D$CxNC8$R7JL=!!2^vpbc5zKdH2k7cxZW7YRRD z=kXgsTKEy15a1`R6>q%`@(L~7)L9gyK^1{+kIdvW7A$Un0^H_P=083IwfGRDR^uMZ za5?4&B9%jezhioSEFr&hxJiwM_ogQQEA1A4 zk)fFW?ni)PUbpWa`K#!k@+;OI_=4>M#^+di($d$4K1fZy^y#^0M9d%REtGu!m3oW% z9fIe7`J(?j;UG1qo=d`R=9nl{23gGWN#Z1Mqj85s;cz937Q8~3U`VcA`{}m)dvb|M?A}G9_Hm$xCGND;D62u{{ce%8=4cOdOLX z!)5mZYPt{EHI#6hxlFK)zP{Xkk9I;iMN3G;e;66iv@6(N>o*1@Sc8UfN~W&oggfTj zNOGRmuiJKU+_wK9Qu{{*MWr|ljRMxA@Po9y<@n?dzv2`UCq9r-caumAfggWF6$pMQ zsrL8yrE6D9&Yj7g;yD*=D6&6!!XdE#K;rg^pZZy= zJ!Afe^$46lz_tPZkMBe+moI1+&=xLn{zI+BZJD3)$G8Y$ybk>W6Mtl?BF6L1Dz6;Z zC7=k*BURb9oyi8@F@E<%__}yK5i#yN=l$pkM;XTRaj+Y`nB~F*Ic^Pkf}MjL=oJ}% z(gjhgP&VLU=V72%wh*S)laS}ymR$(11)*5j^ex1!vea2O46_MHbBD_@)wsnzp=EPZlBuI^ zD$8zSEjD}vcGGMc0}lQ#j$_{m-{Y`soOb*}o93A_T_z*Re)?uq>H}1hp|SyQ1l5r5 z#An#oq$!t}eC;B&^8Nj%T5X(?M~EEJrNf}%92yZE&k*!ukNy`Kf}5;$d28W(xP%XT z4W<>-sQnRw#`U@9eeFqy`*i`?ILb5@_~fvL@9>+%CY{$d1JFv+$UG#u#JXKHyWM?w zysTSsk%+B?Fd8s$pnZ=Y4d=s*M}J&jxi5q(V6WE)42C!gjy}vN_mG>~&JIedW&2IJ z1It3IT*6zldynIYL&tiazQ-D~fMVqh+X4@HLe zgD6mX4UhSniQL$;RJ*qT?~!f`#I}gB9+`py-rSz4EL)&=0_L{*6WsVBM@TVy5f?Hz zINv{2n}(2166<2I3_?1>mv@W4KA#c#tyi(JV5vI@pwLX3V2>%#oS4=xLWpd+W!{Kw zXVv{FuoOl2zEhUiwNM_@+%07I213k7S3XM?7E-A|ucSO*Te{g97^kU@j8B4MYv##L z`iL#XDw0JG{jkswMM5A%4dRKfz^Tl~VhzRWkfr78WgB1>1^zEIu@+C_iJGrZh(K_f zO=r{;u4X1zLaXM=HN~ivt>u{Fxw&sT%SWBO;rRe8)xUU%xd+}#PV-n#{seh&5}mD@ zXb@BgP&VAEw@cV|0T9u&4`2)BP z6B$DI3l4H;eV*Wk_tsNLrw0gA=9d?59pII3R_W4-p>sbS^=vflxhcP)*|ZWPS4N++ zAcF<5gtK^$e0>l6t3>QHdT+_g1H1DK4u-@pwwG<~fC|Y&lmKPjGs3?dFHf{uXUQl` zbVT>T+qU-h@E~I;d3gzE8DU%OJd^{AG*x9?a3vdefWpRz>D3`5KCqv%sThMJDC5PX z$1tIC&^CQWwj5_WA#_89M{vp4o|XD;$^}B!u!$W+>;u3Ka2~E;yn|<{r5ZB{O)lyb z5d-9yRH7qC-nGFavRTdXqI=;%o@$aN#Lb78UHSP7F-JP!o>A~4=D~JFwm}i4=s8Sc z5YsShnd2W6l$Of5tjZ!Rv1SxP*?YD;Z`s9S_&4Gk(9T+8e?0StgC~} z1wIV>jX&{CLF6ywINo<&cR&dW;75#ouIq1mDw=b4CoifWs(aj3)SE%wVr%E5Oeho| zs;*ebVby{omM$?}WS{I8RJ{&m?H4VopG;q&I=0SM>Q3Q3kLDJU35DV{_dNT2x5a=R z9sExA^=+kmSoef?hlwW<&BAoH{I|?f-9~u*TQhwaAWzhsC@vgFIh}vTPWfI_+4Kcq zqt|mPuZA|~5AYt#)cihQo-}liTpi!$fjg(x(UrypDYo(qxe|^Bk!0SRX|L4291a@9 z1^gama%#0hR&~!`RAL+{$oW?i26{ua|e5sd+;YwAv-;#(3_()>O2)B?|NJ zp25Xii7OKE96hVT^O&U;N^;!Qh;{8pr|^-U7hLkA2cu0L9rLLAQ;Zl_ohFX08r!RY zrHeL>tRSf1zKq+=QXGo5lTj&Udv`Wp66kR`xrVbLRI{Eo3 zvv57GxA0(wk?~D$GL%glV3Q{ z_Sp=?UBZq8_I5==6#u;l(Y!@oSzLQ_+u9lt6}JL63qO;G^bVqMkU2HxGGwaw`2ES2 z*T7qSgec+L@kX1swN|9Sx7K{L5uF#q^LLL#3^?v%z~zFgQ$Ab{grI@zCXf7jjw#64 z0mbAE7vr*~CYtlcm| zX%_}oVqNi{o>R~!Zx-Lu(_>w%kP^yxHA8-66MQKAcMcMiV)fZJ)BM&T6MRSxgS_0` zMy4}~@-RaEdz6QuR+Ga3FbVD+j}UY?{RkV}n}UN%k`naGS2kYQJjcB%_f0J)*8%6w z#!I|;TwaIQtkQ`C>j`EVUO(F?7R!NozRgYPeZo2b^WjK?$#?vgeluD0J1s zP7_!Kc-ArJD7hC;qRJRWI=RanL(dcN&zv+l9c`m;&wb-7>hOK15Ft2+{ennP{!up%Yx79DKQ1!e8#!8v9z#IjR? zKBaeS$nMCd_64WYN|-n$t4oG~aO)e}fI6wC9fzWpM*I5jM#Z{^`J=mTCLm%^(j-w* zRHvG^HP(@o(uaWONycaF5xMzO=ok6R)-}8FJ*XCi@3QIq@XT5CBeFp%wIE3SC2PsG z@Gn_Q$<15N|B~uK(LVNuO=!}`NI#=ndF7KHEnyVy<+pGZL`O>+Stgt~_-C@g zIujxrM9ljqvcayne_b}XK1uF-b?M~OSu5tayDkw3#%0fa+p_!J68mvyk=r=!bTCS? z)*%>#(E_A2JkWD^q~epsf$Q*Sd%sY)%7zij6H@Vj~mQs$oK&+LjUQuClL z9Lsw*6PywhE^M!?wiuoB6yt?|r<&qav2%98-T$ncg6{ebyD%ii{*G+Ht1-n_yKPUM zdc3X#hiM7g35^4yK6SR3nEun!>I>l&Dc91AwdozqqU-6T=z3P|@+5=I##N3TVj1eA zjp14MYjbYLwzO{K&+l9zo5f|g)c&!4r zQD>uXD8p{~>=xRV<7>OBaF;nyHjuW=JQIg#H(5j}1%5e}&XbvF zM%fxC=x^-_{FcLe)85A@wW#`;*X)MzZ5gk3v*XKQ3d1m^v&eG#9n z(0$!E79wk3aZBlkD)3x8%pW_tY0Z^JPNR->t<~D4-zR#lEpFYQBxan%9%xqcjS>^S z#sC+(FMHYeR?h6J`q-)8_gG`AFm*PK5h!b)9%j@wu&?VuaE!Ev5pkYSJG%C`-s?!5Dur;4Q_8j8dsgxtuHB5P?Hny< zJ>P`ky;*-#^MtYN@EHUO9M=4|fr6`DotJcE)hWwFO=N%eSZBU_@}tWjrWsRRVpFbP z7Dgr^`|{tUESv}o98*3^^V0K-89MnhO+eeUVOEt0sXtWfFl z-wlE-><*Jy$ShgM!DVU7KAw&S2^F*IuwtjGB()B~AIv03FaI?&>77-$iv{Z=!;gx8 zs0=VU*RL3FldSC5UWuqXqR7{uPu*BOSG@lc+C5~uiq=CZEpK@gctv0StFxEqa2pnz z$eFKO_fJOt>)t%G8t=2-p)M`Si-t<<)9l?ttm3_VK*OJV$y~{AN z#{&>WKf;n$K?6?x*3 zXnnr_;DHnPC(j4=xIB_w!;zWLB;ieU(Hmoc(I?Ej@JQ!vte#I#E#z4>zigew>FtI} z^;Di~O86+v$8<6clt4CB8d?^{KTmcQ(yoM>wI<~DZEN}@51>_Nq5bNxvjcJg=hb;G z`H-RLN+dU-{R2dlN)aS*E+|5d?)SlW+#K&CC`LVXv$qQX|$(`13_dt?bnUY#|o>pGdP ze`5VPT;HlVR?~pl+*?4%*#ypR;W~QQ{B_nRpN&^}k)!)mDYDw9D60C|OKJ(^QE4=b zG)|frjj!Asa6T|pF98BiHJ3@x)9Tg%>4$6=Y^2dD^4>Oi zd}P1U{3oGkVq#DG$815wlPM;B}yo;ekKM+G!&9k0CT>7j&?K zAOPIvbWY8`H{Mb6{L}CX{1m+I2TFrwfymf0aj3ypg&32 z2CLKIggNqUq|5*iENg6n8sVuSEFoC%(A^eaj~&fbrHDbGfjuzY{|ZYf3km7g){pX# zPSyLVsdqhCu2^!*4Tc78N1M^!vaMTedkLg%u}{L`(zBZ_dx`RcL_yWCa*D3@CS+bD zWn~j6103bSB9LuJ=XU&`#Gu^w(*p%&{&CvRedEuj{mELL=P<_KAvhN? zqWr#|*3$=2Tb9&ToW&Nc@Il7r6c%GfkbhlyWZ+z;5(RUI+^ zSj2Ewbp93DsoujGKT8}M`B_2Sb{xDeYxI~rr?ExAD^kBFkt+WgG7-?9uEP+KV6i z4{!Om=l@lIHUB^U;xFd^dPe^+|G$8%;QuoJcf)5zU=6QcL1WT?cLz?~!D@i@PN|3H zb}1x8EF)b^HlJ;e9NEshT;YE71&y(NaB=eWZIqXOdbdT$adMKKMrdoMI+e%Q z0F2l!OQac(o-s8sVIl3+prHYE#l-D=M;l1;Q|X52pwRx+GDah!uZAbw3BPufViS$r zJd{6?jwvbU!f4nT=hJ<0nXTTrbBf74B+RE3VHh3|>+pFAqx?7`QQZmMi7i^;A;Je>65&4^1>Sh{Tu2 zt(L#&3GZvt;8MIXaB;C1{*~j9i$=Xdr_RF*y4dn7nlxdt2nkbn{q`rVQDp zG)&Yqa4YWOW|^pIFtYylY8pDEzCP6LUpJ9EsNeC$1-vir82PhO#i|R`+CzSqPT+Cc z?TKLP26ny%Q-$I>kcqaOXAhm*7}>>?2zV_XjB3E&Yqu$loGx6*R-2p;9pADF|Kcs7 z9oLdOFb*IRQ5q?}`Lmw^(D-TR>*icjY98AasbRNHt$ta2wBO6ai$|2+`!V|%tZvEz zSM*6oLNj)sGiN~cWGN{Np1_`4@`gK3s9Xy!MIzwFcMG8j6cNZu)jo-n;}%c0;|hCs zk1`_#B@Re*!hJ}ic}FhZhE-voQC;*@%UbvSq9*Qn4nu25v|!De#wsN>Mom&M75=^S zt}Ag~bB)?JIi#9SmS|HiQuvtm-dRLmVkjbcw%tmvUFYgwytpiI4Ll^*!O;Gv(=xKI z+W%>y!I>l)cG&$P(ZC>y28RPdKdZDc4KTZTQB@YK7?zJc4Fz}wmz5&$z#p*RncAN6 zrNro=Q~pcwj;ZTqGi*pua5nJR?%SF&6^-Zk1|u#+)H-IP1E-rx@TN1wYYx&+ih)~t zQGsQa{%tJuQ{6t}rUkDkkS1{IHv;#RE^(RqE@JE3lPSqYRG##}{Vzk4S~X4#-@YK6 zH&LNYQyFa25vKCkp6~IR@FGI{MvXN|=rL%L9QZ$zE~H|qF8GWt10`vkbM(QrAM>0O z?X=h^?yn>#hAPPI=@U33%9?`5R-0DE~Y5L5_FEEVvKK#Em^%Zb?_@uGEgJB$(pOoXX8vDxk|)}Q4|(S#kA zAe1b00`|@sO88<_yEd=w$suW0^=_l-%N&ijyv6=hZ@`U7%G?p5Ud@wu?_O2+&YohW zw5+Q;5KA%ARK(~y8i*hLAGMLtcK90WE*WOoz8u4-YDIU`ORqJFAP2yy*Ie%@H+&R- zPOwFLBtZytVMEP1Rx6I_c!?_7Bn`1|9ZhgWgYeC-@zI)k#F5vR;50afc16w;mRm6) z-w_(svfYvpaO_Dpb<9^MKccOG*iF}txUerj)bSk4JKG@s3_03Y{g@Pb8 z0~Mr=U-qEsM0He2XvLYOpVDL_qAHa{+2or)C<)8U4}G1R|L!Cv9BB@66#a@Koz(e5 z4svStaZgmUikAlVR2}jxmnxjO_VhEoK;30;G73=YO}Uu|Zk2*^pgtt3*ZYZC0A64O zyLaFbrgkF*%hEcv(tW)|{-||zoFDh29faV3ikq9Bd@z3S!48p$9N&e|?o}MV*w?27 zjU00=OHaFYL2pcra!o_Tr|Duo|~!*fs?v3gl~xf)fcae?bnd26y>gFrB}oHy*mmohW!@_ zo90Pl+ao##{vT9^wz8W$w^^oGJ+m8LKcCL=E`-q}WK%l|7ddJ(YH9*(zKzY^cK+MD z;Hfu*A6rPXYIR#qU*p)}E+~t&Fce2lTJeS;mE|}za$hlX;4HnVFxw=n_)T>WlM9c! zpwQei2J!~`nf(RA0XylcxdZ z`n5;1Y4S8d(TT7|TNOebCpxQs9STh-o{=53qcADhIOQfa-N*()K%esM7;Y9o++K89 z0g~W-;#h5BaLA`9m5gw$O%2hM-VRe^8cty5^ixuu&4jM7kHxpNqJw+X32^D<2pGbU zu{mP&{uNw3b-T67Qfj#}P{7TmTW+rfH!@xMy6M-cyF__XIDGx3TDqs+lz-wOksCwV z3Kej(!1IR%#sikp)*mk{-eT9C*VsTMZ<6UZpv zi`VJD@OU6Kk9Z@qZ-5|8M88$v52o(jz9$AT5!c+=EFT=c?j4ykOKsvqyr98FEvn;G z_-rNH8{Y!7?fYSV&*#l4y5I%N9$LRM|Ja#@GJGOLQ z{W9>+z)+KAEylr6%SL~Ip%#FnH4cWVb-TQzX95i6vbSoy1>?}K8<)K9Kk9P(30gyg zbLAFHGis--Caq1vK}}A3Jg@M{Zn$!-Cxz~fB6uap!Wi&iSc6BH8e!0it$opcq1IIX zyKbmaQsI=iMNgGGen6vyGm%{$*}eMxpzZ>?0?}}orgd}L#cFwUf%|Se8qXKxa1Vh- zkjoAn*z!3O2n=)cd&XO~?*9i$Wf)RZ zEQfs(G_1nzZG-GYTs#5-&tNhjH|A3^U3S}J^bwjtv6VA`zW^ppjxxjfhLu5EhSU+z zKqL}Ys-AMX_XX#ZY)12}eVJKHUoaAc6k7d(rx$eAOFCJ6JK?!2=WTbkAcRn0Qabef z`ufd41Bq%cky(Up@~2ZGMtc$(rDW%4E#wqJOvH`lezx|G^9p9Rk5|l}=%?nD;!7}< zgD0o>{voc_-20xUr(yDNDK45Xo0hj^`lg(OJ^QKYA8h_fXxzhy)~4E8`6n^}fY`|=E2+wY6M;`?6IVM}SyDBE`e>F>Pxk`MC)GWS73^_O^XvouNe*CN%1ic$)tF6Sf9u`dhqZuJc0J zBQAP-4L&kLy%6Xsq>7}e6t^XMZHDNuu3y4*NFqT-Rm}pnqcN;u0OER2>5t)l?0NO- z{%&JA|4YoX7AC?Ke)_x~g;5ghF{M%-)#_POI0~0ODL*}V=AuZI6Gz2HfiK971?scm2JU%Als{=HW-TT9e2*~@1JMi9X9lU zp9!3DcrYIW8&*)9Yd`;1dcJzcUeB3Yr6o!W$WoVyuj<=;*CzA5HJ9t?n@IGTqu&Yn zPGY>FchYoHQx8I=#N8`KFfmc^ zUZQUSDLY@%VbXxRO>1lGmbBc$)be95N^Z!bbR*6b@sAFL`Eh~Jp9~}dhkZ)atVXas zz~Ih(&AA2U0ge54s~ziRcB(y%L~`#nO7 zVnW!A5*=3>=IC%Xo_w?{Cd6QV$k1N!UQ6RULTu<>!Go!t4+eB=5V$NBvOoQgnGy|o z=PdKD{%2(c8&6hLn^)Kvj?7C$jlg&hN47(v&RCvtO_@h9S+F9YqARCk84%Tc^0TF! zpSZ*7J0((2rKdt*8ai{RR7#Mp>Nj_c;)4aY<*F8`oY1}&ZPjC;?U^^JJg$L*qEM9f zla(h=ZOhklLo@6p6darktXeRZ@uo(WxP`RYbQlVb58yMGI0b|tMI%L9T^EekH9)10 zwbGN&M5pe!{tH>5%BL+RVHqLS;8gEBXLHM%FI_cK8Xlv4>i;-kR4$IB&C(C6)V@|$ z1=yl$Lq)Y*2Y@eSefygyhXoV*_gPeSgym{tMcR>YG@5?OX*yQ3JR{o{9Dmz= zhwal$ua!vTaCOgpRaPT^vP$v=99B5#HjB)R<#swBngA$ zVHQ@|u=8{3{<^f|*0t2=-$Sh!dm{7l1hJf9xd5AaG>aW(29#*5Qmt^fT| zd3MFuvE3j}A=@5jmK!)u;4xJ+Jj1N&cuIBojb}xo+2cH`l*Qtr>HIPlQ_@pB+D&b3 zRRH0e?->*VCULRtJpL>xig#Asm}Xgo9QW3n^kt@gj!8quo6Bs?sv08eFR{xu^!VsO zDDenRFke1BjxyuHhby*^v;dw~gQwn1H%+Mh=xYAD!8Bgk zkvFfRJ%G$g`PyOfz`EzeZu^xz0Rm}@W117Uu98n0Vs}dJpSK3_aMGr?`Wu{n$lG8n$q3AttPwEZn-3k7HQ5`PYhwS#86gSGgj8QxuF#5HZ7!iAMq+k%%0(FCb= z5bk`3X7a(J&b-83{=I$`Pfz%zJq`DBTnzO)HDdQ+cV@vhUk|V&v9fbO_VA^5`$w4_ z=cAR?U9GLlPF5fKT4NyGA?Qar7tfTEnggWc#|MY>4OXKq!8<#ATakZ_555%^8ZS z^!E>75!Fx6Bpqex$<^mfaKw#Mv#vVsT>WYyS%Joh)uis?%^)+YGTW%+e&I{ucqcAF zxqCa{T4Bxi)%%V4^vU%{O66m!#g%Fq7==5rgk>R6Q-oLwBroRw_RN`4*zsJa6vyuq z;mf_|Wp;g3i-y&81bp?tc3fCI_-bPy9hTF%tQg?`th~Tt!(1SM|0*Rx8()UAZ@i3e zpLY4l#lo++Gjk-l|Mz*7ucYrH?D22En3)=h;`*ttO}tf30ZC?k#Sdv}!PNitA6!ss z%NxS>y5%eC`6?s)IrAksKw&0pl>(Ga2#mO|^xxC?giUZ8{AtBuVqN^8kXLP*@|EJRR+~CKuth+7Os5x{h_n){hqSnRabA>L)rVEF7}un-UWns z;N*Vkff&>`HOBXHxA>;v`;lnV+ovT$6yUx-6_oKspzDYB+1S2KpP{>RW{o=@{>)ZT zQ1GG_!H})gBM=OZ{NRB~cLfFyN%h>~NG zLsmkF(gIF4T$szX2mj;Lrp)UGrpg1qn6Q#bjEswmGr0N%mhV971rymN&%GZr^!1TI z-M`M_zWRss*oq1;e_1EIWO)N_t;8@SJByk@#R( z;S2@B|A0PZsrgA~uCqMQhNVAr4i#MaSnH-?K@LE?Cy1C35d(ohXzldALuhx}%-Iei zF=!;8EXl$p;SCO>)>0JOkAQFpJVXG`qt0_*qeYzT-@OjD!aMbMVP;d@&-INAe z%FvcJ2>t-70Xt?8&m{TfDMH^*<;|M$04YuU#3zQI|(^W1V%DzKXtN+(i)X=z6kF!}K}~?ggKhiFPi{8)d20eQ+ziZNt()K;D$1sw}_{5`_!RNlRuo87fr9+=t?auy=@4V4%yRyqo08x4|MX0{&R>a*E>`Nyb_CMJ z!YW+nv(`TkA$0nS7a_L9aSz;~*+3&g0MV4(+}z{1y#vMIiF)@RKoo~Sh2WrisX88j zX+NlA(;j4Y-UXZQvZ7y4`b&2v4Auk*Ac|>3;ryqnLfekrhOnDI*>i}%Sh+pvlh;1( zx2wKO06*RqyOo&(1(@fC{O#$Pice6Xg)II0+{Yyc3LJ}Um<)fd5J;}JoEQTH^MhUS zv^mVhW%juBQlJdC95U@w;peB_INHY})S)zhVDNy&b|K^_1Y%qOZ&%q$mASW{9@~q{ zs?-JQEiDzjE8vh6U)CuH2iS|{*&ahCDGMhlFTfx6LV1D>uKPz$P7ZRw5x}4tKv^zy zD4N9mCVhIhd1LjKfjC(P!C4_yu3F29F!^RTZn|ZpDR6mZIa2Pv(H9`oq-Vq9!oySwHzJ*ly9Nr7-x=$Ws{D8#g}v>%FsT)NOM=XdVJ zrrI2*rp5SOG+gc@ursS!TONzs&n=}6rD;5P&~|iPuKt}}60@cg=119t5U@+tS**2< zR$B`7L_G?=I1?u;9VEFP?G#9Us+;so&QUU32P?Z4489E3fOp4d1#@+$ny@&%0wp?+ z%aW>O$Z>OnQDRDMS51foeM#?Be?}Egl125NOhD@m!RXg!_dOUQ2iw`piwBT2gjW*k zR+sm2&;~e4yd7vyn_(=1I|dMokP{s+I3Q#Ovp10w34ziIpq@BC+elau)8!0?x%b)E zIQ_!dP>_$DKY#z(7IYELuI~CLg$17RBegI}nq1qD?)AM*lQIW=3sSPd0}N=ZW9su- z0ArSwj7zaFg(R(g_-=;wrf`0aBCF9JTwb`59oM(H+YT)k-Mni9>F5SxHuGX=d-*P- zZch*V3j7fJ?a{%&8-dW@y>vO#g{M{A{nk9N%%s!jlCY z4SY>|dcp}Co|F%vc%Qrb+P5d3BWZ}vH4_q@|1qgDj%fTAy_8F!{Y)s{54bLS%Zv5; zxv-M@pNNX+3i>{-$}-FWOtD0vgT%)?%+&gi6lKtBdwJ(QXk=U${U8T36KwDcL^abv zht}%jK{9durwMp3+8=Ml4`OTYECc)w!HBt9z+pFoRzNM_ z{~ZC?q?JMlMpRbe?##}N26G9F~hj^yA`$li2+zz zMBkNZs|3QSO=7y=TpB`7Bxhyj2K73H>#?lnTP!B5vquKcCTp?Cbe<)~K2ju4X(}h= z*f}vMKTg^~dTpm2IeGGFLQQRHx_i^3NSVzjN?g}UqKjP8SId>~WQH_CQD>=V%6VN~ zT}W)VDYpYK0=X~li;13nxcxS7jK==-`iGe?vIVW~loZ7Yhk`w-FW93vTEMdrtXQ!! zCyQ~?6g*fLnb2u|2zKBDEN9#q_TM#DM?(F8+*G@S;Qf(F`Ip_lD>?mDupuaX%O@BU z|0j)lQ?(9UaSUv2)h8q&O4_6jZZTH>>lYtguL{)n@}Db$Dcn}-+_JiRyBO<7R=krg zWZYV~bL-ZC;cb^JxS2v^#E#`+FabXdHxcDutpa*TkCqQ{<9$Y%4y4zow1k4QsWgKe zP4ul)0jIi?-BVEa@jjc@#@TVCU8y$^x?>mED33}($u}K)6@|wu|?`) zV_lzN1&Dj!CRNBRerT*-`_pI9w3nLx-+iA4?NBu}#cl(kZCIg6rkXIMiav#N6Hw(0 zp7U)d4%!yuzU=ByTL2v)D;d)TO z>>O@4ICg{z#Ae7IjxH(bTQKCgU=U}2E;E>_{5ytQ8i4&CwgoU_>J)AyMnvqL^0;*R z*&WJ>7w_rSSS@~n$ed^AnsVb_aJM4|dhCtq%Q7mevx<%!YE(k^S2bQr#Iz8PgTNFP zib&t~ee9#o+AL1$&szmUaYu8G7%Ex7U<7p*WZvK2s+|GB+1b@Q-709%;e5TskjWr> zn|7|EacbfjgiLo2%_3P6PGz5mti&?&^94&@`cUY!kWh+HD%6_>}Y7IuF^dz9#&BaL&q=N8>LN(^S@mwMbbO1h=9F!j zW)Xj5%I^s?rl<>kee36??`d#16aEUw;h~-d+pJozqJM$ud|U{ws@O%k|kTS69cInRVAPD;=8$7H=)DbG+5DiZ1t=BV)ytt5)w0 z*+d_|aKc2Ksjl`@;UoJduC!ax&!2G-?>%8T-BbPBa_rbqYU!!z%JXJi^z7c!sH?x+ z?q|kxNY^B5g<-wq?Sr=6YEeE`Ti7~Vk@$G>U4x{Q#^~6Muf+Y*SsUu4gv3$DiG*k= z*NL^Y_a&Ac=9et`4t#Zr`n4}dyUny@MDT=g*0P~H8lzfNgFnK#wo`20vI&%+!vLe&5xT;)0Y9qM`V42Y zPpbYZ`-;CWtgSY8xal#&s&}0bcX^D0nYD z+rqH7)9c=bf5$_7mErBCJ zQ)C*8wxSqd4wmdYf}zB@5ed8M*Q-$g9<22Nk(T&EX5+&iLbt33A;1d`3H&p+PZu}- z{Av4TvRp;TiIc%@R2M^?LIh{~kHZoTh#$}A4y50@YWUNE=9+`DN}>{Qe{ zPrICKt%Fnoan@vA6+sXG5PvD|ijV#VPpFoq+XQ7g32?7E;w&@A%mj<(j zbE-or7Yc6XS}4dHtS9Gq*mc>cD+3ypLL!j@EUXJi)0)XSH-oz$1Pdg0V)3bl8P&Ot z+nDf6J~*>_20A*s*_(2*>i*ZGQ^@L8eTNix)^c&;mjFfKcK6dsOWg*! zia6TaI|;uMb7-FHHxr9?y5wH!81KcZZ^R9@6MVZjhixQNQK~lI!fP6btiK51CfRAK zOKP!d1vZ8>%OyVFRN}XDp={1dG7Rh?!c}jM(as+6|2R==EDGZT3H}p1{i7o9XL%~E zU-(kS1krTSR?5N=tJ>4`BemF)vLwpuanaDPol<&a@P-1)uq1gz0PCM~+_)1;h!4gAfkkxi;44Ta-gv{GL}%84ji`C>Pt&}fWnvP_bQ1qT?L>og2H zf8oPYl;c`Y5zhoG#(t&EiDeW;xlEV)aZ_1PgFhN_<*Zw(pz)^?WFbK8v!K4?_m(gv zL26G52Znu;tySREDdqM904zqQ;HN`cljR<4F1BD)3)kXwSzQE!6|K3!CA4+k^ul97 z4*%AdV|#n_VT@3)?@iQzc7nQK?3&qO&jd?>qAxG!-`5yR zwa{4bgyvDTJ3YZwQK6H@I_fm1rlCrIF33wb&!iNT99+$woSOdp_$L+1Pz|0_iPP^; zmhxu~`JF0K=O^oM8yUu_G~+Dg&L*kp=lNmFFMJD6m5Wwwy&n>zeHKrzqpI%Qg%V+o zj*d_sq7#Ti``in3-j_>z`qh9`nr2s_v^8`==#2n9ijUZ>7(P+py#Dpk)nI@!KehYe zR_*QXV(ndP!hg1Qvp^R96d8=TJ6z?}|vxL~=1KT^2gYX@hxyogs3 z+Oli8MUppX65NM3Gtu};OeK2k)EBz3f4et?^eQipcfmfagHjTXgNT!pNsFl7G>dD6 zyM|RuZalUsvEDG`Fw<0dV)*GOnc>Yfew#4kj4&=^;{5^J2y>Gi=r@x`<-S=DUN`#< zx7%UQANIwsOD>a4FYFqzRv_VLrl#H(i~LyN=HY=9=jM0z+9J$5pdgQxX`8fz@UUfL zlYr~dFThO%7!Ui3LKOMIx&_}6+XwQ1sp;ttT_p2Ms!1es`=^X-8YTNaf>D^V#@z%WJwzqFInIFvHlcnNP>lAr}!mt zacdgI4#W(b%?8(_o4+?Vv-VZomQIyIhC|aeyW49d%t2e~&%pI+75Eq=2Lg~SCF9bp zf>D#L+DB-folpPvt;S^>;%UbnVpIhPzVrtfBDx-3?4$IX1oB)i-Iy)u>lcS=1H=yt zloz$sQH8DZberS!&u9g>w862l-Jq@mKVQf};HI!F9jqu3|3%3l-dB z=3$?WOiUE8rzp>TX;}6z9InS*HZiC7tysz&F|M8wU&HZ`1z5k&7{`^!-066sflMc5ovZKFN2bu1uHtxHfy(2Q?{-xdU zY;U8RSeAmPYj{WvSb?LZwr)9u@(XS(prZncJz(25Y^=v{(^ssYNE|O2EJJKR@1@!x07?U=X0$jJty?7MrHoyb}DS75~lCDmumqz?H8@lC_DXG?qOcU>O1~!vRp#K((gW zFyafN9N7PDYe@={BXV+bKJsDP=cT7482nfxiKGc;Iq=v8*a{&tHq<3`2mLBE;oqQ? zASer;h^^rpJt%LkD-|&l4~#wgcLs)}2B-=qGVh%_x-S>RO?P2co&xqtnlX3AO|2qt zkDVU$iti}3DzeSwWiLCg`~Wn*Nw-gD!CR#kJoU1~f@*BvjX!WHi{khe5oMLf`b{P+ zs;ea3Q>I{j3#_M-?xy7Ay~IeAFYAy$potQ$-qr!ENx4y7R9zFC3Y! zo+a(ew>}Sh5!Gxe6k2E3aU9$Sb1o+Ln-F{gN?z)%J&zb`8*S6Co+&z*i@zTwW%CH3wzwa{784OWcNNKn5E2>c9` zHnz(x^#np7hPye!y;Nq5KeR3zf>EKyn>Xt2?hYWNNMURziDbKwqjlQ(=2iawd;6$Z z^SFC~WQWx*tcHtd^Ex@c6{D&Sz9mJ~59HrWGCq5+1t(=qB(B)X+&n9Bdb9RXVksr3 zsHna>qHg%Er$H73CkRDg7e$c%}UJSEEoM!56yAt5RyHqhI<38uP|{dK41U>5AeXA z8Xc!D6%A^OlK6%^A8Sw1GmZrHTJ;c@p1G0wumIj0S!Scqf*cm_a?kd5xf z=(#w-*uOY`ELXcWG}pgR6s5ftUuycrqqTR30I zwcFz=Y)HoCu(AGJ6o*8I)$K~6Fm8DP9Ki15@ofiMVYvd$$`8DSfBm%P_n~oMbOzmt z;x@BU6yAwd^-)}^Owp#L`ZST(`l>ui*4o+mr@m|W$B%D=B&V`#5XFeiTY-+zKqTiK zR~1Jj2oBpcf|uF}-U%T*;UR~;%1PPF2B&&2Ly6~n(8k1Td_qb$wjh6aaf2Tg zr_&SdYfn#{sgS&J0JFrmk$rfwwZ&?Gvwly*ellg$HiAF2ao8qy<)J9&r;(+XuXPPZ z69ofS(;uSkmN-}D3RH7laRV;{aK_eRfj)d~0&z8bIkGeMc{I7BN+ucK{nCri7l`UY zSUHbh$9tCUK8vFs{OFoT1aDn*y0oD6>d9JvF7dlB&bnI!Nr}{ zdKfiUJy+J4u>EVQL|!m+O5>8eiElrMnL4JFYn>F$L3Vh^Z#gw%;-Z{C?!j0$Sea&5 zMdUCT&HSp5N)?zYzX_*w+629~%#_o!*}AZO;7jNej*Xonn?2sOM~5_fCMpCQyvq7D z!K`bfvT_$D7X8nVqTwin{(3=(SMy+;(^MszorIs5P?HYTK!=wnxZr7vM+DTD4f!oc zL`YA4OQ~c$?&Mx27q-E*j=Kp0&t{c$4X*pdyj z3^sE~ZP^SA4Ds!g&);d4Cr0>R-=R-f=coG(kfFQ%+Eb~1`uHZ!TtO#BsZZ+~!~mRn6Sphj{(}-6PUVU$a+0;s+aPue zwra0uZn{O@+MUQ)p+7b@++XIkaQrh(#D>m+I$yOef*D~*OAq|$2T(L@K;Nmp*`>Yi zM|Q|9y@f6sYn`0E9=y5EDbO<(!+YKZD5vVh@$6t`evX9YP(@jlbTR}e*_DRbfVSk_`&imvH}BQThO2b;FG z_V<|?;hEkdKRCg##`sp<6qy(bBlqSiyk0wOeWr8nfaj_929%Bdm$U$HgLSB;t=#L@e48+JYGpXU(R^GG?FsfC4^qO*3 z{~o=MmaD3Kt_*6E?PQsuy&_@g4T@7+~-%)H@UHqucUtO@GDjx`*C)u1cAmO5J+0na~ea)@&=E(2gbO2-` z=_IhaDtU^G;ZQ+=9-0p5zcwOn$96qODCIIp>RZ*qf@mN8mp>&^q-N4Q+P1vrdWF7s zZ=@l^k=(Y|IBLaym&CQ@^xs}MEML*DI>d#Omv7f@3V94wRaGS+Elp2XxKJxZT}vY@ z3w^$vvv1$NwRzd-X4Btub1Z=Gg{W*}qn()BDYoLh^m#BQ0*yw)#0ono&GPl8)#JyC z8v#F#eNN<<7cK{guwJV#MQ&LHm-FfiwE2W$;ZCEyf zN(1EMNRTk#+V{TM$T#$gmv3I4FK3UvK<6-D!Li<_nYKmm@%IAhxiWq<$S4rb$B(t= z?G^kaqCUxV7nMJl7P{tmIWkE_ucXbwq-kuTLMUt`Ld-V4H8uSv1TCHc7U6HJ8M%s&8MRaVgyBtR-VdT!Gznwp)B?lj*{2kU{eb@;*m z#MkJaLn8er-qmQCgOmi0w55fHXk@fK!)H|`4W%il`=lyay1^OYp8h+q7CB_n}ZFDG}#b_xht@yFxj_0BJgb!Qf#sbUyO};2m-to4$NtEVm@^W*wJ8`gWFQHuQ zAXE%$C`;;Hd+NF@OFMf$<<~M440o-kyDr`LR1B>ZRr_i+{ zA9)wuYwa{Ks+_a=Ymq9f!F~RMv_&y2y?>aa2dt$UIZlD6A+(^@ORdvzRVT6AaQGT# zCiERJLo24)B^C|G=WV#>3CT4ZJ~P4dh!>oTpP9LzUEoQU%+C&*zIQy-Xw;Z?2x`O+ zwKWA_I!Low7g2R}ay=2(vfN4gjutif1<}f%LYdtLLMw&th%TLs4q@FM@{L}}YBQuV z4#xZP&2*fwbgiGTT~ImLJWfoNsUW)oo+T7ilLgE__|JQ11VUPBOpJ)awv5|)rFJ%m z53D1#FuX-@dO9?z1>g78A={lx*Xy#meM8Goani38K27}C2-(VXniB%NnAx3Kuon~>T*2_y9&+KG8Pb~>xede?N- z8?)>qu00xG05^gm6uM#1F~(jxKBFvZT3{qG9ur1mF1rH5j?1sN&88QQ+KV_Xjj{H+ zZqWjqlw#O`)HM9SAweaWFfJbo)5X0K0yzgO1o2IAuKAREIy&Rt^1$CiEQF8NMJl0n zq}lcTJ32c>T#UAtR!KpM;kt-`+t+IVlR-`)B*rT|gTH!+g9AM=%YntGFLJO3ukYrW zcq(WzjN*&?F(X@Vl!%`*>Pd{^YP4-mXxv{kww4`43$G+{xaEDCOH_%YBvU{3t2ZP@ zh$6v;P3D)H%`YK_@3l3b<`X%%Z7Z)e!Fp2L6F{Kbz^KDutTw`;BPB5o-x?+uqz3gE zxLqjo1*&nbxIwwsV?TvV<7k_!CF0!C?IMgQX$VAb5#@H65}7Y~DJMg$8X7tY&=Znc zF4-xTYt>f?jty|0z~bYa08y~Ri%Th{<+khdx5su5T##KnEMnTU3{Vyimg0aJr!D#| zFWbT53lK_>3f0um_xpl(ROKYwr-)jrGEXKvv|J zX2i@K;LJYnDCh|4?7j+sR3_-sJ`Qzdn^ix|)W+dP&<*eue0z^dC)@DIIG+|kK3M+! z+T!6q2!t$xwSes)U^`&^56eHV*$^N#Jfl3?Jz#r<>ER)uv%Lj=Bh2TwPrrPa=>tOC zPkUu@>Sw{Y;@toKV^5&CgmvqE@NqE7?2(WrWRFA=?cTElU>{Zjgplue;WaE_U?wh3;qHvAaSR;5jx~mM=uraNeVC zSsUqBHWR$acI2A^AC!SQ``r2ElWi5Dn~}@STQ=X4PXY;1W~azYAf#HVp|vRhwS~(q zeSpi*n)j+|P};)Tx#wM5JpqyFz%o2NJ^eXIBqcvQxpGv&@}-2%(Bk^O?yp;bB!(@p zxqC=ontu8B;3Xtaqu#TUb@cVC0D1$mN4Olzjvqj=S?e1Z_`n7H`Sm^+_TIIjfQbA0 z#z|yr933_;-Ep9iItbdMh2w6=?p?Jmwr5rf=N-bob!96nJr;PP^oTEtChz#;EU_`n z*vcv}4uB)OFMTDhed?PRSo|uG4W#oY-45?Yj372wnNWrI6(@IbEkf5dEG*1Mw4W`> z;Y{7JEAz>@01vOTh&<0LnmO)mV5~hrP0tECwu|$K0)3?Cf?Mb2X7NoPX@~Ev@T2Js z7PD)&;gfjMV_2@l?#zTv{ytc*WZ$iS*B($`e+!C#sK-1ia~YBcV$PP=i~=$qx}SB2 zclRXEUzB!W=yT!y>n!C@7D-zAfSQ2R-3Yq=01N}MHaOh>m^WQy`^A55FT?ML{{h;9 zPX;Sy1CE2hS}<=bMFdV)pBGUXuORHR__K|Wq z!Mh5jUu-@Bk)V$(tgLb&kTmOCDy`h+Cr1Td%*DG(Sid-#-0(!A2i6v`qJUkc!0!eK zV5O#~43L&rky^sL@j@F;ZiCV8-rf-!tn1H}oxcl0uwZPQE_{HRr=cVCX7Y`Eu512q z=@Aj1n@xi^Vk``9Z5fNguH4nrvp~KXsJ`RU@F@UD9B^s#%32-k>VuyHaNwwkNei!h zSrMuj)AHs}+Y*P+RyLPCPX3q4si1zB)SSWRYUX>*R!}_wE?it(r!bS6n#=iDesw~v zl+=}@b}HzRUa|9sRl8@;x^uAPO^?d~Mh8_nbMqG;c0|kQL`V^0AKRg-mkM_m!aB-EDmbxWOjg6UBS-QY#gt{!6 zqmglNDm&{B0Ch(n@vu(X6oFwGSr|fZHnI`gCOw6YYU}PZMZaNve1V36TM*#8V8wnQ zOLANM%I=$=&v>^Z4?NYjHed**dv5Z{(UTV~A@SQaDW}yJ@bp)Kf%YBq0~LNSqF-9K z2M=|Kck?O&Lul~oUK1%XNZWgPV+xEZppT#Mt`40~YdsfNcavM;)&GqcMrrB&xtd_! zcI=fF(9=qds$4g$wo*>iCza5z3oZg^<_=agYEyu!zXu0;>PSfF=J28;jO@Q*7fxZG z3Qi8^BaY*+7QE6u10GPo;FWfTxLi__)~yGHOQ;*_RVg!tXQHB2JOha1U3v@}E>7|S z!Xkwo$4%Y5+iB%9_FfVClTlouw#x}<9@}otB|Gj5Rc}&di~eyU4nW)K;yCAMn*71RNU=hN`pq{g%DR^n}_t@${LB!_gHG|jYqZVuTnK&Jvc83(feEfZjgwx3!W4_exJHxB7 z4H9fIVYAhuXWLEO1;s6UOn2to2X{Yqii!QqCEn(=A$2sS#OS+wXWi1u2!edX^5dBw@-hWLk-|F^>Q1OlG z#*l5I^89c1Z{t?5q`kcPo3TA&Nrxgv!@4EV`n7TDr)SFIYTBhWkas$*A&{0 zvu$kzsmO@LZ2N9PbgA45;bk;VZ#R`AP{sw;S-ggc;o(yb#&Za)W{y)D1GDXU59jJb zszjLZV=rX!(vlH`F}DvYStxX1YsDHN#mD}SD;stCo7?Hul1eB-bl*EOP3GtNuQ`Jy zy~9eU$%~aFZ@RhZ+`c{h_3>G1Banx=aQt65A06E%0_D-?BC{A$bdg~3=dwTirXq)5 zy2z|%E4g>jyJ*uMBo0Wr2at(g#mScX`D-kd;;ubRQ#sF9I{AJ~ zFvyMmCWAodXB%DpLw0ei-glV{ikN|H?*Q)P%9K{87yI%{#O-Q!8F8%|w8m8-k-Y@x_bkib3JUx{g?)U=W$?`# z*2pjR+D)Mqh%%}>*DCs+@E|1JxEmY>V632^z($i@YGng3N;(6#069K57KgPtgDUYO zL%$Qcv792G?aLlDr-mLqdh|&_hSkUA?SKo1sKpQmPZJyMUq!;&06;NGq2E6+R^)sS zXj;Nj0bca**f}*bGXQK*Sc-)7%^f*qySxCdHS;BiplNe;GOeUxD+I+x8AlO}vW4O- zx5{k>?)CbAPGhp&Xy0#JmC{hysg%F2!-Dr>aG8L~0xKmTkD9uxMokSA#tpnpm9eXI z55@C+pOiw4O{iTTwx^?mCKkxGVEO{Ycl&h3*L%Vy-@db+q+w%LC_FH`xQLG71ZS5uK5r(bM=b&zaeJ!qs*Y3fw2BQgbl>iAtV*vLOpAoUSAy z_*sq3n=#5BpIC%PYsNuQ{y9A539xWi>W;v; z;$$wUkFBWWhI=12>xiDlhlg07Y&E?AoszPW!JP(N=4dC*Mq4-)5fR)x`Z(nFmwK4l znMKVhPhV3KPt`AJKn;~kB*NLaCm3u4cC|}(FD$oq?aci%EpwqUi)Ax{*1h|3De4tw z_a+32UM&(PVnU}6IF84J4qZbUwom%E)BQ#kKSA5%kvW+#!7g0sZi%Mlr7~?VSx%_i z+>~)ulsM#P@bKaFh5;^MZt~9G^^nbQ*Hw45ng}?-N?sGKZa3MXa=XCT^J1>RUi;4a zvU*GDfq>`cq0C9#7pQC*7(Rqcx8Dauekuk zD^+iB`(M}npzLY2p&Oht9U@O6oJBYW9WmF;1GOvR^i{qH?|U3(k&~%SlI;^T%vSR| zn;fV~Ftrno%Na+`<=M0PF?ae0kCA%s-px@dseIERj5Zi9Tr&|49sGmQ&PO~=+*iPr zP${NoV5hxKjA(RhW@1022LAuENv)~svJ^j#j208ck509c4WmDyOqw?r;kXytuS-u zEQg6Wm`mI;YrEcgzw6PlK9IdwkB>V`u6=L0>v>*`AbS#%M9B~^EV!3_AoK1_N$93e zgE>DzGTPKc(%(k|1L(3s*n3DhSKAu;s;oXOFv9M-_I2Cc>?S`;^l;%<34ca`ZXaF6 zx?$Uw+4?iNrOBnnO*W$f>F@9*kUf2YknL$|K&)d ztw%&-tqk6}$q)D5Th~F%`bcg+9j(Hz+Tv|R%?QkxnyHUPEbUU6l7(}I7IlZMyRk{4 zHazGzb>qIyI3eq=k+p@7VuZe7G#$&G@rGJ)0ptg&K8rT_!5!He8n%OPg9CJUR$P;$ z3Eq#xL%W3)(+0B@k#!*GPdnNKaQb(Bd|f>+4!q@Xsa;Pgx0H_0$<*b{ZMUJn* zG`BW7Nkua4EWVwcOu+ap>#X~6sB$Y1%FS}4%)HVc-do@^ME7j=8V_7J6<#Yck1kd} zw-jY$pOnE*>=SG}R`;iGUG&yxWw00`*T8Nnc6F1>e;2HR0i1lcz+(=GCE{w_nt>}0 zdGh4Rb0Cw2$$Y7S`GVSf0|RsGx^r(%Dq&U;^r}XbQ`@_yrKF|H2L__fq;V&%B#|Cd zt*pI-=Vw7FW7d3p0iQCndDdfBHcs0Yq((gD0*yjdzjTE0F3YI9sso;-E}Y56+7Iq7NnnG zy40WlJs|>1W7>nMw76#7`%AvPk(yr6EivmQqcL|vc;_t^>)!oE^v&2eV42oWTkV_8 z;{l#3NbuijRi?O#st>|6Tok}S!k5NP=HsH18gUuGT(nrK6-vt^1UKwW9E|V3R*0|r z-1&nsqA!d&!usTZi8JRHxVMKh-n@AeGXC8t&}ZwiYVsde9DsfXmX=uTsa;3E@*)sr zp4Z4Nx7%J8;o$l0ygr9;`$WRwFc?;ww+ImY4d@lCUj-dqg`sN0Rp#@?7zFfrvcEp; zFF~t~Au*nEyfUMhqkA;1Gf)r4+yRX_UU+xaD?{ndRQ-^fPfBt;+&$qPHt=-HUgo4m{UniLy9OT2p z?jr}{m|q57&_lm*lFvlI`^8YX@ca7u`oA`3AhoQkN!9*0nrWMOyoh<1vDc5|5m*Qz zXQR8F)}E1_?E$+2TT03~Nwh5UPWOp+!p~V*G4pvRTibVAFcsZ;RQ`K_)Ch+v53Tx!*q2xhUe zLusokE6-Q+_m3T$ zMlD$H-yJJP zCq@Hs4j>;8>-LuY_FDu(S{$W@-%#T7PUM-`AC9&U*h2ok+7Vf1p`muT*PL!sk1_+r zgpO{FcWcn~^PL1bIU04I?&YSarYoBmd3D}F=>wXo6@Vfmkj=a z>qFh}S-U}v|HXuCdH`UMV^NnZX(kE?_DWl9-6um0qRdtSV(RMXI6q!-O9vQP@>Qei zG-YwDIea-n_5)TS*bVBEa>!yIlqX?I2G57`MuAC%c6~X$>glJxO8G4*#emtUUO%AW z+Du{!BCIX{o2)1`!isL5H`ADp-ba2qHqn|wqp+sg4ZhUSiOcaRUFh(~#LcIo)94Rk zio8>C>eDl%Mq{$XaIidSpTT}7Mj;$(9Y|+J2%v^Y1G*|xxjG~=*mA93Gu8#w4ZoXC z6&lbstkQl~Kx)6GfREC}YN!vsJe{~1B{W5`F*1ri;qT+1R~um08p*ZwE22-M&SzSE zdGhlVOWPbr+TtEG&yd4xOi!M5bjx4F-A&eKwMH1;UrQhtR(_O=a!`{ zxK}~FrY?B#HZSwiHPk;Y3V@rA9|fbu@`Z)3!y9G5y5U}juAqgzAuGh@q*1TNT80dcY3Ga9g-8(kAIdRuTacal; zC@ulFC5}rn#p@$nl0J(N>&j@F+qd6#)ShhfN;c3We{&%CSk<$}X2(CkW^n1%$nUee zk9hvjx(PS4f~1}g=!}4dgFHNNliF#3%=qA)Q)JDgX}NpHgIkl!{cz=%Mrws}=HarBJu^HA3VOI@&M!E1`Sv!0ga|-Q3y~kxyjJeL8mwA9;0@vE5+#vR zn$#s*H8Y^+14aC7u?nDywVLN<&X0BJE%kwfNtD@r@~ZUc#6%)$Il*_#$Wm_Q8j5hL zZTb{89c}#&#`qN)0*?>~jN^8yT9tk&4=mQxpK5mFO(+o4civw_03E;Pmx`l|w;Pfd z);~Bbn{l&F8dF}!bhBdyBzv-M0*i6QsI=u7XW9pEDQk9tWVxxd@Kby0qD6aYAGMsg z;W-GvmGVprw@tP+8e5y$12uA@VsiOyV*HBA(mCHsal zVC1~Xge%76S?P9?{mzrUjeeYG?!`-%oF>~E3*-;0%Zp=ycM13!0JArY_$K`7caUwz zhKTu#1(uSU51R%*q?VjFu&_g?c{?#aE?m81*^}3N2Zz+>0lw_0m-%{QP6soD1*#@4 ztxc6D5WhN*{5r13$)5SteFpRFXLi?cl)`~rO4eh)2>6%8v97!3aT+X#P;aMr>NO5R z{Xb%qi*?dq6)5l{7?Z3-#{8=6rg>og`teKfc^ps}@DpuuC!zwsu{Q57qI`H9o}Qn6 z-?3vWWkJFxsKk3T_c0NBZ{ieo*=O@)KqhX@F9^5m2P8*6ap|A9u0D_yk(ZDerk_~u ztmiTTGM+dmjq;kt_wkjCLN(v+NmwUVSr%@Hq-n2v^v$>WNsx@~(VEhmv~spozY?{0ij)v+XGB*CdnW6P^Bz3BIP+G6DxA-nyv0 zvy}SEPPQT3%$4}jqE409>KXf?sIOTiXT6}J*&w(nSwAZ^TzZYIG5kwd$$9IRz~FSM ztp6&`uTAI!SUC^4>bjKA0>cd2h_4qY@?iP^!e`MB7hGQAbVQdG&aLh=1+C61Vbs6ak>7dd z?afw!HGISr(?WoP2}n$!`@bqf?~uNMbWfvekObY~hC9#)M#u}?ttl#qXi)}An3v;r zCC)Ndib^NhV#ze>k{C&%e`oF{-vGbx_uT%xD#Dfg{(6lfFdwz@F0w z_AD3#aFp;If)8o{475K`d}zo9BO$9ZeWv|zRBZ<;XCWCR1A-WtpYi2B$7mk&z^tzJa&ik@kBGd3HGVz# z;gxTR1AqKH4}C4uLl}Iivt+U!qzUgcm`x0L^=dc&vQO1up3BB1$ovNT@cWC_+UCFjE{z4P9W*s*p7uIt5wbGYTWr2g{qbCB8M%SM7z}DckkK@9%zz&8PMt3TA6@?E@GdGn0a6 zq)9ti_80;4@9ER0@592%ptKvBayemIG{%-C<0X$WL=*p0$9$-RY6!|+PlNkTP9GU= zA1VhPf%CW4-=vwTgGX{mur zkUvK0Oz(ey;~ZK`s7voCBI0)sI3*Bit^B4%Yu3d$U;(cuMPpxHJq~mA`rP+Hh49^_ z`W>w_^Z6EEq^o$2?)TW(4eXuDKPR4--hcr^^=Q&Q-`&@z0MZ*eXevs`JRWBNub!P< zvp;J4^1ZvAxnxKFFm$1Hs`il!Vf3%xW{{nf2gI*N;-(X;OZh~>E|^+Hw>EzN{{C)6 z00;r_T&{uGemw)6{XQ&I=ViF3k|Fq%o({_OkQ_9`9XRivyKnyoU%RX`*2ZzceZYj= zv>ZxUK&&3TfW5F*apc+F9Xv2bq1=DBgN;#Hwpsg7*|or0YAQe<8#>K(4sc!?gv!|9 zMr@t2q$-C3eAcl1mTOmxYkvHg{ATg5wKWOX?Mju65*4h$kfjV<(<+Q$R4G@UA9dfN){P+CiG@yBcvqgmMetA0=934K215 zT4Zann-pmgDcOx|p)A?=nWAJVDr+H92xZH@P4=x2*~y-L8C#Ya{_jV(n{L0~?>~3O zb=~>Sd~?qCo%5dOeV*rinYa0gFCJ)D*IlI-(;a*ffwj$1rMZ)^@Lze{9QF3oWX@9B zk-;*oJYDNsDtRT_SaAf-C^X~tEtrTb_cu_@vuow>{53zUtCgCIU%f!`|u;nBMe5@iV!JQn`T% zVrhb&4#x%qOKW^FV;_Chhg(hZ#E0umFx!N%PIPwWjQKEG>|#U%-4TcsC+9eWtm$J% zxvqa1XZPpmHEMCk80hGMm7M&HY-5p%f_|`H zxN2UI^-<81gmqnf2lrV{>FbNpMHV5RoImb)v9H95<23_6+;;Dv6um5!8Mt6b$-h$c zCX{uci=;4URbL&srX{ILGJpy$YlOsizJSOF*rQWHDOob2e8*&* z3cU>2SG$}v4K?*0AQHDrrOJuOy4Cvt$$IAC$d++CkPm~)rtMHHIPO?lro0}{0lo#` zI_e|w8?m*wErmXvK)PsZ3W_s@gh^Z5UR-(^wRWy}pnhM|h>~8cmBQSX3s0~yKFqjb z=3hG9*q=IGA!Sl&0w!!^G+Jl%yD(lF;Jgp5B}f%!9vaED;Rvr1ZKWYA8`YJz$Cyw_ z6TgaBV-jJc8Vf2BUY^W-64kYfMIgG0{IGn9<6+*#(IVP=~|EP#0h1Ez{qi! z`8lOOA9eqvkN6@q+$(2eSiM9`t_y_o-zkpIIh?@ib&WFDg|4_rD>GG>cLq^d0w_fM zZU}D%`c~Kj4vJ^MW1RC_VIY@|bXE0ry+ZmW{XMhog(`6T6xez@P%bHVj}Gz(%MMM~h?jRR=w#?_nwtHsilfGAFKe$Pm8tR^XjMDh{4dh(UpS zD?>j&e6JehT-VzSL-A?nKoMv_V&Ip9JPh}@O*2DlM*rb6F6G={TEoxoBiukQU_~N4 zP8eKxiXOhBCLRa_gP3)zQ(>n7sR| z757p@uW(3qR;<)rcIx5hft|3j0d9ype~MWz>lGo zQrySRE*$OX+RbquI^dL;Sk)@n0mPg@uX7%qIH>1_z8qM*QoAh`SJHjj4!sAY^AER7 zkM?jIFK^L~P&Lk@0RXW<4$2n+br%JN1oUs;-T|~{c!JGb9R;rK!JoM|uqgZQmGI+N06yqHyV-p~TgH-L+A=ii z)(V$a-ODTl6*TBt2M`OW&a>`)?BPXw#Ux-c&uC7z0&E!I)E#3Hc` zhMoqu-(%n2N%z9!w*AlLITe$H4j-42s=)<(%eA+7YbtdJfpZF6ox3qm8zysRITDcA zN0MJ09UQh63uLVTCkv_2cqH&i^#Ej3IGDLS!ZhA{IOUsb2slH9j5^~JR}vcK9UtB+ z;0p;qwM59_UcX}m!(dQAGqMwQ9s*SEt6RA`tij1JV4GrTmk02yyd|Wy=6Kf$u8Mi3UN5;Qt2el5$R|gL@2-l z)6CXNEKah{)_PdGFwksX{xl-0mtU;!{Cpsy3>RUUNG=B@x1oIMUO70uujbiQ44F%_ z)mZb=+pc1ELpO~qsh9Qpl7+n!f zYOTN{-&y!VPbXBx2dqVhkx=w24>urJzVl6V=%v9@o&HlI!(1H?36Y=&_>Sf;O1##Z z?i2s^(E%l8Qyy<~SMZntLsH!wzRV+|4|O!dt1q*=FOfdFSUWq1W(opwOI2))Akkvs z^RC)&n%6`WO$;|-qPCy>CVM1M$kq4>fDoXmF2J^cscJ9$2GmEmlZmdzPuC#Hr78|d z`f3p=sqcQ$sTQJYg=`VTV)hjd9x_x}SsBo`Z={ljh7KW+W7jxMn~riy+n!+{QxYxg zjHaI(z3eG35$8lwuFNL@r+72G`s&J7z&wFkX+P01+;&bJm(u>3C-gJuFEb9{b$4$T zuKD6Cd4$vutz&tm5eui9Y2I1%^{etj9mUI+dqAjIfKBR2dvqXePD)DZo35V^Gryjr zW9u8$T+1{#8*dAt;wVLJVzT=qweLei6#?HF8ws(u?Vk#NbJz@@4NoYU8WZMnL@C{! zL1BeH#qq^E5bZ&PgvGL{CTgYRnSUu9bx>{e3CG!mcDcV)wfXX8@cVb}AMVW<=lA-m zqNgbQe0{F~g;=8wB&vO2xCM43a@ktn@zxZCxDVLHOIP}&gE60Q3PccBP_}iAXz1wJ zla6H9h)lZ$FF{HxLZ@`pO4u-e3cvbeHHfCQKBXSG2&fT~f3Qj@utfjjlM{5u(GBQg-nBrU$dTU*rR zl$UtERi@tZbKtpD@Zpp1@!SvBtrrvhT`+inBRUd40?S$DdX{tMI@dFyLj|Ed=@(Vy zq~tOK5*w3p^A|mov0D7vJJ&I4rqKwI32?lRd*tvVL58+Wlv#@o}Ooi*w_=h$gr_WigF%caAX2+UJc9mHOb~YmP=ycX)=sRLf~! zh)RGu*HuZPviX8KkmY5yys)d$&KqWg)He;o)a}>XPd%G)qHA1^+$1RD9 z-cv7!KjAP30gX%4e*P_Y_?kV7pi)YB@RCeEbRiUue6PH$p(PR9DN$A5xs=WeGY-(` zbEL?a`zI-57)}+mgn%1^3ssmm&!-me!$=Kapm2qUv#!wLk!f}@J*rB@nFc&^KHmg{ zdTea0qI9VU(rhX!D)PAjU_YV<5iy6^X7+b=Lg3h;)gz-x(QT&GL!^qwqapqc@lOK3xcBj60?e#T4>1O$E2tn z+66(Fh5n&kMf|wMOWC^zf$miTCg+n~V2BL?6UQU7GDL;C@&iq3KwGkdL=v3i5g6)z zhkGHUKq-p_7P8tPDF{Z9D4!L_S8%tMVxx4LZ=K8or@p)nO^f`}$*6KYL7j;+5iyn} z4nXIDu@1Y|06X7zX7I%0 z0~DH%7%v>8hcy5U&&ebnz1D4vNQtsOk3(>J{XU6$*O0RYGd(+WH^OTnk-L!_9W)ec@Z0bF}o zs-tc`z81uFhzHJiNR3o@DpX+kAbTwdLLay|@}S0OjADNBz9G`-avdTkj2pAqOavm7 zX*CVPS5>=!;gGJQV|FnG%qvH7=AiJ27rt!)*qG{k#*<@Xv!?qmKERT&sJl?>X9ho@ zNK3`=CsKU$`Lw@iPO|t>SRA}$-0(+nKPI#xV(*%y4YR?5-SRXZM;g_c!S@kF*iYk^ zJ0}+`GhM*;L>_amdL&%w*S9JKtfYA^cV0gE5l9U#@D`Z@0j@~#wJVWq)LiLE;JEG&S>ZV>u0 z_)^kse~_G)XIeE_E^<{mQz|8F=$iX^G!6|L@C7IRC>;=I&AG!kV)t7Dq|7e_daS4C z4^BaOh#Mv*7x>P#{6k3X_VYjEAd%HGCHDQo0KziGeaL0)a+%qQB@*P=4z=Qo^Ufq* zT-Vd%@(TXd)APv)Tw!6s7_;o8z_#D(JREEhPkW!yoQT}v7Ws2b-vJl2)DB+(L0dP63@4@tRbqmu_T5IY^KcM$k$J%(V^!*^lUP96tq}Qx8q=G%P zCln>;0$hwEBfDt6DPaT{Ce87r`F2*nlRiZl8p4iwg(<+#g(dd&0~JJ4d%vVfK>MUW zIF7Tu?a1>TCU4h0OO;2OWoA?puLX`NbJ;`}P&t9fjD5NcRutKR@K>&%gU%mxab%3s>Cyy_6pV4YAax&@Dqwq$s+S{Bmg;l>?dcaEbs@J|Uf4M4>v4pK zTeF=pH^FLtOg)lxY|EnF`IU*g?8)K@DQcgt@*w4bX~J*W^xoj7!(X3*t0!bs1M6k5e7Z1Fc?W+h*6A!TH%im*g?%n+6 zb*wZw3zDn02k5;-_&!R^d^%3FAbe&`>f8E^#&buV%1{$e5Tv?&UB^B4;pao8mq`Na zoRO!Q^=b5$`x-%b1}e4jmYAl`rxMLG3k}<2sx%{!ly^q1U3(G+#SvHxZ8=w`Fmp%UtO-WQqH#AJ}z^Mbu1n@_I!4{LGOf;GhWV z3)lxrd^TAD{-K?WuA?%@mvh_0bp~?eGQ%|KmdlBC7y758T86JMCD}b`e>K8y2Nj*g zeFZJ!b(lH{6W94wR)$aDox@d?Z8*Izx@>czjzx3L2i$h4rVwTtj2B0HlaVr>~ zTdOuC*$l?&qviXuS*n@8Iy@;&q*k&SL~HAkjxzZ`L~N7_D!n5Sez0$QXff=2Kq+Je zWPp{}?0MTX_BnNc{GTht9&>dASAo%sd>^}LkFTlx%?oX5VFL^VoY!IgZ0c-_MVbMK*C^k+9)7IX zkiANw!meCw^1bn|pbi`k+Tom>>kWaY?~IVHf24yv6&5kGygZ>G29{?T14LR@oYJrk z8ah4MI)0X)S9?M8LW1Tz-eFjPhNR-5iQ^**-Y0sV0ESWRV;SJ(~>IWwgR1{LzONN%y&*L8-3vaWC z4K}xhoHb^U@p`y`k{7>x*pgcM7oMX~Nn0MRfiQbXus3BL$V>D0f`Vte8dtyqC#+Us ztZIuOn@!*I31aNgUbol&2ri!5!7xBBW@u^UFES0Wdb&$PFr5s+Ht|)y=>%2hC|jph>|^O^W(StZc#*}}2I99Vk))w0dY#>%`^0O`6Ux~`F~Q=n z;nuGlcD^Y#&cdy{+{_RDIQ9}lbqR~LzNy}ckY}<%w|7H?v-GXEgQM1-;*Z~ z!zM*XD(yT#w&aQsCH&?A@Lc58lTJm-N`q%zd7st=;vZRD9x8DxB%Hg& zZO?KRsrVrcp}lLRY4)BH&l)PNC$a#lm_-IzV_Zjit7$LT8kGYZtXFJ;p?FJc_&Qyd z{XYOU=zMl=8fO3FfJ8@TcH&Pcc zOrDmzLT4Rp_t^1Ynr!|J6~17onVbT}c@e{c8?LV5<=d3NX*4W-@`}U(qMnI~*YY)f z;LWn?n*vSk_Z_=be2R_uT?VxG$QPiK04y0;-vANC^Z_^nQsEeV@-LU5>S4Joe!*QRqBf$9 zovhf~+{N)nr#)z8pbF1}Ys>mA<`Q7z_oKx3fxo$ZC*ic(anBX2z2)N^&@+;7=B$9Y zL-}MYa2nW*>_ox&6rM0IKrh^<7`E99z6+?kFd+aF7A6FEg6l_R>%ILigdawS=TKc* zCEhZET^n4lkV1oED>Vpl72E}W5YpmEXCwxEBTcMckfaz&FEZYFq}(Y znGVGR%wtV-*;#yymQ}Pd$Fqa{CIYw>C?rKN}?8sqf3VgsS+?~9YrBN$7y}MI;bzmNXXdDSG z2U2vy>k5CIbTyYsbHg=dRcB}e~*Hd zCKwM4v%t#p9_sno2N*em4pjrBDc8e!75Rck-^Q+a6WGY@VZ9K2HKxFpA>|#rlTylA z5f*cRm0OjK{vg{%L79OxgwYD( z8>bu(HUnZ+j6SmEPPQ)R(s8|uh9vP;NmKoYU^btcwB}a&(2&X?Tlrqg>V zK~y=CHQt2I9pzQ(sF4GC^Fy1_1m4u0ooSc(Uf=qWtf!^dYs+SRRHcA#M<@+Y2;?-> zUg0n#*^>$aPtAsu9|pc%QWrVB>=jk7pw0uy^u4ue9Y91zEaL&p#{ot+A$3_-Wy3Bkb!cF$XB~ z8vR`8zZ6=l^5sOBVPHprxCVgtF?u4V27qXrug5_&epJ?*mg0de`ozaEHR>B4!L^80^w1hjU%PypToc*jV zv9%SbmX}-w)``AQjW5pEPGKFZ&LHrgx0wa_&I>&GP#&dxq4xC}nlkWh); z@)>$wSKTn-`S zPNhf?u*2Bzl3)$U^Ma*)80@HCkUAnqn#BlIh({VMF=T1u08;`)|8WLzIJn=hABmQ$ zvxBXQon=A0t(7o^VaLn!HZNg!TLRmM0?u2zDRe#`19WttsD4DBFZlR-Q*V3R@#j?h z2NH=v{~(^`xh+A6-KX}L1Kc5i`xb0^c`@2S93kTF%lB?Ui+)zHK?VsZJYZd*h1}>s zJc)s%$+u&)s?qCo+zNaD-1isl!AJBB2DX*5v)5bMNwfrW%@nnM`+JD_=&5%a?>a_D z9t7&z&#Q^1B&qbB&n!reVYMN(#VZ`unY-lXYlNANy7dsd5UZd?KdLvV4VD0T2_pxO zNP!!y$~i;gq9NtH%(Wwdx6DjUr~35rm~6Yxjm?(Sch-dFEJ3tMtjTyQe2O{nB3CQs_4WaP&P>OA1BqvLQvQ7i!-wvjo%*-eh?o`KaBT zWSvt}AMO{DY$I$}M~Cm-!H|NtdclhsPoxjF`7WqUx6Bo*K0s#+@<(nkj6ZRdu$L#~ zZHwes9Zq~(6;>~|!}EH3sWrGfwL6scLj773#Ot6}liOeLbE%cOHldo@oFr=`u|PLC zV5c@fXO`5UAUrtE{q9jaLF(;VI%nEV{ls2d<-0>$RnSs0(VBbYq!5d0Z5#D1WuOQY zMYUwV<7?`mreJ-#+5j+V(zGSKWxQ8`pyab&d~zPrz1ilpYs$Qc6Dy z6)U*-QiNbu&X_bWd0)Hm1$~s%{uqrOyTRyR>Z5OmQKB?2QBdUVyogQ(zqYUfsAs$v zt+FU<8J`)NHOI{3m7(LKI~aH5k)v=_8#)MGk+n<3)JQmA#~gR@)vXVxIGEqclv;{^ zPH_A)bPU;(SRWn2121nyr$rwva832&K+=?Gu1NTL{`Cmyj7i zYbqX6?9Fu2bx47#ng4Km1%wc8?<-O~JxVL<4jEL^I)40khF|x*8^_$H;5l`rL%W>j z#cCTA(K}H{2%qHLx|qZ66sM9fy`Kmy!0G)TW0!V|*-k(Fyo_Vd1?1X8};(LQLiWf!t;GSwr^4abY{D0SbQ03xVJUictkf7}N^jd+ApV zmG1fUiY~K&^URchxh+9oYUCuzlV9a+oAyb*;ZOR~ebG29twGoou0zXq`D-=BfgCy_}SapU-b-*h7Y|C;A@O92qDs+qu?Qi-4dY;gFg(LCE z^#iO@K-6@MWWYvN4i-TR)hSMti%$+< zM@n~M!HAnW2g?c)dL!&`>3de%Wa!6esEh+Be-F&hfCfX*i9|R6QzDYEH?^8p&chMR zc`aa90GO@xbSp;teoo;Ux(ZaRl~pV*^OG-dRvI!|f^I{i7-EhNIbE%X5`X+qSu~`kkn9bT&IF$oW_9@bL^p}h&-0{B zPli~5_4SNqCpkp@aM=PS0a2gqy9$*PVBtatyyoUL)8+&Lf%BGezdTFNCMcq}>Pc~q zeF-mj+TtT>jBfl&LvdKaM!#;%UPE-;Zufml=c@?9s`|PMyKL6*IwG|tpD)@?Eh6G* zaj|bAYYU(6-yqVLwO(+xFz_&5Y#}ZECr_SiR23f{F>JZvH=6?_UUKaSb}TEobomGD zFo5)N`+|KTPnMdh=QKh!E(=~rJ=F(BfM`w0Z|BhK#UpQmnu6@jG`{tLF?tFCN!x5J z9f4y35a#EASRL^2-v+q>{w8llf5Q*z)y*OvUNDquc^M}pC3hAJxHQH!TfPx9x83F> zB{sZ37i=>qhz7s?HNF>0;F$d47c}G29v+)L+g!3ElQ#>;daN0L0#%Yf1*g9{{>b}JX(Nw3vt@*zn_t3j zGbhBG%)<#>B*U&XuhdOTk-?FpGsQ`hfI+V^;m$8#s9Otc1P-ml(Fe=Cdnq=v8Nb}T zQ(N3qzZ;yyPC_yQC~cosCmJ&gIzEMhxCShdFGk;;7fyiOU>%#kdHI7-JEvD&ivDw{ zIBZ!oCgU39bKi)Y%PJ>+Acpx-e4&=^JIPl$RHKhU6#P)22zOciU3=|HHh~#@d5>VV zXy%Kt`KN_n?%WQX;0cGR12xsNP909HH0EW?A7AHOx~bMC83kllBvL(&I@mExCuF53PRnx&)GcYZeAZ z($KtLpi+vnu($v^7!bEizs!X8c(h%Vn8nBlNIa&q-4pE52< zHXX;C8PNU)xBVq=h<2v!Ro(oe;CNY46sdE*!8c;2-gi*5C)n!E92o`Ea6=e3xjP4DwY z>X6i(@1`B-@MGOgIyPnQZ*d7C7WU!1?U%L^%D4F)zC5aO9zsezPofCixp*I&9`9}aw2 z7((&3LlOR97caCkCjLr9m?{%~jx)Bp{=F2phwhKL9Uww3ZINKDNa)rd|FY{mD;VPD1Ttl7X^mL|5&*0IVw^my$ z8Q1)t#=kb^x+tiJ)kKT?hPkwvbX1AyLvf#h7=_#=2;B;4lv1KYnja@yOv)^SnIjPx(YQ*sALA20B$*dbD${OZrYY7MCxfHNR?3#N|4Yzh!3 zplx7YVt03 z#w}j4ImvMQgEJ44QWuH}%myeGoCwEXE=Dsmy3H!`TgI`!ov+=YV0Xtm7eLu{lL1yN zMB;#*wtTTe*F6`$K6eD>T{7bNk!yP& zxbkByznV$ywGp4)%;Tz28ZG_2yk$3FkVrLMv9_8k;}M=~7kgg!ywja4W6hWLz98_P zUoxZ&Cv@)`%+=OukHIBx}t3BYC$ zzjoHE7o(qPJ>uAOkN6M8R_xVie~&xD!DT-jT#dR5l5T`EQd#hEBsr?pR1fS&5kpFP zY4ReI=~}Qv67F(gKutlgLfs!U0+dk%@dT=IXoF3-3q7pzu^@n+LM+~0AW@6Do7siU zH9~tJrXNN zzlV`a&(dlL?0B-a0vl8U#cfp_LihUJpiu%;51w!bS*0EWCxL&zKZnAOv>P`exjqOi zhH}42sM%F-<2XC?)TM$v;RDcdt*x&CGy1m!oVJ32EE0qd6uMIq7oeW5yh(hr#Y55z zdi6t?fu6NBA`%8#8u-9J`K`PZpyOkuJa6kt@ zOa=f%M_f$Dg1|Ve8+;0gEWiL9U%I!9Ak=$n=1dht#*+ zM5zdQ4z++Qfqe^7L#g%m(_iv!a%%-1iOCWUoR)Z*&K?OY0!A^^`?)+U1SMYz5p{Im zJm5STxo4m_=T8_&8S2vCfLjdg!NxR<(0hP~yhd1t3I&yC-4j{Yu^I6(v)vRmVWRrr z@$hRn$-h{nF@1t`dKW>L2vIGBpP2I(f+8&F zusog#*eR6z@`I0qOa1OS`?zh!k$SG1QMc0fLX^B{>EX@) zTbE0d-)nMH|NEv(G{FdXpbK5FA7EuKnUeE`LsUY`2|a@;E z^*aYxhCq+fE)7sVQmbBKWub2&Vs!@B3uS{D08&K08|<$7piIa@Yc19TrPzybuN{xr7> z+gf*~gabd$L)V7I2-04NBRIaM3ZuhVon1K6fw}`LFEBh71#Y~y@VF()DlQV(+SXPC zs*dhD;otAKNLV*jaa`8GIzsZoG(=&TDSsApY*t*8Zy~i9AwhHa{TE{@BHts3fPPGL zZbl(0+i-W@s}@84<~sdp9$MpNN@Sq!i9`r5TO%F_kP$A90-isLpw3uh`_3>0{glZxfTgC$*3boQ*rVKP9a9 zS^qI5hh3a;Ovm07QQA1h&VSi7U}!3Xnj_okt*-vfX94Z5>Mo!0^;vmzL7!>*&g_|C zQ=R5>J^rfsvT~Ffp1QIM9xWUf3g(q|kerh%4EU&T#|+-~?NU}dSa+J^?f7`^=7#aZ z>I8b%^UL{qW~2HqL#v$v{6w~8OP&*Qtk0!viz_%<#%63MWi0aOY+upNR}F8=FVGf+ z49!2|Y~-1?>zKJVBS-JKvqL7gejE2}%*M(B8{$+Z`Umgboh zkMfQQZd+V=(}^y6Hxbf$?L?jO$wQBPo!I3zqX#D&nRfX+&JRBvb&p4AEO#JEvd1)< zm(b6U5G8X;!0SzT(-=2Z(9w&#s|_Mq-USHxiK*MC9~iOKwy;?-JN-j9{;UND`x3?1 z#pf5NS5NJ9w_lK*U=pZer)%p}JET?Q@+lyz;*{SZio6>RnJ?lycI4ciE5~*2_@8?T zxy0-(tPK9zL7U1tuUJ6mjBG`rST?@Vz(U{F%+TECSNG%xNrD41t;dfutL|SgN2}~F zh4Dr|Nq>+qXIN<)%zO6yP=U&m4P-nRaemxF5eeB1KYfrX;)eM`X03 z*=CQM7e~`%_U3pC3#|q)P7#bx3?^#2zfZcw-Oj0Rt|^gfU~g(XuzI^aRasP`ofuiz z{g^AOKJ64^K;ouPAs%Nctwk>HyC#SYy*GbjtH%XTT|ug1krDxR59#~6Zwl^tA~C*j zXYX{wwKoUNBWM~zsBUc6f9iWZVSsDfetA94fTmt{k5k*FAL*ZQIQ=HEq4nV<`ZKgj z;k@q)!-;Rj71ETRaK;BN7QSwr?AJ+xV^qRqwUthWDaKyeY9 zz348l1WWGs=_}8SwvXexER7E+M|oUz@(K`{)cX&86Fb~TX zWRJ0Z$5$U!;yB!OXrIJaz0HTrZ+7v;%v>ht-94VdbScW=7}p4w$b;*i-Hy>aD-ZX! zr#(>`#@!;DE;pFIHqyOKA}~O-#MEiWdkys?6HUa0#%;y2gweTt-s?LUU!T;V+ZX6~ zow^sJmHY5G-)LfWp3oyNte|IQ?L)e4yc5#j+MMSS?o0hQ-R?7LS82aUmNH*z#Q)*Hyx|Gf*4 z2Ci6H+_7F;WB#`e*|y#Y{QReDVESD<6oQA1Jt3i*_ske(0;f&?zIpw_eTz`2Ux(@5 zxr0m!e;z$qckeJV;itjTbU#0I{pf=wza6b_VP*KY!-e~{#Y4$w)C3&9vFnP!hVj>j zudm8~YapPelKD8C=UvPe6w`*!=YjwF)qst$t(l%V_&e~~o8A21I&RC)HrD&XyZq-` z_rD#2EQsq5q_0VE`D|DoH}>|Q3nX&jdVg_uY&Z~3edBWZXV162c9G{7MWNn$kp0o~ zZCqsQr|^3le;%HgcH+hu^t=Rwl^X|6ndRro`FXsV zp@E6+e=heLlgBJa=1?dOcrk7`@h;y@>)p0~poy8Tk)a^p9dn~UyG~gCle`ZD8OjA- z8=H{0zxh8+2>q>z;LL(Utp~|aRq)!_M8n3@UptHAe`~@b;bOxFA_^4;uZ>MqmTvja zS)BM=6N)uyp?=%RP-2#(fX|I2kkbYcED?66YToi{?mlW- -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/tensor.h" - -namespace { -using DataType = infini_train::DataType; -using TinyShakespeareType = TinyShakespeareDataset::TinyShakespeareType; -using TinyShakespeareFile = TinyShakespeareDataset::TinyShakespeareFile; - -const std::unordered_map kTypeMap = { - {20240520, TinyShakespeareType::kUINT16}, // GPT-2 - {20240801, TinyShakespeareType::kUINT32}, // LLaMA 3 -}; - -const std::unordered_map kTypeToSize = { - {TinyShakespeareType::kUINT16, 2}, - {TinyShakespeareType::kUINT32, 4}, -}; - -const std::unordered_map kTypeToDataType = { - {TinyShakespeareType::kUINT16, DataType::kUINT16}, - {TinyShakespeareType::kUINT32, DataType::kINT32}, -}; - -std::vector ReadSeveralBytesFromIfstream(size_t num_bytes, std::ifstream *ifs) { - std::vector result(num_bytes); - ifs->read(reinterpret_cast(result.data()), num_bytes); - return result; -} - -template T BytesToType(const std::vector &bytes, size_t offset) { - static_assert(std::is_trivially_copyable::value, "T must be trivially copyable."); - T value; - std::memcpy(&value, &bytes[offset], sizeof(T)); - return value; -} - -TinyShakespeareFile ReadTinyShakespeareFile(const std::string &path, size_t sequence_length) { - /* =================================== 作业 =================================== - TODO:实现二进制数据集文件解析 - 文件格式说明: - ---------------------------------------------------------------------------------- - | HEADER (1024 bytes) | DATA (tokens) | - | magic(4B) | version(4B) | num_toks(4B) | reserved(1012B) | token数据 | - ---------------------------------------------------------------------------------- - =================================== 作业 =================================== */ -} -} // namespace - -TinyShakespeareDataset::TinyShakespeareDataset(const std::string &filepath, size_t sequence_length) { - // =================================== 作业 =================================== - // TODO:初始化数据集实例 - // HINT: 调用ReadTinyShakespeareFile加载数据文件 - // =================================== 作业 =================================== -} - -std::pair, std::shared_ptr> -TinyShakespeareDataset::operator[](size_t idx) const { - CHECK_LT(idx, text_file_.dims[0] - 1); - std::vector dims = std::vector(text_file_.dims.begin() + 1, text_file_.dims.end()); - // x: (seq_len), y: (seq_len) -> stack -> (bs, seq_len) (bs, seq_len) - return {std::make_shared(text_file_.tensor, idx * sequence_size_in_bytes_, dims), - std::make_shared(text_file_.tensor, idx * sequence_size_in_bytes_ + sizeof(int64_t), - dims)}; -} - -size_t TinyShakespeareDataset::Size() const { return num_samples_; } +#include "example/common/tiny_shakespeare_dataset.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/tensor.h" + +namespace { +using DataType = infini_train::DataType; +using TinyShakespeareType = TinyShakespeareDataset::TinyShakespeareType; +using TinyShakespeareFile = TinyShakespeareDataset::TinyShakespeareFile; + +const std::unordered_map kTypeMap = { + {1, TinyShakespeareType::kUINT16}, // GPT-2 (legacy) + {20240520, TinyShakespeareType::kUINT16}, // GPT-2 + {20240801, TinyShakespeareType::kUINT32}, // LLaMA 3 +}; + +const std::unordered_map kTypeToSize = { + {TinyShakespeareType::kUINT16, 2}, + {TinyShakespeareType::kUINT32, 4}, +}; + +const std::unordered_map kTypeToDataType = { + {TinyShakespeareType::kUINT16, DataType::kUINT16}, + {TinyShakespeareType::kUINT32, DataType::kINT32}, +}; + +std::vector ReadSeveralBytesFromIfstream(size_t num_bytes, std::ifstream *ifs) { + std::vector result(num_bytes); + ifs->read(reinterpret_cast(result.data()), num_bytes); + return result; +} + +template T BytesToType(const std::vector &bytes, size_t offset) { + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable."); + T value; + std::memcpy(&value, &bytes[offset], sizeof(T)); + return value; +} + +TinyShakespeareFile ReadTinyShakespeareFile(const std::string &path, size_t sequence_length) { + /* =================================== 作业 =================================== + TODO:实现二进制数据集文件解析 + 文件格式说明: + ---------------------------------------------------------------------------------- + | HEADER (1024 bytes) | DATA (tokens) | + | magic(4B) | version(4B) | num_toks(4B) | reserved(1012B) | token数据 | + ---------------------------------------------------------------------------------- + =================================== 作业 =================================== */ + + std::ifstream ifs(path, std::ios::binary); + CHECK(ifs.is_open()) << "Failed to open file: " << path; + + // Read header (1024 bytes) + auto header = ReadSeveralBytesFromIfstream(1024, &ifs); + + int32_t magic = BytesToType(header, 0); + int32_t version = BytesToType(header, 4); + int32_t num_toks = BytesToType(header, 8); + + CHECK(kTypeMap.contains(version)) << "Unknown version: " << version; + TinyShakespeareType type = kTypeMap.at(version); + size_t element_size = kTypeToSize.at(type); + + // Read token data + size_t data_size = num_toks * element_size; + auto data = ReadSeveralBytesFromIfstream(data_size, &ifs); + + // Convert tokens to int64_t and reshape into (num_toks / sequence_length, sequence_length) + int64_t num_sequences = num_toks / sequence_length; + std::vector dims = {num_sequences, static_cast(sequence_length)}; + + auto tensor = infini_train::Tensor(dims, DataType::kINT64); + int64_t *tensor_ptr = static_cast(tensor.DataPtr()); + + int64_t num_elements = num_sequences * sequence_length; + for (int64_t i = 0; i < num_elements; ++i) { + if (type == TinyShakespeareType::kUINT16) { + tensor_ptr[i] = static_cast(BytesToType(data, i * element_size)); + } else { + tensor_ptr[i] = static_cast(BytesToType(data, i * element_size)); + } + } + + return {type, dims, tensor}; +} +} // namespace + +TinyShakespeareDataset::TinyShakespeareDataset(const std::string &filepath, size_t sequence_length) + : text_file_(ReadTinyShakespeareFile(filepath, sequence_length)), + sequence_length_(sequence_length), + sequence_size_in_bytes_(sequence_length * sizeof(int64_t)), + num_samples_(text_file_.dims[0] - 1) { + // =================================== 作业 =================================== + // TODO:初始化数据集实例 + // HINT: 调用ReadTinyShakespeareFile加载数据文件 + // =================================== 作业 =================================== +} + +std::pair, std::shared_ptr> +TinyShakespeareDataset::operator[](size_t idx) const { + CHECK_LT(idx, text_file_.dims[0] - 1); + std::vector dims = std::vector(text_file_.dims.begin() + 1, text_file_.dims.end()); + // x: (seq_len), y: (seq_len) -> stack -> (bs, seq_len) (bs, seq_len) + return {std::make_shared(text_file_.tensor, idx * sequence_size_in_bytes_, dims), + std::make_shared(text_file_.tensor, idx * sequence_size_in_bytes_ + sizeof(int64_t), + dims)}; +} + +size_t TinyShakespeareDataset::Size() const { return num_samples_; } diff --git a/example/common/tiny_shakespeare_dataset.h b/example/common/tiny_shakespeare_dataset.h index 8e061dd..fccbe78 100644 --- a/example/common/tiny_shakespeare_dataset.h +++ b/example/common/tiny_shakespeare_dataset.h @@ -1,43 +1,43 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "infini_train/include/dataset.h" -#include "infini_train/include/tensor.h" - -class TinyShakespeareDataset : public infini_train::Dataset { - /* - Dataset bin file is downloaded and processed using the script at - https://github.com/karpathy/llm.c/blob/master/dev/data/tinyshakespeare.py - */ -public: - enum class TinyShakespeareType : int { - kUINT16, // For GPT-2 - kUINT32, // For LLaMA 3 - kINVALID, - }; - - struct TinyShakespeareFile { - TinyShakespeareType type = TinyShakespeareType::kINVALID; - std::vector dims; - infini_train::Tensor tensor; - }; - - TinyShakespeareDataset(const std::string &filepath, size_t sequence_length); - - std::pair, std::shared_ptr> - operator[](size_t idx) const override; - - size_t Size() const override; - -private: - TinyShakespeareFile text_file_; - const size_t sequence_length_ = 0; - const size_t sequence_size_in_bytes_ = 0; - const size_t num_samples_ = 0; -}; +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "infini_train/include/dataset.h" +#include "infini_train/include/tensor.h" + +class TinyShakespeareDataset : public infini_train::Dataset { + /* + Dataset bin file is downloaded and processed using the script at + https://github.com/karpathy/llm.c/blob/master/dev/data/tinyshakespeare.py + */ +public: + enum class TinyShakespeareType : int { + kUINT16, // For GPT-2 + kUINT32, // For LLaMA 3 + kINVALID, + }; + + struct TinyShakespeareFile { + TinyShakespeareType type = TinyShakespeareType::kINVALID; + std::vector dims; + infini_train::Tensor tensor; + }; + + TinyShakespeareDataset(const std::string &filepath, size_t sequence_length); + + std::pair, std::shared_ptr> + operator[](size_t idx) const override; + + size_t Size() const override; + +private: + TinyShakespeareFile text_file_; + const size_t sequence_length_ = 0; + const size_t sequence_size_in_bytes_ = 0; + const size_t num_samples_ = 0; +}; diff --git a/example/common/tokenizer.cc b/example/common/tokenizer.cc index 23b9537..ed53250 100644 --- a/example/common/tokenizer.cc +++ b/example/common/tokenizer.cc @@ -1,117 +1,179 @@ -#include "example/common/tokenizer.h" - -#include -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -namespace infini_train { - -constexpr uint32_t kGpt2Eot = 50256; -constexpr uint32_t kLLaMA3Eot = 128001; -constexpr uint64_t kRandomU32Multiplier = 0x2545F4914F6CDD1Dull; -constexpr float kF32Divisor = 16777216.0f; // 2^24 -constexpr uint64_t kRngState = 1337; - -using Version = Tokenizer::Version; - -const std::unordered_map kEotMap = { - {20240328, kGpt2Eot}, // GPT-2 - {20240801, kLLaMA3Eot}, // LLaMA-3 -}; - -const std::unordered_map> kPromptMap = { - // e.g. "The meaning of life is" - // ref: https://tiktokenizer.vercel.app/ - {20240328, std::vector{464, 3616, 286, 1204, 318}}, // GPT-2 - {20240801, std::vector{791, 7438, 315, 2324, 374}}, // LLaMA-3 -}; - -std::vector ReadSeveralBytesFromIfstream(size_t num_bytes, std::ifstream *ifs) { - std::vector result(num_bytes); - ifs->read(reinterpret_cast(result.data()), num_bytes); - return result; -} - -template T BytesToType(const std::vector &bytes, size_t offset) { - static_assert(std::is_trivially_copyable::value, "T must be trivially copyable."); - T value; - std::memcpy(&value, &bytes[offset], sizeof(T)); - return value; -} - -unsigned int RandomU32(uint64_t &state) { - state ^= state >> 12; - state ^= state << 25; - state ^= state >> 27; - return (state * kRandomU32Multiplier) >> 32; -} - -float RandomF32(uint64_t &state) { // random float32 in [0,1) - return (RandomU32(state) >> 8) / kF32Divisor; -} - -int SampleMult(float *probabilities, int n, float coin) { - // sample index from probabilities (they must sum to 1!) - // coin is a random number in [0, 1), usually from RandomF32() - float cdf = 0.0f; - for (int i = 0; i < n; i++) { - cdf += probabilities[i]; - if (coin < cdf) { - return i; - } - } - return n - 1; // in case of rounding errors -} - -Tokenizer::Tokenizer(const std::string &filepath) { - /* ===================================== 作业 ===================================== - TODO:实现Tokenizer二进制文件加载 - - 文件格式说明: - ---------------------------------------------------------------------------------- - | HEADER (1024 bytes) | VOCAB TABLE | - | magic(4B) | version(4B) | vocab_size(4B) | reserved(1012B) | token词表数据 | - ---------------------------------------------------------------------------------- - ===================================== 作业 ===================================== */ -} - -std::string Tokenizer::Decode(uint32_t token_id) const { - /* ===================================== 作业 ===================================== - TODO:实现token_id到文本的转换 - 功能描述:根据token_id返回对应的文本片段 - ===================================== 作业 ===================================== */ - return ""; -} - -void Tokenizer::GenerateText(infini_train::nn::Module &model, uint32_t batch_size, uint32_t sequence_length, - uint32_t text_length, Device device) const { - std::vector dims; - dims.assign({batch_size, sequence_length}); - // x_tensor (FLAGS_batch_size, FLAGS_sequence_length) eq:(4, 64) - infini_train::Tensor x_tensor = infini_train::Tensor(dims, DataType::kINT64); - int64_t *x_buff = static_cast(x_tensor.DataPtr()); - for (int i = 0; i < batch_size * sequence_length; ++i) { x_buff[i] = eot_token_; } - - // Give some contexts: "The meaning of life is " - auto prompt = kPromptMap.at(magic_number_); - auto prompt_len = prompt.size(); - for (int i = 0; i < prompt_len; ++i) { x_buff[i] = prompt[i]; } - std::cout << "The meaning of life is"; - - auto x = std::make_shared(x_tensor.To(device)); - uint64_t kRngState = kRngState; - LOG(INFO) << "start generate text:"; - for (int t = prompt_len; t < text_length; t++) { - /* ===================================== 作业 ===================================== - TODO:实现单步文本生成逻辑 - HINT:调用model.Forward推理获取logits,根据推理结果进行随机采样,调用Decode获取文本结果 - ===================================== 作业 ===================================== */ - } - std::cout << std::endl; -} -} // namespace infini_train +#include "example/common/tokenizer.h" + +#include +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/nn/functional.h" + +namespace infini_train { + +constexpr uint32_t kGpt2Eot = 50256; +constexpr uint32_t kLLaMA3Eot = 128001; +constexpr uint64_t kRandomU32Multiplier = 0x2545F4914F6CDD1Dull; +constexpr float kF32Divisor = 16777216.0f; // 2^24 +constexpr uint64_t kRngState = 1337; + +using Version = Tokenizer::Version; + +const std::unordered_map kEotMap = { + {20240328, kGpt2Eot}, // GPT-2 + {20240801, kLLaMA3Eot}, // LLaMA-3 +}; + +const std::unordered_map> kPromptMap = { + // e.g. "The meaning of life is" + // ref: https://tiktokenizer.vercel.app/ + {20240328, std::vector{464, 3616, 286, 1204, 318}}, // GPT-2 + {20240801, std::vector{791, 7438, 315, 2324, 374}}, // LLaMA-3 +}; + +std::vector ReadSeveralBytesFromIfstream(size_t num_bytes, std::ifstream *ifs) { + std::vector result(num_bytes); + ifs->read(reinterpret_cast(result.data()), num_bytes); + return result; +} + +template T BytesToType(const std::vector &bytes, size_t offset) { + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable."); + T value; + std::memcpy(&value, &bytes[offset], sizeof(T)); + return value; +} + +unsigned int RandomU32(uint64_t &state) { + state ^= state >> 12; + state ^= state << 25; + state ^= state >> 27; + return (state * kRandomU32Multiplier) >> 32; +} + +float RandomF32(uint64_t &state) { // random float32 in [0,1) + return (RandomU32(state) >> 8) / kF32Divisor; +} + +int SampleMult(float *probabilities, int n, float coin) { + // sample index from probabilities (they must sum to 1!) + // coin is a random number in [0, 1), usually from RandomF32() + float cdf = 0.0f; + for (int i = 0; i < n; i++) { + cdf += probabilities[i]; + if (coin < cdf) { + return i; + } + } + return n - 1; // in case of rounding errors +} + +Tokenizer::Tokenizer(const std::string &filepath) { + /* ===================================== 作业 ===================================== + TODO:实现Tokenizer二进制文件加载 + + 文件格式说明: + ---------------------------------------------------------------------------------- + | HEADER (1024 bytes) | VOCAB TABLE | + | magic(4B) | version(4B) | vocab_size(4B) | reserved(1012B) | token词表数据 | + ---------------------------------------------------------------------------------- + ===================================== 作业 ===================================== */ + + std::ifstream ifs(filepath, std::ios::binary); + CHECK(ifs.is_open()) << "Failed to open tokenizer file: " << filepath; + + // Read header (1024 bytes) + auto header = ReadSeveralBytesFromIfstream(1024, &ifs); + + magic_number_ = BytesToType(header, 0); + uint32_t version = BytesToType(header, 4); + vocab_size_ = BytesToType(header, 8); + + CHECK(kEotMap.contains(magic_number_)) << "Unknown magic number: " << magic_number_; + eot_token_ = kEotMap.at(magic_number_); + + // Read vocab table + token_table_.resize(vocab_size_); + constexpr size_t kMaxTokenLength = 1024; + for (uint32_t i = 0; i < vocab_size_; ++i) { + auto len_bytes = ReadSeveralBytesFromIfstream(sizeof(uint8_t), &ifs); + uint32_t token_len = BytesToType(len_bytes, 0); + CHECK_LE(token_len, kMaxTokenLength) << "Token too long: " << token_len; + auto token_bytes = ReadSeveralBytesFromIfstream(token_len, &ifs); + token_table_[i] = std::string(reinterpret_cast(token_bytes.data()), token_len); + // Strip trailing whitespace + while (!token_table_[i].empty() && std::isspace(token_table_[i].back())) { + token_table_[i].pop_back(); + } + } +} + +std::string Tokenizer::Decode(uint32_t token_id) const { + /* ===================================== 作业 ===================================== + TODO:实现token_id到文本的转换 + 功能描述:根据token_id返回对应的文本片段 + ===================================== 作业 ===================================== */ + CHECK_LT(token_id, token_table_.size()) << "Token ID out of range: " << token_id; + return token_table_[token_id]; +} + +void Tokenizer::GenerateText(infini_train::nn::Module &model, uint32_t batch_size, uint32_t sequence_length, + uint32_t text_length, Device device) const { + std::vector dims; + dims.assign({batch_size, sequence_length}); + // x_tensor (FLAGS_batch_size, FLAGS_sequence_length) eq:(4, 64) + infini_train::Tensor x_tensor = infini_train::Tensor(dims, DataType::kINT64); + int64_t *x_buff = static_cast(x_tensor.DataPtr()); + for (int i = 0; i < batch_size * sequence_length; ++i) { x_buff[i] = eot_token_; } + + // Give some contexts: "The meaning of life is " + auto prompt = kPromptMap.at(magic_number_); + auto prompt_len = prompt.size(); + for (int i = 0; i < prompt_len; ++i) { x_buff[i] = prompt[i]; } + std::cout << "The meaning of life is"; + + auto x = std::make_shared(x_tensor.To(device)); + uint64_t kRngState = kRngState; + LOG(INFO) << "start generate text:"; + { + autograd::NoGradGuard no_grad; + for (int t = prompt_len; t < text_length; t++) { + /* ===================================== 作业 ===================================== + TODO:实现单步文本生成逻辑 + HINT:调用model.Forward推理获取logits,根据推理结果进行随机采样,调用Decode获取文本结果 + ===================================== 作业 ===================================== */ + + // Forward pass + auto logits = model.Forward({x})[0]; + + // Get logits for the last token position (position t-1) + auto last_logits = logits->Slice(1, t - 1, t, 1); + // Squeeze the sequence dimension: (batch_size, 1, vocab_size) -> (batch_size, vocab_size) + last_logits = last_logits->Squeeze(1); + + // Apply softmax to get probabilities + auto probs = infini_train::nn::function::Softmax(last_logits, -1); + + // Sample from the probability distribution for each batch + auto probs_cpu = probs->To(Device(DeviceType::kCPU, 0)); + float *probs_ptr = static_cast(probs_cpu.DataPtr()); + int64_t vocab_size = probs_cpu.Dims().back(); + + for (int b = 0; b < batch_size; ++b) { + float coin = RandomF32(kRngState); + int next_token = SampleMult(probs_ptr + b * vocab_size, vocab_size, coin); + x_buff[b * sequence_length + t] = next_token; + std::cout << Decode(next_token); + } + std::cout.flush(); + + // Update the device tensor with the new token + x = std::make_shared(x_tensor.To(device)); + } + } + std::cout << std::endl; +} +} // namespace infini_train diff --git a/example/common/tokenizer.h b/example/common/tokenizer.h index 1da539f..9f39c64 100644 --- a/example/common/tokenizer.h +++ b/example/common/tokenizer.h @@ -1,35 +1,35 @@ -#include -#include -#include -#include - -#include "infini_train/include/device.h" -#include "infini_train/include/nn/functional.h" -#include "infini_train/include/nn/modules/module.h" -#include "infini_train/include/tensor.h" - -namespace infini_train { - -class Tokenizer { -public: - enum class Version : uint32_t { - kV1 = 1, - kV2 = 2, - }; - - Tokenizer(const std::string &filepath); - - std::string Decode(uint32_t token_id) const; - - void GenerateText(infini_train::nn::Module &model, uint32_t batch_size, uint32_t sequence_length, - uint32_t text_length, Device device) const; - - uint32_t GetEndToken() const { return eot_token_; }; - -private: - uint32_t magic_number_ = 0; - uint32_t vocab_size_ = 0; - std::vector token_table_; - uint32_t eot_token_ = 0; -}; -} // namespace infini_train +#include +#include +#include +#include + +#include "infini_train/include/device.h" +#include "infini_train/include/nn/functional.h" +#include "infini_train/include/nn/modules/module.h" +#include "infini_train/include/tensor.h" + +namespace infini_train { + +class Tokenizer { +public: + enum class Version : uint32_t { + kV1 = 1, + kV2 = 2, + }; + + Tokenizer(const std::string &filepath); + + std::string Decode(uint32_t token_id) const; + + void GenerateText(infini_train::nn::Module &model, uint32_t batch_size, uint32_t sequence_length, + uint32_t text_length, Device device) const; + + uint32_t GetEndToken() const { return eot_token_; }; + +private: + uint32_t magic_number_ = 0; + uint32_t vocab_size_ = 0; + std::vector token_table_; + uint32_t eot_token_ = 0; +}; +} // namespace infini_train diff --git a/example/gpt2/net.cc b/example/gpt2/net.cc index 441b121..8e614ea 100644 --- a/example/gpt2/net.cc +++ b/example/gpt2/net.cc @@ -1,377 +1,386 @@ -#include "example/gpt2/net.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/device.h" -#include "infini_train/include/nn/functional.h" -#include "infini_train/include/nn/init.h" -#include "infini_train/include/nn/modules/container.h" -#include "infini_train/include/nn/modules/linear.h" -#include "infini_train/include/nn/modules/module.h" -#include "infini_train/include/nn/modules/normalization.h" -#include "infini_train/include/nn/modules/sparse.h" -#include "infini_train/include/tensor.h" - -namespace nn = infini_train::nn; - -namespace { -constexpr int kRandomSeed = 42; - -class GPT2Linear : public nn::Linear { -public: - GPT2Linear(int64_t in_features, int64_t out_features, bool residual_scale = false, bool skip_init = false) - : Linear(in_features, out_features), residual_scale_(residual_scale), skip_init_(skip_init) {} - - bool residual_scale() const { return residual_scale_; } - bool skip_init() const { return skip_init_; } - -private: - const bool residual_scale_ = false; - const bool skip_init_ = false; -}; - -static std::mt19937 gen{kRandomSeed}; -} // namespace - -std::vector> -NewGELU::Forward(const std::vector> &x) { - auto &input = x[0]; - return {0.5 * input - * (1.0 + nn::function::Tanh(std::sqrt(2.0 / M_PI) * (input + 0.044715 * nn::function::Pow(input, 3.0))))}; -} - -CausalSelfAttention::CausalSelfAttention(const GPT2Config &config) - : config_(config), n_head_(config.n_head), n_embd_(config.n_embd) { - CHECK_EQ(config.n_embd % config.n_head, 0); - modules_[kCAttnLayerName] = std::make_unique(config.n_embd, config.n_embd * 3); - modules_[kCProjLayerName] = std::make_unique(config.n_embd, config.n_embd, true, false); - // (1, 1, block_size, block_size) - bias_ = nn::function::Tril(nn::function::Ones({config_.block_size, config_.block_size})) - ->View({1, 1, config_.block_size, config_.block_size}); -} - -void CausalSelfAttention::To(infini_train::Device device) { - nn::Module::To(device); - bias_ = std::make_shared(bias_->To(device)); -} - -std::vector> -CausalSelfAttention::Forward(const std::vector> &x) { - const auto B = x[0]->Dims()[0]; // bs - const auto T = x[0]->Dims()[1]; // seq_len - const auto C = x[0]->Dims()[2]; // n_embd - - // calculate query, key, values for all heads in batch and move head forward to be the batch dim - // (bs, seq_len, n_embd) -> Linear(n_embd, 3 * n_embd) -> (bs, seq_len, 3 * n_embd) - // -> Split -> (3, bs, seq_len, n_embd) - auto qkv = modules_[kCAttnLayerName]->Forward(x)[0]->Split(n_embd_, 2); - // (bs, seq_len, n_embd) - auto q = qkv[0]; - // (bs, seq_len, n_embd) - auto k = qkv[1]; - // (bs, seq_len, n_embd) - auto v = qkv[2]; - // (bs, seq_len, n_embd) -> (bs, seq_len, n_head, n_embd / n_head) -> (bs, n_head, seq_len, n_embd / n_head) - k = k->View({B, T, n_head_, C / n_head_})->Transpose(1, 2); - // (bs, seq_len, n_embd) -> (bs, seq_len, n_head, n_embd / n_head) -> (bs, n_head, seq_len, n_embd / n_head) - q = q->View({B, T, n_head_, C / n_head_})->Transpose(1, 2); - // (bs, seq_len, n_embd) -> (bs, seq_len, n_head, n_embd / n_head) -> (bs, n_head, seq_len, n_embd / n_head) - v = v->View({B, T, n_head_, C / n_head_})->Transpose(1, 2); - - // manual implementation of attention - // this materializes the large (T,T) matrix for all the queries and keys - - // q: (bs, n_head, seq_len, n_embd / n_head) - // k: (bs, n_head, seq_len, n_embd / n_head) -> (bs, n_head, n_embd / n_head, seq_len) - // q matmul k: (bs, n_head, seq_len, seq_len) -> mul 1.0 / sqrt(n_embd / n_head) -> (bs, n_head, seq_len, seq_len) - auto att = q->Matmul(k->Transpose(-2, -1)) * (1.0 / std::sqrt(*k->Dims().rbegin())); - // (1, 1, seq_len, seq_len) - auto mask = bias_->Slice({0, 0, 0, 0}, {1, 1, T, T}, {1, 1, 1, 1}); - // (1, 1, seq_len, seq_len) -> eq 0 -> (1, 1, seq_len, seq_len) -> masked_fill -> (bs, n_head, seq_len, seq_len) - att = att->MaskedFill(mask == 0, -std::numeric_limits::infinity()); - // (bs, n_head, seq_len, seq_len) - att = nn::function::Softmax(att, -1); - // (bs, n_head, seq_len, n_embd / n_head) - auto y = att->Matmul(v); - // (bs, n_head, seq_len, n_embd / n_head) -> Transpose(1, 2) -> (bs, seq_len, n_head, n_embd / n_head) - // -> (bs, seq_len, n_embd) - y = y->Transpose(1, 2)->Contiguous()->View({B, T, C}); - // output projection - // (bs, seq_len, n_embd) -> Linear(n_embd, n_embd) -> (bs, seq_len, n_embd) - y = modules_[kCProjLayerName]->Forward({y})[0]; - // (bs, seq_len, n_embd) - return {y}; -} - -MLP::MLP(const GPT2Config &config) { - modules_[kCFclayerName] = std::make_unique(config.n_embd, config.n_embd * 4); - modules_[kGeluLayerName] = std::make_unique(); - modules_[kCProjLayerName] = std::make_unique(config.n_embd * 4, config.n_embd, true, false); -} - -std::vector> -MLP::Forward(const std::vector> &x) { - // (bs, seq_len, n_embd) -> Linear(n_embd, 4 * n_embd) -> (bs, seq_len, 4 * n_embd) - auto x1 = modules_[kCFclayerName]->Forward(x); - // (bs, seq_len, 4 * n_embd) -> GELU -> (bs, seq_len, 4 * n_embd) - auto x2 = modules_[kGeluLayerName]->Forward(x1); - // (bs, seq_len, 4 * n_embd) -> Linear(4 * n_embd, n_embd) -> (bs, seq_len, n_embd) - auto x3 = modules_[kCProjLayerName]->Forward(x2); - // (bs, seq_len, n_embd) - return x3; -} - -Block::Block(const GPT2Config &config) { - modules_[kLn1LayerName] = std::make_unique(std::vector{config.n_embd}); - modules_[kAttnLayerName] = std::make_unique(config); - modules_[kLn2LayerName] = std::make_unique(std::vector{config.n_embd}); - modules_[kMlpLayerName] = std::make_unique(config); -} - -std::vector> -Block::Forward(const std::vector> &x) { - // (bs, seq_len, n_embd) -> Layernorm -> (bs, seq_len, n_embd) -> attention -> (bs, seq_len, n_embd) - // -> Add -> (bs, seq_len, n_embd) - auto x1 = x[0] + modules_[kAttnLayerName]->Forward(modules_[kLn1LayerName]->Forward(x))[0]; - // (bs, seq_len, n_embd) -> Layernorm -> (bs, seq_len, n_embd) -> MLP -> (bs, seq_len, n_embd) - // -> Add -> (bs, seq_len, n_embd) - auto x2 = x1 + modules_[kMlpLayerName]->Forward(modules_[kLn2LayerName]->Forward({x1}))[0]; - // (bs, seq_len, n_embd) - return {x2}; -} - -GPT2::GPT2(const GPT2Config &config) : config_(config) { - { - std::unordered_map> transformer; - transformer[kWTELayerName] = std::make_unique(config.vocab_size, config.n_embd); - transformer[kWPELayerName] = std::make_unique(config.block_size, config.n_embd); - { - std::vector> h; - for (int64_t i = 0; i < config.n_layer; i++) { h.push_back(std::make_unique(config)); } - transformer[kHLayerName] = std::make_unique(std::move(h)); - } - transformer[kLnFLayerName] = std::make_unique(std::vector{config.n_embd}); - modules_[kTransformerLayerName] = std::make_unique(std::move(transformer)); - } - // don't init this one, we will tie weights - modules_[kLMHeadLayerName] = std::make_unique(config.n_embd, config.vocab_size, false, true); - // https://paperswithcode.com/method/weight-tying - *mutable_module(kTransformerLayerName) - ->mutable_module(kWTELayerName) - ->mutable_parameter(GPT2Linear::kParamWeightName) - = module(kLMHeadLayerName).parameter(GPT2Linear::kParamWeightName); - - // init all weights - Apply([&](Module *module) { - if (module->type() == nn::Linear::kType) { - auto *linear = static_cast(module); - const float std = linear->residual_scale() ? 0.02 / std::sqrt(2 * config_.n_layer) : 0.02; - if (!linear->skip_init()) { - nn::init::Normal(*linear->mutable_parameter(GPT2Linear::kParamWeightName), 0.0f, std, gen); - } - if (linear->has_parameter(nn::Linear::kParamWeightName)) { - nn::init::Zeros(*linear->mutable_parameter(nn::Linear::kParamBiasName)); - } - } else if (module->type() == nn::Embedding::kType) { - nn::init::Normal(*module->mutable_parameter(nn::Embedding::kParamWeightName), 0.0f, 0.02, gen); - } - }); -} - -std::vector> -GPT2::Forward(const std::vector> &x) { - // (bs, seq_len) - auto &idx = x[0]; - const auto device = idx->GetDevice(); - const auto t = idx->Dims()[1]; // seq_len - CHECK_LE(t, config_.block_size) << "Cannot forward sequence of length " << t << ", block size is only " - << config_.block_size; - // (seq_len) - auto pos = nn::init::Arange(0, t, infini_train::DataType::kINT64, device); - - // forward the GPT2 model itself - auto &transformer = modules_[kTransformerLayerName]; - // (bs, seq_len) -> Embedding(vocab_size, n_embd) -> (bs, seq_len, n_embd) - auto tok_emb = transformer->mutable_module(kWTELayerName)->Forward({idx})[0]; - // (seq_len) -> Embedding(block_size, n_embd) -> (seq_len, n_embd) - auto pos_emb = transformer->mutable_module(kWPELayerName)->Forward({pos})[0]; - // (bs, seq_len, n_embd) - auto x1 = tok_emb + pos_emb; - - // (bs, seq_len, n_embd) -> transformer -> (bs, seq_len, n_embd) - auto x2 = transformer->mutable_module(kHLayerName)->Forward({x1}); - // (bs, seq_len, n_embd) -> Layernorm -> (bs, seq_len, n_embd) - auto x3 = transformer->mutable_module(kLnFLayerName)->Forward(x2); - - // (bs, seq_len, n_embd) -> Linear(n_embd, vocab_size) -> (bs, seq_len, vocab_size) - auto logits = modules_[kLMHeadLayerName]->Forward(x3); - - // (bs, seq_len, vocab_size) - return logits; -} - -std::unique_ptr GPT2::FromPretrained(ModelType model_type) { - LOG(FATAL) << "Not implemented yet"; - return nullptr; -} - -namespace { -std::vector ReadSeveralBytesFromIfstream(size_t num_bytes, std::ifstream *ifs) { - std::vector result(num_bytes); - ifs->read(reinterpret_cast(result.data()), num_bytes); - return result; -} - -template T BytesToType(const std::vector &bytes, size_t offset) { - static_assert(std::is_trivially_copyable::value, "T must be trivially copyable."); - T value; - std::memcpy(&value, &bytes[offset], sizeof(T)); - return value; -} - -constexpr int32_t kHeaderMagic = 20240326; -constexpr int32_t kHeaderFP32Version = 3; -} // namespace - -std::unique_ptr GPT2::FromLLMC(const std::string &filepath) { - if (!std::filesystem::exists(filepath)) { - LOG(FATAL) << "File not found: " << filepath; - } - - std::ifstream ifs(filepath, std::ios::binary); - const auto header = ReadSeveralBytesFromIfstream(256 * sizeof(int32_t), &ifs); - - const auto magic = BytesToType(header, 0); - CHECK_EQ(magic, kHeaderMagic); - const auto version = BytesToType(header, 4); - CHECK_EQ(version, kHeaderFP32Version); - - const auto block_size = BytesToType(header, 8); - const auto vocab_size = BytesToType(header, 12); - const auto n_layer = BytesToType(header, 16); - const auto n_head = BytesToType(header, 20); - const auto n_embd = BytesToType(header, 24); - auto gpt2 = std::make_unique(GPT2Config{ - .block_size = block_size, .vocab_size = vocab_size, .n_layer = n_layer, .n_head = n_head, .n_embd = n_embd}); - - const auto padded_vocab_size = BytesToType(header, 28); - LOG(ERROR) << "magic: " << magic << " version: " << version << " block_size: " << block_size - << " vocab_size: " << vocab_size << " n_layer: " << n_layer << " n_head: " << n_head - << " n_embd: " << n_embd << " padded_vocab_size: " << padded_vocab_size; - - auto state_dict = gpt2->StateDict(); - // transformer.wte.weight - // (padded_vocab_size, n_embd) -> un_pad -> (vocab_size, n_embd) - auto &transformer_wte_weight = state_dict[std::format("{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kWTELayerName, - nn::Embedding::kParamWeightName)]; - ifs.read(reinterpret_cast(transformer_wte_weight->DataPtr()), transformer_wte_weight->SizeInBytes()); - ifs.ignore((padded_vocab_size - vocab_size) * n_embd * sizeof(float)); - // transformer.wpe.weight - auto &transformer_wpe_weight = state_dict[std::format("{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kWPELayerName, - nn::Embedding::kParamWeightName)]; - ifs.read(reinterpret_cast(transformer_wpe_weight->DataPtr()), transformer_wpe_weight->SizeInBytes()); - // transformer.h.{i}.ln_1.weight - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor - = state_dict[std::format("{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kLn1LayerName, nn::LayerNorm::kParamWeightName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.h.{i}.ln_1.bias - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor - = state_dict[std::format("{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kLn1LayerName, nn::LayerNorm::kParamBiasName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.h.{i}.attn.c_attn.weight - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kAttnLayerName, - CausalSelfAttention::kCAttnLayerName, GPT2Linear::kParamWeightName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.h.{i}.attn.c_attn.bias - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kAttnLayerName, - CausalSelfAttention::kCAttnLayerName, GPT2Linear::kParamBiasName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.h.{i}.attn.c_proj.weight - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kAttnLayerName, - CausalSelfAttention::kCProjLayerName, GPT2Linear::kParamWeightName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.h.{i}.attn.c_proj.bias - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kAttnLayerName, - CausalSelfAttention::kCProjLayerName, GPT2Linear::kParamBiasName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.h.{i}.ln_2.weight - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor - = state_dict[std::format("{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kLn2LayerName, nn::LayerNorm::kParamWeightName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.h.{i}.ln_2.bias - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor - = state_dict[std::format("{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kLn2LayerName, nn::LayerNorm::kParamBiasName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.h.{i}.mlp.c_fc.weight - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kMlpLayerName, MLP::kCFclayerName, - GPT2Linear::kParamWeightName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.h.{i}.mlp.c_fc.bias - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kMlpLayerName, MLP::kCFclayerName, - GPT2Linear::kParamBiasName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.h.{i}.mlp.c_proj.weight - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kMlpLayerName, MLP::kCProjLayerName, - GPT2Linear::kParamWeightName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.h.{i}.mlp.c_proj.bias - for (int idx = 0; idx < n_layer; idx++) { - auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, - std::to_string(idx), Block::kMlpLayerName, MLP::kCProjLayerName, - GPT2Linear::kParamBiasName)]; - ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); - } - // transformer.ln_f.weight - auto &transformer_ln_f_weight = state_dict[std::format("{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kLnFLayerName, - nn::LayerNorm::kParamWeightName)]; - ifs.read(reinterpret_cast(transformer_ln_f_weight->DataPtr()), transformer_ln_f_weight->SizeInBytes()); - // transformer.ln_f.bias - auto &transformer_ln_f_bias = state_dict[std::format("{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kLnFLayerName, - nn::LayerNorm::kParamBiasName)]; - ifs.read(reinterpret_cast(transformer_ln_f_bias->DataPtr()), transformer_ln_f_bias->SizeInBytes()); - - return gpt2; -} +#include "example/gpt2/net.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/device.h" +#include "infini_train/include/nn/functional.h" +#include "infini_train/include/nn/init.h" +#include "infini_train/include/nn/modules/container.h" +#include "infini_train/include/nn/modules/linear.h" +#include "infini_train/include/nn/modules/module.h" +#include "infini_train/include/nn/modules/normalization.h" +#include "infini_train/include/nn/modules/sparse.h" +#include "infini_train/include/tensor.h" + +namespace nn = infini_train::nn; + +namespace { +constexpr int kRandomSeed = 42; + +class GPT2Linear : public nn::Linear { +public: + GPT2Linear(int64_t in_features, int64_t out_features, bool residual_scale = false, bool skip_init = false) + : Linear(in_features, out_features), residual_scale_(residual_scale), skip_init_(skip_init) {} + + bool residual_scale() const { return residual_scale_; } + bool skip_init() const { return skip_init_; } + +private: + const bool residual_scale_ = false; + const bool skip_init_ = false; +}; + +static std::mt19937 gen{kRandomSeed}; +} // namespace + +std::vector> +NewGELU::Forward(const std::vector> &x) { + auto &input = x[0]; + return {0.5 * input + * (1.0 + nn::function::Tanh(std::sqrt(2.0 / M_PI) * (input + 0.044715 * nn::function::Pow(input, 3.0))))}; +} + +CausalSelfAttention::CausalSelfAttention(const GPT2Config &config) + : config_(config), n_head_(config.n_head), n_embd_(config.n_embd) { + CHECK_EQ(config.n_embd % config.n_head, 0); + modules_[kCAttnLayerName] = std::make_unique(config.n_embd, config.n_embd * 3); + modules_[kCProjLayerName] = std::make_unique(config.n_embd, config.n_embd, true, false); + // (1, 1, block_size, block_size) + bias_ = nn::function::Tril(nn::function::Ones({config_.block_size, config_.block_size})) + ->View({1, 1, config_.block_size, config_.block_size}); +} + +void CausalSelfAttention::To(infini_train::Device device) { + nn::Module::To(device); + bias_ = std::make_shared(bias_->To(device)); +} + +std::vector> +CausalSelfAttention::Forward(const std::vector> &x) { + const auto B = x[0]->Dims()[0]; // bs + const auto T = x[0]->Dims()[1]; // seq_len + const auto C = x[0]->Dims()[2]; // n_embd + + // calculate query, key, values for all heads in batch and move head forward to be the batch dim + // (bs, seq_len, n_embd) -> Linear(n_embd, 3 * n_embd) -> (bs, seq_len, 3 * n_embd) + // -> Split -> (3, bs, seq_len, n_embd) + auto qkv = modules_[kCAttnLayerName]->Forward(x)[0]->Split(n_embd_, 2); + // (bs, seq_len, n_embd) + auto q = qkv[0]; + // (bs, seq_len, n_embd) + auto k = qkv[1]; + // (bs, seq_len, n_embd) + auto v = qkv[2]; + // (bs, seq_len, n_embd) -> (bs, seq_len, n_head, n_embd / n_head) -> (bs, n_head, seq_len, n_embd / n_head) + k = k->View({B, T, n_head_, C / n_head_})->Transpose(1, 2); + // (bs, seq_len, n_embd) -> (bs, seq_len, n_head, n_embd / n_head) -> (bs, n_head, seq_len, n_embd / n_head) + q = q->View({B, T, n_head_, C / n_head_})->Transpose(1, 2); + // (bs, seq_len, n_embd) -> (bs, seq_len, n_head, n_embd / n_head) -> (bs, n_head, seq_len, n_embd / n_head) + v = v->View({B, T, n_head_, C / n_head_})->Transpose(1, 2); + + // manual implementation of attention + // this materializes the large (T,T) matrix for all the queries and keys + + // q: (bs, n_head, seq_len, n_embd / n_head) + // k: (bs, n_head, seq_len, n_embd / n_head) -> (bs, n_head, n_embd / n_head, seq_len) + // q matmul k: (bs, n_head, seq_len, seq_len) -> mul 1.0 / sqrt(n_embd / n_head) -> (bs, n_head, seq_len, seq_len) + auto att = q->Matmul(k->Transpose(-2, -1)) * (1.0 / std::sqrt(*k->Dims().rbegin())); + // (1, 1, seq_len, seq_len) + auto mask = bias_->Slice({0, 0, 0, 0}, {1, 1, T, T}, {1, 1, 1, 1}); + // (1, 1, seq_len, seq_len) -> eq 0 -> (1, 1, seq_len, seq_len) -> masked_fill -> (bs, n_head, seq_len, seq_len) + att = att->MaskedFill(mask == 0, -std::numeric_limits::infinity()); + // (bs, n_head, seq_len, seq_len) + att = nn::function::Softmax(att, -1); + // (bs, n_head, seq_len, n_embd / n_head) + auto y = att->Matmul(v); + // (bs, n_head, seq_len, n_embd / n_head) -> Transpose(1, 2) -> (bs, seq_len, n_head, n_embd / n_head) + // -> (bs, seq_len, n_embd) + y = y->Transpose(1, 2)->Contiguous()->View({B, T, C}); + // output projection + // (bs, seq_len, n_embd) -> Linear(n_embd, n_embd) -> (bs, seq_len, n_embd) + y = modules_[kCProjLayerName]->Forward({y})[0]; + // (bs, seq_len, n_embd) + return {y}; +} + +MLP::MLP(const GPT2Config &config) { + modules_[kCFclayerName] = std::make_unique(config.n_embd, config.n_embd * 4); + modules_[kGeluLayerName] = std::make_unique(); + modules_[kCProjLayerName] = std::make_unique(config.n_embd * 4, config.n_embd, true, false); +} + +std::vector> +MLP::Forward(const std::vector> &x) { + // (bs, seq_len, n_embd) -> Linear(n_embd, 4 * n_embd) -> (bs, seq_len, 4 * n_embd) + auto x1 = modules_[kCFclayerName]->Forward(x); + // (bs, seq_len, 4 * n_embd) -> GELU -> (bs, seq_len, 4 * n_embd) + auto x2 = modules_[kGeluLayerName]->Forward(x1); + // (bs, seq_len, 4 * n_embd) -> Linear(4 * n_embd, n_embd) -> (bs, seq_len, n_embd) + auto x3 = modules_[kCProjLayerName]->Forward(x2); + // (bs, seq_len, n_embd) + return x3; +} + +Block::Block(const GPT2Config &config) { + modules_[kLn1LayerName] = std::make_unique(std::vector{config.n_embd}); + modules_[kAttnLayerName] = std::make_unique(config); + modules_[kLn2LayerName] = std::make_unique(std::vector{config.n_embd}); + modules_[kMlpLayerName] = std::make_unique(config); +} + +std::vector> +Block::Forward(const std::vector> &x) { + // (bs, seq_len, n_embd) -> Layernorm -> (bs, seq_len, n_embd) -> attention -> (bs, seq_len, n_embd) + // -> Add -> (bs, seq_len, n_embd) + auto x1 = x[0] + modules_[kAttnLayerName]->Forward(modules_[kLn1LayerName]->Forward(x))[0]; + // (bs, seq_len, n_embd) -> Layernorm -> (bs, seq_len, n_embd) -> MLP -> (bs, seq_len, n_embd) + // -> Add -> (bs, seq_len, n_embd) + auto x2 = x1 + modules_[kMlpLayerName]->Forward(modules_[kLn2LayerName]->Forward({x1}))[0]; + // (bs, seq_len, n_embd) + return {x2}; +} + +GPT2::GPT2(const GPT2Config &config) : config_(config) { + { + std::unordered_map> transformer; + transformer[kWTELayerName] = std::make_unique(config.vocab_size, config.n_embd); + transformer[kWPELayerName] = std::make_unique(config.block_size, config.n_embd); + { + std::vector> h; + for (int64_t i = 0; i < config.n_layer; i++) { h.push_back(std::make_unique(config)); } + transformer[kHLayerName] = std::make_unique(std::move(h)); + } + transformer[kLnFLayerName] = std::make_unique(std::vector{config.n_embd}); + modules_[kTransformerLayerName] = std::make_unique(std::move(transformer)); + } + // don't init this one, we will tie weights + modules_[kLMHeadLayerName] = std::make_unique(config.n_embd, config.vocab_size, false, true); + // https://paperswithcode.com/method/weight-tying + *mutable_module(kTransformerLayerName) + ->mutable_module(kWTELayerName) + ->mutable_parameter(GPT2Linear::kParamWeightName) + = module(kLMHeadLayerName).parameter(GPT2Linear::kParamWeightName); + + // init all weights + Apply([&](Module *module) { + if (module->type() == nn::Linear::kType) { + auto *linear = static_cast(module); + const float std = linear->residual_scale() ? 0.02 / std::sqrt(2 * config_.n_layer) : 0.02; + if (!linear->skip_init()) { + nn::init::Normal(*linear->mutable_parameter(GPT2Linear::kParamWeightName), 0.0f, std, gen); + } + if (linear->has_parameter(nn::Linear::kParamWeightName)) { + nn::init::Zeros(*linear->mutable_parameter(nn::Linear::kParamBiasName)); + } + } else if (module->type() == nn::Embedding::kType) { + nn::init::Normal(*module->mutable_parameter(nn::Embedding::kParamWeightName), 0.0f, 0.02, gen); + } + }); +} + +std::vector> +GPT2::Forward(const std::vector> &x) { + // (bs, seq_len) + auto &idx = x[0]; + const auto device = idx->GetDevice(); + const auto t = idx->Dims()[1]; // seq_len + CHECK_LE(t, config_.block_size) << "Cannot forward sequence of length " << t << ", block size is only " + << config_.block_size; + // (seq_len) + auto pos = nn::init::Arange(0, t, infini_train::DataType::kINT64, device); + + // forward the GPT2 model itself + auto &transformer = modules_[kTransformerLayerName]; + // (bs, seq_len) -> Embedding(vocab_size, n_embd) -> (bs, seq_len, n_embd) + auto tok_emb = transformer->mutable_module(kWTELayerName)->Forward({idx})[0]; + // (seq_len) -> Embedding(block_size, n_embd) -> (seq_len, n_embd) + auto pos_emb = transformer->mutable_module(kWPELayerName)->Forward({pos})[0]; + // (bs, seq_len, n_embd) + auto x1 = tok_emb + pos_emb; + + // (bs, seq_len, n_embd) -> transformer -> (bs, seq_len, n_embd) + auto x2 = transformer->mutable_module(kHLayerName)->Forward({x1}); + // (bs, seq_len, n_embd) -> Layernorm -> (bs, seq_len, n_embd) + auto x3 = transformer->mutable_module(kLnFLayerName)->Forward(x2); + + // (bs, seq_len, n_embd) -> Linear(n_embd, vocab_size) -> (bs, seq_len, vocab_size) + auto logits = modules_[kLMHeadLayerName]->Forward(x3); + + // (bs, seq_len, vocab_size) + return logits; +} + +std::unique_ptr GPT2::FromPretrained(ModelType model_type) { + LOG(FATAL) << "Not implemented yet"; + return nullptr; +} + +namespace { +std::vector ReadSeveralBytesFromIfstream(size_t num_bytes, std::ifstream *ifs) { + std::vector result(num_bytes); + ifs->read(reinterpret_cast(result.data()), num_bytes); + return result; +} + +template T BytesToType(const std::vector &bytes, size_t offset) { + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable."); + T value; + std::memcpy(&value, &bytes[offset], sizeof(T)); + return value; +} + +constexpr int32_t kHeaderMagic = 20240326; +constexpr int32_t kHeaderFP32Version = 3; +} // namespace + +std::unique_ptr GPT2::FromLLMC(const std::string &filepath) { + if (!std::filesystem::exists(filepath)) { + LOG(FATAL) << "File not found: " << filepath; + } + + std::ifstream ifs(filepath, std::ios::binary); + const auto header = ReadSeveralBytesFromIfstream(256 * sizeof(int32_t), &ifs); + + const auto magic = BytesToType(header, 0); + CHECK_EQ(magic, kHeaderMagic); + const auto version = BytesToType(header, 4); + CHECK_EQ(version, kHeaderFP32Version); + + const auto block_size = BytesToType(header, 8); + const auto vocab_size = BytesToType(header, 12); + const auto n_layer = BytesToType(header, 16); + const auto n_head = BytesToType(header, 20); + const auto n_embd = BytesToType(header, 24); + auto gpt2 = std::make_unique(GPT2Config{ + .block_size = block_size, .vocab_size = vocab_size, .n_layer = n_layer, .n_head = n_head, .n_embd = n_embd}); + + const auto padded_vocab_size = BytesToType(header, 28); + + auto state_dict = gpt2->StateDict(); + // transformer.wte.weight + // (padded_vocab_size, n_embd) -> un_pad -> (vocab_size, n_embd) + auto key = std::format("{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kWTELayerName, + nn::Embedding::kParamWeightName); + auto &transformer_wte_weight = state_dict.at(key); + ifs.read(reinterpret_cast(transformer_wte_weight->DataPtr()), transformer_wte_weight->SizeInBytes()); + // Use a small stack buffer to skip the padded part instead of ifs.ignore() + { + const std::streamsize skip_size = (padded_vocab_size - vocab_size) * n_embd * sizeof(float); + char skip_buf[4096]; + std::streamsize remaining = skip_size; + while (remaining > 0) { + std::streamsize to_read = std::min(remaining, static_cast(sizeof(skip_buf))); + ifs.read(skip_buf, to_read); + remaining -= to_read; + } + } + // transformer.wpe.weight + auto &transformer_wpe_weight = state_dict[std::format("{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kWPELayerName, + nn::Embedding::kParamWeightName)]; + ifs.read(reinterpret_cast(transformer_wpe_weight->DataPtr()), transformer_wpe_weight->SizeInBytes()); + // transformer.h.{i}.ln_1.weight + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor + = state_dict[std::format("{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kLn1LayerName, nn::LayerNorm::kParamWeightName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.h.{i}.ln_1.bias + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor + = state_dict[std::format("{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kLn1LayerName, nn::LayerNorm::kParamBiasName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.h.{i}.attn.c_attn.weight + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kAttnLayerName, + CausalSelfAttention::kCAttnLayerName, GPT2Linear::kParamWeightName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.h.{i}.attn.c_attn.bias + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kAttnLayerName, + CausalSelfAttention::kCAttnLayerName, GPT2Linear::kParamBiasName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.h.{i}.attn.c_proj.weight + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kAttnLayerName, + CausalSelfAttention::kCProjLayerName, GPT2Linear::kParamWeightName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.h.{i}.attn.c_proj.bias + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kAttnLayerName, + CausalSelfAttention::kCProjLayerName, GPT2Linear::kParamBiasName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.h.{i}.ln_2.weight + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor + = state_dict[std::format("{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kLn2LayerName, nn::LayerNorm::kParamWeightName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.h.{i}.ln_2.bias + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor + = state_dict[std::format("{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kLn2LayerName, nn::LayerNorm::kParamBiasName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.h.{i}.mlp.c_fc.weight + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kMlpLayerName, MLP::kCFclayerName, + GPT2Linear::kParamWeightName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.h.{i}.mlp.c_fc.bias + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kMlpLayerName, MLP::kCFclayerName, + GPT2Linear::kParamBiasName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.h.{i}.mlp.c_proj.weight + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kMlpLayerName, MLP::kCProjLayerName, + GPT2Linear::kParamWeightName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.h.{i}.mlp.c_proj.bias + for (int idx = 0; idx < n_layer; idx++) { + auto &tensor = state_dict[std::format("{}.{}.{}.{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kHLayerName, + std::to_string(idx), Block::kMlpLayerName, MLP::kCProjLayerName, + GPT2Linear::kParamBiasName)]; + ifs.read(reinterpret_cast(tensor->DataPtr()), tensor->SizeInBytes()); + } + // transformer.ln_f.weight + auto &transformer_ln_f_weight = state_dict[std::format("{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kLnFLayerName, + nn::LayerNorm::kParamWeightName)]; + ifs.read(reinterpret_cast(transformer_ln_f_weight->DataPtr()), transformer_ln_f_weight->SizeInBytes()); + // transformer.ln_f.bias + auto &transformer_ln_f_bias = state_dict[std::format("{}.{}.{}", GPT2::kTransformerLayerName, GPT2::kLnFLayerName, + nn::LayerNorm::kParamBiasName)]; + ifs.read(reinterpret_cast(transformer_ln_f_bias->DataPtr()), transformer_ln_f_bias->SizeInBytes()); + + return gpt2; +} diff --git a/example/gpt2/net.h b/example/gpt2/net.h index 619cbdc..13cee23 100644 --- a/example/gpt2/net.h +++ b/example/gpt2/net.h @@ -1,97 +1,97 @@ -#pragma once - -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/nn/modules/module.h" -#include "infini_train/include/tensor.h" - -struct GPT2Config { - int64_t block_size = 1024; - int64_t vocab_size = 50257; - int64_t n_layer = 12; - int64_t n_head = 12; - int64_t n_embd = 768; -}; - -class NewGELU : public infini_train::nn::Module { -public: - std::vector> - Forward(const std::vector> &x) override; -}; - -class CausalSelfAttention : public infini_train::nn::Module { -public: - static constexpr char kCAttnLayerName[] = "c_attn"; - static constexpr char kCProjLayerName[] = "c_proj"; - - explicit CausalSelfAttention(const GPT2Config &config); - - std::vector> - Forward(const std::vector> &x) override; - - void To(infini_train::Device device) override; - -private: - GPT2Config config_; - int64_t n_head_ = 0; - int64_t n_embd_ = 0; - std::shared_ptr bias_ = nullptr; -}; - -class MLP : public infini_train::nn::Module { -public: - static constexpr char kCFclayerName[] = "c_fc"; - static constexpr char kGeluLayerName[] = "gelu"; - static constexpr char kCProjLayerName[] = "c_proj"; - - explicit MLP(const GPT2Config &config); - - std::vector> - Forward(const std::vector> &x) override; -}; - -class Block : public infini_train::nn::Module { -public: - static constexpr char kLn1LayerName[] = "ln_1"; - static constexpr char kAttnLayerName[] = "attn"; - static constexpr char kLn2LayerName[] = "ln_2"; - static constexpr char kMlpLayerName[] = "mlp"; - - explicit Block(const GPT2Config &config); - - std::vector> - Forward(const std::vector> &x) override; -}; - -class GPT2 : public infini_train::nn::Module { -public: - static constexpr char kWTELayerName[] = "wte"; - static constexpr char kWPELayerName[] = "wpe"; - static constexpr char kHLayerName[] = "h"; - static constexpr char kLnFLayerName[] = "ln_f"; - static constexpr char kTransformerLayerName[] = "transformer"; - static constexpr char kLMHeadLayerName[] = "lm_head"; - - enum class ModelType : int8_t { - kGPT2, - kGPT2Medium, - kGPT2Large, - kGPT2XL, - }; - - explicit GPT2(const GPT2Config &config); - - std::vector> - Forward(const std::vector> &x) override; - - static std::unique_ptr FromPretrained(ModelType model_type); - static std::unique_ptr FromLLMC(const std::string &filepath); - -private: - GPT2Config config_; -}; +#pragma once + +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/nn/modules/module.h" +#include "infini_train/include/tensor.h" + +struct GPT2Config { + int64_t block_size = 1024; + int64_t vocab_size = 50257; + int64_t n_layer = 12; + int64_t n_head = 12; + int64_t n_embd = 768; +}; + +class NewGELU : public infini_train::nn::Module { +public: + std::vector> + Forward(const std::vector> &x) override; +}; + +class CausalSelfAttention : public infini_train::nn::Module { +public: + static constexpr char kCAttnLayerName[] = "c_attn"; + static constexpr char kCProjLayerName[] = "c_proj"; + + explicit CausalSelfAttention(const GPT2Config &config); + + std::vector> + Forward(const std::vector> &x) override; + + void To(infini_train::Device device) override; + +private: + GPT2Config config_; + int64_t n_head_ = 0; + int64_t n_embd_ = 0; + std::shared_ptr bias_ = nullptr; +}; + +class MLP : public infini_train::nn::Module { +public: + static constexpr char kCFclayerName[] = "c_fc"; + static constexpr char kGeluLayerName[] = "gelu"; + static constexpr char kCProjLayerName[] = "c_proj"; + + explicit MLP(const GPT2Config &config); + + std::vector> + Forward(const std::vector> &x) override; +}; + +class Block : public infini_train::nn::Module { +public: + static constexpr char kLn1LayerName[] = "ln_1"; + static constexpr char kAttnLayerName[] = "attn"; + static constexpr char kLn2LayerName[] = "ln_2"; + static constexpr char kMlpLayerName[] = "mlp"; + + explicit Block(const GPT2Config &config); + + std::vector> + Forward(const std::vector> &x) override; +}; + +class GPT2 : public infini_train::nn::Module { +public: + static constexpr char kWTELayerName[] = "wte"; + static constexpr char kWPELayerName[] = "wpe"; + static constexpr char kHLayerName[] = "h"; + static constexpr char kLnFLayerName[] = "ln_f"; + static constexpr char kTransformerLayerName[] = "transformer"; + static constexpr char kLMHeadLayerName[] = "lm_head"; + + enum class ModelType : int8_t { + kGPT2, + kGPT2Medium, + kGPT2Large, + kGPT2XL, + }; + + explicit GPT2(const GPT2Config &config); + + std::vector> + Forward(const std::vector> &x) override; + + static std::unique_ptr FromPretrained(ModelType model_type); + static std::unique_ptr FromLLMC(const std::string &filepath); + +private: + GPT2Config config_; +}; diff --git a/infini_train/include/autograd/activations.h b/infini_train/include/autograd/activations.h index 952acc4..b7e3aae 100644 --- a/infini_train/include/autograd/activations.h +++ b/infini_train/include/autograd/activations.h @@ -1,21 +1,21 @@ -#pragma once - -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class Sigmoid : public Function { -public: - static constexpr char kType[] = "SigmoidFunction"; - - Sigmoid() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; -}; -} // namespace infini_train::autograd +#pragma once + +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class Sigmoid : public Function { +public: + static constexpr char kType[] = "SigmoidFunction"; + + Sigmoid() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/elementwise.h b/infini_train/include/autograd/elementwise.h index 9f5a3e6..8fa73e0 100644 --- a/infini_train/include/autograd/elementwise.h +++ b/infini_train/include/autograd/elementwise.h @@ -1,192 +1,192 @@ -#pragma once - -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class Neg : public Function { -public: - static constexpr char kType[] = "NegFunction"; - - Neg() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; -}; - -class Reciprocal : public Function { -public: - static constexpr char kType[] = "ReciprocalFunction"; - - Reciprocal() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; -}; - -class Sin : public Function { -public: - static constexpr char kType[] = "SinFunction"; - - Sin() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; -}; - -class Cos : public Function { -public: - static constexpr char kType[] = "CosFunction"; - - Cos() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; -}; - -class Tanh : public Function { -public: - static constexpr char kType[] = "TanhFunction"; - - Tanh() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; -}; - -class Pow : public Function { -public: - static constexpr char kType[] = "PowFunction"; - - explicit Pow(float exponent, bool scalar_is_base = false) - : Function(kType), exponent_(exponent), scalar_is_base_(scalar_is_base) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - const float exponent_ = 0.0f; - const bool scalar_is_base_ = false; -}; - -class Rsqrt : public Function { -public: - static constexpr char kType[] = "RsqrtFunction"; - - explicit Rsqrt() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; -}; - -class EqualsScalar : public Function { -public: - static constexpr char kType[] = "EqualsScalarFunction"; - - explicit EqualsScalar(float scalar) : Function(kType), scalar_(scalar) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - const float scalar_ = 0.0f; -}; - -class Add : public Function { -public: - static constexpr char kType[] = "AddFunction"; - - explicit Add() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - std::vector a_dims_; - std::vector b_dims_; -}; - -class AddScalar : public Function { -public: - static constexpr char kType[] = "AddScalarFunction"; - - explicit AddScalar(float scalar) : Function(kType), scalar_(scalar) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - const float scalar_ = 0.0f; -}; - -class Sub : public Function { -public: - static constexpr char kType[] = "SubFunction"; - - explicit Sub() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - std::vector a_dims_; - std::vector b_dims_; -}; - -class Mul : public Function { -public: - static constexpr char kType[] = "MulFunction"; - - explicit Mul() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; -}; - -class MulScalar : public Function { -public: - static constexpr char kType[] = "MulScalarFunction"; - - explicit MulScalar(float scalar) : Function(kType), scalar_(scalar) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - const float scalar_ = 0.0f; -}; - -class Div : public Function { -public: - static constexpr char kType[] = "DivFunction"; - - explicit Div() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; -}; - -} // namespace infini_train::autograd +#pragma once + +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class Neg : public Function { +public: + static constexpr char kType[] = "NegFunction"; + + Neg() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; +}; + +class Reciprocal : public Function { +public: + static constexpr char kType[] = "ReciprocalFunction"; + + Reciprocal() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; +}; + +class Sin : public Function { +public: + static constexpr char kType[] = "SinFunction"; + + Sin() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; +}; + +class Cos : public Function { +public: + static constexpr char kType[] = "CosFunction"; + + Cos() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; +}; + +class Tanh : public Function { +public: + static constexpr char kType[] = "TanhFunction"; + + Tanh() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; +}; + +class Pow : public Function { +public: + static constexpr char kType[] = "PowFunction"; + + explicit Pow(float exponent, bool scalar_is_base = false) + : Function(kType), exponent_(exponent), scalar_is_base_(scalar_is_base) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + const float exponent_ = 0.0f; + const bool scalar_is_base_ = false; +}; + +class Rsqrt : public Function { +public: + static constexpr char kType[] = "RsqrtFunction"; + + explicit Rsqrt() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; +}; + +class EqualsScalar : public Function { +public: + static constexpr char kType[] = "EqualsScalarFunction"; + + explicit EqualsScalar(float scalar) : Function(kType), scalar_(scalar) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + const float scalar_ = 0.0f; +}; + +class Add : public Function { +public: + static constexpr char kType[] = "AddFunction"; + + explicit Add() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + std::vector a_dims_; + std::vector b_dims_; +}; + +class AddScalar : public Function { +public: + static constexpr char kType[] = "AddScalarFunction"; + + explicit AddScalar(float scalar) : Function(kType), scalar_(scalar) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + const float scalar_ = 0.0f; +}; + +class Sub : public Function { +public: + static constexpr char kType[] = "SubFunction"; + + explicit Sub() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + std::vector a_dims_; + std::vector b_dims_; +}; + +class Mul : public Function { +public: + static constexpr char kType[] = "MulFunction"; + + explicit Mul() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; +}; + +class MulScalar : public Function { +public: + static constexpr char kType[] = "MulScalarFunction"; + + explicit MulScalar(float scalar) : Function(kType), scalar_(scalar) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + const float scalar_ = 0.0f; +}; + +class Div : public Function { +public: + static constexpr char kType[] = "DivFunction"; + + explicit Div() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; +}; + +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/function.h b/infini_train/include/autograd/function.h index b3bc71a..cd8645c 100644 --- a/infini_train/include/autograd/function.h +++ b/infini_train/include/autograd/function.h @@ -1,40 +1,51 @@ -#pragma once - -#include -#include -#include - -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class Function : public std::enable_shared_from_this { -public: - static constexpr char kUndefinedType[] = "Undefined"; - - Function() : type_(kUndefinedType) {} - explicit Function(const std::string &type) : type_(type) {} - - virtual ~Function() = default; - - virtual std::vector> Forward(const std::vector> &input_tensors) = 0; - virtual void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) {} - virtual std::vector> Backward(const std::vector> &grad_outputs) = 0; - - std::vector> Apply(const std::vector> &input_tensors); - virtual void BackwardPartial(const std::shared_ptr &grad_output, int idx); - - void IncreaseDependenciesNumber(); - -protected: - std::vector> saved_tensors_; - -private: - std::vector, int>> next_functions_; - int dependencies_number_ = 0; - int dependencies_reached_ = 0; - int grad_outputs_reached_ = 0; - std::vector> grad_outputs_; - const std::string type_ = kUndefinedType; -}; -} // namespace infini_train::autograd +#pragma once + +#include +#include +#include + +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { + +class NoGradGuard { +public: + NoGradGuard() { ++depth_; } + ~NoGradGuard() { --depth_; } + static bool is_enabled() { return depth_ > 0; } + +private: + static thread_local int depth_; +}; + +class Function : public std::enable_shared_from_this { +public: + static constexpr char kUndefinedType[] = "Undefined"; + + Function() : type_(kUndefinedType) {} + explicit Function(const std::string &type) : type_(type) {} + + virtual ~Function() = default; + + virtual std::vector> Forward(const std::vector> &input_tensors) = 0; + virtual void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) {} + virtual std::vector> Backward(const std::vector> &grad_outputs) = 0; + + std::vector> Apply(const std::vector> &input_tensors); + virtual void BackwardPartial(const std::shared_ptr &grad_output, int idx); + + void IncreaseDependenciesNumber(); + +protected: + std::vector> saved_tensors_; + +private: + std::vector, int>> next_functions_; + int dependencies_number_ = 0; + int dependencies_reached_ = 0; + int grad_outputs_reached_ = 0; + std::vector> grad_outputs_; + const std::string type_ = kUndefinedType; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/linear.h b/infini_train/include/autograd/linear.h index ee7dc3c..1f3431b 100644 --- a/infini_train/include/autograd/linear.h +++ b/infini_train/include/autograd/linear.h @@ -1,25 +1,25 @@ -#pragma once - -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class Linear : public Function { -public: - static constexpr char kType[] = "LinearFunction"; - - Linear() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - int64_t out_features_ = 0; - bool bias_ = true; -}; -} // namespace infini_train::autograd +#pragma once + +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class Linear : public Function { +public: + static constexpr char kType[] = "LinearFunction"; + + Linear() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + int64_t out_features_ = 0; + bool bias_ = true; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/loss.h b/infini_train/include/autograd/loss.h index 93487f3..c98751c 100644 --- a/infini_train/include/autograd/loss.h +++ b/infini_train/include/autograd/loss.h @@ -1,21 +1,21 @@ -#pragma once - -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class CrossEntropy : public Function { -public: - static constexpr char kType[] = "CrossEntropyFunction"; - - CrossEntropy() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; -}; -} // namespace infini_train::autograd +#pragma once + +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class CrossEntropy : public Function { +public: + static constexpr char kType[] = "CrossEntropyFunction"; + + CrossEntropy() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/matmul.h b/infini_train/include/autograd/matmul.h index af4ffda..d768471 100644 --- a/infini_train/include/autograd/matmul.h +++ b/infini_train/include/autograd/matmul.h @@ -1,24 +1,24 @@ -#pragma once - -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class Matmul : public Function { -public: - static constexpr char kType[] = "MatmulFunction"; - - Matmul() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - int64_t out_features_ = 0; -}; -} // namespace infini_train::autograd +#pragma once + +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class Matmul : public Function { +public: + static constexpr char kType[] = "MatmulFunction"; + + Matmul() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + int64_t out_features_ = 0; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/misc.h b/infini_train/include/autograd/misc.h index a6516d4..7857c1d 100644 --- a/infini_train/include/autograd/misc.h +++ b/infini_train/include/autograd/misc.h @@ -1,75 +1,75 @@ -#pragma once - -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class Split : public Function { -public: - static constexpr char kType[] = "SplitFunction"; - - Split(int64_t split_size, int dim = 0) : Function(kType), split_size_(split_size), dim_(dim) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - const int64_t split_size_ = 0; - const int dim_ = 0; - std::vector input_dims_; -}; - -class NoOp : public Function { -public: - static constexpr char kType[] = "NoOpFunction"; - - explicit NoOp(const std::vector &output_dims) : Function(kType), output_dims_(output_dims) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - const std::vector output_dims_; - std::vector input_dims_; -}; - -class Slice : public Function { -public: - static constexpr char kType[] = "SliceFunction"; - - Slice(const std::vector &starts, const std::vector &ends, const std::vector &steps) - : Function(kType), starts_(starts), ends_(ends), steps_(steps) {} - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - const std::vector starts_; - const std::vector ends_; - const std::vector steps_; -}; - -class Stack : public Function { -public: - static constexpr char kType[] = "StackFunction"; - - Stack(int64_t dim) : Function(kType), dim_(dim) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - int64_t dim_ = 0; - std::vector input_dims_; -}; -} // namespace infini_train::autograd +#pragma once + +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class Split : public Function { +public: + static constexpr char kType[] = "SplitFunction"; + + Split(int64_t split_size, int dim = 0) : Function(kType), split_size_(split_size), dim_(dim) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + const int64_t split_size_ = 0; + const int dim_ = 0; + std::vector input_dims_; +}; + +class NoOp : public Function { +public: + static constexpr char kType[] = "NoOpFunction"; + + explicit NoOp(const std::vector &output_dims) : Function(kType), output_dims_(output_dims) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + const std::vector output_dims_; + std::vector input_dims_; +}; + +class Slice : public Function { +public: + static constexpr char kType[] = "SliceFunction"; + + Slice(const std::vector &starts, const std::vector &ends, const std::vector &steps) + : Function(kType), starts_(starts), ends_(ends), steps_(steps) {} + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + const std::vector starts_; + const std::vector ends_; + const std::vector steps_; +}; + +class Stack : public Function { +public: + static constexpr char kType[] = "StackFunction"; + + Stack(int64_t dim) : Function(kType), dim_(dim) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + int64_t dim_ = 0; + std::vector input_dims_; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/normalization.h b/infini_train/include/autograd/normalization.h index c8432b9..6c858bb 100644 --- a/infini_train/include/autograd/normalization.h +++ b/infini_train/include/autograd/normalization.h @@ -1,24 +1,24 @@ -#pragma once - -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class LayerNorm : public Function { -public: - static constexpr char kType[] = "LayerNormFunction"; - - explicit LayerNorm(float eps) : Function(kType), eps_(eps) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - const float eps_ = 1e-5f; -}; -} // namespace infini_train::autograd +#pragma once + +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class LayerNorm : public Function { +public: + static constexpr char kType[] = "LayerNormFunction"; + + explicit LayerNorm(float eps) : Function(kType), eps_(eps) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + const float eps_ = 1e-5f; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/outer.h b/infini_train/include/autograd/outer.h index 4661f74..65899e9 100644 --- a/infini_train/include/autograd/outer.h +++ b/infini_train/include/autograd/outer.h @@ -1,21 +1,21 @@ -#pragma once - -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class Outer : public Function { -public: - static constexpr char kType[] = "OuterFunction"; - - Outer() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; -}; -} // namespace infini_train::autograd +#pragma once + +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class Outer : public Function { +public: + static constexpr char kType[] = "OuterFunction"; + + Outer() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/reduction.h b/infini_train/include/autograd/reduction.h index a7eea8e..7da6923 100644 --- a/infini_train/include/autograd/reduction.h +++ b/infini_train/include/autograd/reduction.h @@ -1,75 +1,75 @@ -#pragma once - -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class Mean : public Function { -public: - static constexpr char kType[] = "MeanFunction"; - - explicit Mean(int64_t dim, bool keep_dim = false) : Function(kType), dim_(dim), keep_dim_(keep_dim) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - std::vector input_dims_; - int64_t dim_ = 0; - bool keep_dim_ = false; -}; - -class Sum : public Function { -public: - static constexpr char kType[] = "SumFunction"; - - explicit Sum(int64_t dim, bool keep_dim = false) : Function(kType), dim_(dim), keep_dim_(keep_dim) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - std::vector input_dims_; - int64_t dim_ = 0; - bool keep_dim_ = false; -}; - -class Max : public Function { -public: - static constexpr char kType[] = "MaxFunction"; - - explicit Max(int64_t dim, bool keep_dim = false) : Function(kType), dim_(dim), keep_dim_(keep_dim) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - int64_t dim_ = 0; - bool keep_dim_ = false; -}; - -class Min : public Function { -public: - static constexpr char kType[] = "MinFunction"; - - explicit Min(int64_t dim, bool keep_dim = false) : Function(kType), dim_(dim), keep_dim_(keep_dim) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - int64_t dim_ = 0; - bool keep_dim_ = false; -}; -} // namespace infini_train::autograd +#pragma once + +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class Mean : public Function { +public: + static constexpr char kType[] = "MeanFunction"; + + explicit Mean(int64_t dim, bool keep_dim = false) : Function(kType), dim_(dim), keep_dim_(keep_dim) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + std::vector input_dims_; + int64_t dim_ = 0; + bool keep_dim_ = false; +}; + +class Sum : public Function { +public: + static constexpr char kType[] = "SumFunction"; + + explicit Sum(int64_t dim, bool keep_dim = false) : Function(kType), dim_(dim), keep_dim_(keep_dim) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + std::vector input_dims_; + int64_t dim_ = 0; + bool keep_dim_ = false; +}; + +class Max : public Function { +public: + static constexpr char kType[] = "MaxFunction"; + + explicit Max(int64_t dim, bool keep_dim = false) : Function(kType), dim_(dim), keep_dim_(keep_dim) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + int64_t dim_ = 0; + bool keep_dim_ = false; +}; + +class Min : public Function { +public: + static constexpr char kType[] = "MinFunction"; + + explicit Min(int64_t dim, bool keep_dim = false) : Function(kType), dim_(dim), keep_dim_(keep_dim) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + int64_t dim_ = 0; + bool keep_dim_ = false; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/softmax.h b/infini_train/include/autograd/softmax.h index 025a4e3..2bfe5ec 100644 --- a/infini_train/include/autograd/softmax.h +++ b/infini_train/include/autograd/softmax.h @@ -1,25 +1,25 @@ -#pragma once - -#include -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class Softmax : public Function { -public: - static constexpr char kType[] = "SoftmaxFunction"; - - explicit Softmax(int64_t dim = -1) : Function(kType), dim_(dim){}; - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - const int64_t dim_ = -1; -}; -} // namespace infini_train::autograd +#pragma once + +#include +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class Softmax : public Function { +public: + static constexpr char kType[] = "SoftmaxFunction"; + + explicit Softmax(int64_t dim = -1) : Function(kType), dim_(dim){}; + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + const int64_t dim_ = -1; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/sparse.h b/infini_train/include/autograd/sparse.h index 76c2a6b..de676db 100644 --- a/infini_train/include/autograd/sparse.h +++ b/infini_train/include/autograd/sparse.h @@ -1,24 +1,24 @@ -#pragma once - -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class Embedding : public Function { -public: - static constexpr char kType[] = "EmbeddingFunction"; - - explicit Embedding() : Function(kType) {} - - std::vector> Forward(const std::vector> &input_tensors) override; - void SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - std::vector weight_dims_; -}; -} // namespace infini_train::autograd +#pragma once + +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class Embedding : public Function { +public: + static constexpr char kType[] = "EmbeddingFunction"; + + explicit Embedding() : Function(kType) {} + + std::vector> Forward(const std::vector> &input_tensors) override; + void SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + std::vector weight_dims_; +}; +} // namespace infini_train::autograd diff --git a/infini_train/include/autograd/transform.h b/infini_train/include/autograd/transform.h index 3ef4908..5e5dbcc 100644 --- a/infini_train/include/autograd/transform.h +++ b/infini_train/include/autograd/transform.h @@ -1,78 +1,78 @@ -#pragma once - -#include -#include -#include - -#include "infini_train/include/autograd/function.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -class Tril : public Function { -public: - static constexpr char kType[] = "TrilFunction"; - - Tril(int64_t diagonal) : Function(kType), diagonal_(diagonal) {} - std::vector> Forward(const std::vector> &input_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - int64_t diagonal_ = 0; -}; - -class Triu : public Function { -public: - static constexpr char kType[] = "TriuFunction"; - - Triu(int64_t diagonal) : Function(kType), diagonal_(diagonal) {} - std::vector> Forward(const std::vector> &input_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - int64_t diagonal_ = 0; -}; - -class Transpose : public Function { -public: - static constexpr char kType[] = "TransposeFunction"; - - Transpose(int64_t dim0, int64_t dim1) : Function(kType), dim0_(dim0), dim1_(dim1) {} - std::vector> Forward(const std::vector> &input_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - int64_t dim0_ = 0; - int64_t dim1_ = 0; -}; - -class Mask : public Function { -public: - static constexpr char kType[] = "MaskFunction"; - - Mask(std::shared_ptr mask, float value) : Function(kType), mask_(mask), value_(value) {} - std::vector> Forward(const std::vector> &input_tensors) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - std::shared_ptr mask_; - float value_ = 0.f; -}; - -class RepeatInterleave : public Function { -public: - static constexpr char kType[] = "RepeatInterleaveFunction"; - - RepeatInterleave(int64_t repeat, int64_t dim) : Function(kType), repeat_(repeat), dim_(dim) {} - - std::vector> Forward(const std::vector> &inputs) override; - void SetupContext(const std::vector> &inputs, - const std::vector> &outputs) override; - std::vector> Backward(const std::vector> &grad_outputs) override; - -private: - int64_t repeat_ = 0; - int64_t dim_ = 0; - std::vector input_dims_; -}; - -} // namespace infini_train::autograd +#pragma once + +#include +#include +#include + +#include "infini_train/include/autograd/function.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +class Tril : public Function { +public: + static constexpr char kType[] = "TrilFunction"; + + Tril(int64_t diagonal) : Function(kType), diagonal_(diagonal) {} + std::vector> Forward(const std::vector> &input_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + int64_t diagonal_ = 0; +}; + +class Triu : public Function { +public: + static constexpr char kType[] = "TriuFunction"; + + Triu(int64_t diagonal) : Function(kType), diagonal_(diagonal) {} + std::vector> Forward(const std::vector> &input_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + int64_t diagonal_ = 0; +}; + +class Transpose : public Function { +public: + static constexpr char kType[] = "TransposeFunction"; + + Transpose(int64_t dim0, int64_t dim1) : Function(kType), dim0_(dim0), dim1_(dim1) {} + std::vector> Forward(const std::vector> &input_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + int64_t dim0_ = 0; + int64_t dim1_ = 0; +}; + +class Mask : public Function { +public: + static constexpr char kType[] = "MaskFunction"; + + Mask(std::shared_ptr mask, float value) : Function(kType), mask_(mask), value_(value) {} + std::vector> Forward(const std::vector> &input_tensors) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + std::shared_ptr mask_; + float value_ = 0.f; +}; + +class RepeatInterleave : public Function { +public: + static constexpr char kType[] = "RepeatInterleaveFunction"; + + RepeatInterleave(int64_t repeat, int64_t dim) : Function(kType), repeat_(repeat), dim_(dim) {} + + std::vector> Forward(const std::vector> &inputs) override; + void SetupContext(const std::vector> &inputs, + const std::vector> &outputs) override; + std::vector> Backward(const std::vector> &grad_outputs) override; + +private: + int64_t repeat_ = 0; + int64_t dim_ = 0; + std::vector input_dims_; +}; + +} // namespace infini_train::autograd diff --git a/infini_train/include/dataloader.h b/infini_train/include/dataloader.h index 5e9cd43..74fd98f 100644 --- a/infini_train/include/dataloader.h +++ b/infini_train/include/dataloader.h @@ -1,43 +1,43 @@ -#pragma once - -#include -#include -#include - -#include "infini_train/include/dataset.h" -#include "infini_train/include/tensor.h" - -namespace infini_train { -class DataLoaderIterator { -public: - DataLoaderIterator(const Dataset &dataset, size_t batch_size, size_t batch_idx, size_t max_batch_idx); - - std::pair, std::shared_ptr> operator*() const; - - DataLoaderIterator &operator++(); - DataLoaderIterator operator++(int); - - friend bool operator<(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs); - friend bool operator!=(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs); - friend bool operator==(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs); - -private: - const Dataset *dataset_ = nullptr; // not owned - size_t batch_size_ = 0; - size_t batch_idx_ = 0; - size_t max_batch_idx_ = 0; -}; - -class DataLoader { -public: - DataLoader(const std::shared_ptr &dataset, size_t batch_size); - - DataLoaderIterator begin() const; - DataLoaderIterator end() const; - -private: - std::shared_ptr dataset_; - size_t batch_size_ = 0; - size_t max_batch_idx_ = 0; -}; -} // namespace infini_train +#pragma once + +#include +#include +#include + +#include "infini_train/include/dataset.h" +#include "infini_train/include/tensor.h" + +namespace infini_train { +class DataLoaderIterator { +public: + DataLoaderIterator(const Dataset &dataset, size_t batch_size, size_t batch_idx, size_t max_batch_idx); + + std::pair, std::shared_ptr> operator*() const; + + DataLoaderIterator &operator++(); + DataLoaderIterator operator++(int); + + friend bool operator<(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs); + friend bool operator!=(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs); + friend bool operator==(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs); + +private: + const Dataset *dataset_ = nullptr; // not owned + size_t batch_size_ = 0; + size_t batch_idx_ = 0; + size_t max_batch_idx_ = 0; +}; + +class DataLoader { +public: + DataLoader(const std::shared_ptr &dataset, size_t batch_size); + + DataLoaderIterator begin() const; + DataLoaderIterator end() const; + +private: + std::shared_ptr dataset_; + size_t batch_size_ = 0; + size_t max_batch_idx_ = 0; +}; +} // namespace infini_train diff --git a/infini_train/include/dataset.h b/infini_train/include/dataset.h index 8769f45..a1ab248 100644 --- a/infini_train/include/dataset.h +++ b/infini_train/include/dataset.h @@ -1,17 +1,17 @@ -#pragma once - -#include -#include -#include - -#include "infini_train/include/tensor.h" - -namespace infini_train { -class Dataset { -public: - virtual ~Dataset() = default; - - virtual std::pair, std::shared_ptr> operator[](size_t idx) const = 0; - virtual size_t Size() const = 0; -}; -} // namespace infini_train +#pragma once + +#include +#include +#include + +#include "infini_train/include/tensor.h" + +namespace infini_train { +class Dataset { +public: + virtual ~Dataset() = default; + + virtual std::pair, std::shared_ptr> operator[](size_t idx) const = 0; + virtual size_t Size() const = 0; +}; +} // namespace infini_train diff --git a/infini_train/include/device.h b/infini_train/include/device.h index ebc49e3..ed464ff 100644 --- a/infini_train/include/device.h +++ b/infini_train/include/device.h @@ -1,38 +1,38 @@ -#pragma once - -#include - -#include "glog/logging.h" - -namespace infini_train { -enum class DeviceType : int8_t { - kCPU = 0, - kCUDA = 1, -}; - -class Device { -public: - Device(); - - Device(DeviceType type, int8_t index); - - bool operator==(const Device &other) const; - - bool operator!=(const Device &other) const; - - DeviceType Type() const; - int8_t Index() const; - - bool IsCPU() const; - bool IsCUDA() const; - - std::string ToString() const; - - friend std::ostream &operator<<(std::ostream &os, const Device &device); - -private: - DeviceType type_; - int8_t index_; -}; - -} // namespace infini_train +#pragma once + +#include + +#include "glog/logging.h" + +namespace infini_train { +enum class DeviceType : int8_t { + kCPU = 0, + kCUDA = 1, +}; + +class Device { +public: + Device(); + + Device(DeviceType type, int8_t index); + + bool operator==(const Device &other) const; + + bool operator!=(const Device &other) const; + + DeviceType Type() const; + int8_t Index() const; + + bool IsCPU() const; + bool IsCUDA() const; + + std::string ToString() const; + + friend std::ostream &operator<<(std::ostream &os, const Device &device); + +private: + DeviceType type_; + int8_t index_; +}; + +} // namespace infini_train diff --git a/infini_train/include/dispatcher.h b/infini_train/include/dispatcher.h index 5b91d85..ac85eeb 100644 --- a/infini_train/include/dispatcher.h +++ b/infini_train/include/dispatcher.h @@ -1,62 +1,66 @@ -#pragma once - -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/device.h" - -namespace infini_train { -class KernelFunction { -public: - template explicit KernelFunction(FuncT &&func) : func_ptr_(reinterpret_cast(func)) {} - - template RetT Call(ArgsT... args) const { - // =================================== 作业 =================================== - // TODO:实现通用kernel调用接口 - // 功能描述:将存储的函数指针转换为指定类型并调用 - // =================================== 作业 =================================== - - using FuncT = RetT (*)(ArgsT...); - // TODO: 实现函数调用逻辑 - } - -private: - void *func_ptr_ = nullptr; -}; - -class Dispatcher { -public: - using KeyT = std::pair; - - static Dispatcher &Instance() { - static Dispatcher instance; - return instance; - } - - const KernelFunction &GetKernel(KeyT key) const { - CHECK(key_to_kernel_map_.contains(key)) - << "Kernel not found: " << key.second << " on device: " << static_cast(key.first); - return key_to_kernel_map_.at(key); - } - - template void Register(const KeyT &key, FuncT &&kernel) { - // =================================== 作业 =================================== - // TODO:实现kernel注册机制 - // 功能描述:将kernel函数与设备类型、名称绑定 - // =================================== 作业 =================================== - } - -private: - std::map key_to_kernel_map_; -}; -} // namespace infini_train - -#define REGISTER_KERNEL(device, kernel_name, kernel_func) \ - // =================================== 作业 =================================== - // TODO:实现自动注册宏 - // 功能描述:在全局静态区注册kernel,避免显式初始化代码 - // =================================== 作业 =================================== +#pragma once + +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/device.h" + +namespace infini_train { +class KernelFunction { +public: + template explicit KernelFunction(FuncT &&func) : func_ptr_(reinterpret_cast(func)) {} + + template RetT Call(ArgsT... args) const { + // =================================== 作业 =================================== + // TODO:实现通用kernel调用接口 + // 功能描述:将存储的函数指针转换为指定类型并调用 + // =================================== 作业 =================================== + + using FuncT = RetT (*)(ArgsT...); + auto func = reinterpret_cast(func_ptr_); + return func(std::forward(args)...); + } + +private: + void *func_ptr_ = nullptr; +}; + +class Dispatcher { +public: + using KeyT = std::pair; + + static Dispatcher &Instance() { + static Dispatcher instance; + return instance; + } + + const KernelFunction &GetKernel(KeyT key) const { + CHECK(key_to_kernel_map_.contains(key)) + << "Kernel not found: " << key.second << " on device: " << static_cast(key.first); + return key_to_kernel_map_.at(key); + } + + template void Register(const KeyT &key, FuncT &&kernel) { + // =================================== 作业 =================================== + // TODO:实现kernel注册机制 + // 功能描述:将kernel函数与设备类型、名称绑定 + // =================================== 作业 =================================== + CHECK(!key_to_kernel_map_.contains(key)) << "Kernel already registered: " << key.second + << " on device: " << static_cast(key.first); + key_to_kernel_map_.emplace(key, KernelFunction(std::forward(kernel))); + } + +private: + std::map key_to_kernel_map_; +}; +} // namespace infini_train + +#define REGISTER_KERNEL(device, kernel_name, kernel_func) \ + static auto _register_##kernel_name##_##__LINE__ = []() { \ + infini_train::Dispatcher::Instance().Register({device, #kernel_name}, kernel_func); \ + return 0; \ + }(); diff --git a/infini_train/include/nn/functional.h b/infini_train/include/nn/functional.h index 7713a40..f4ebf83 100644 --- a/infini_train/include/nn/functional.h +++ b/infini_train/include/nn/functional.h @@ -1,170 +1,170 @@ -#pragma once - -#include -#include -#include - -#include "infini_train/include/tensor.h" - -namespace infini_train::nn::function { - -// Returns the lower triangular part of a 2D tensor or a batch of matrices. -// -// The lower triangular part includes elements on and below the specified -// diagonal. Elements above the diagonal are set to zero. -// -// Args: -// input: The input tensor. -// diagonal: Diagonal offset (default 0). Positive means above the main diagonal, -// negative means below. -// -// Returns: -// A tensor with the same shape as input, with upper-triangular values zeroed. -std::shared_ptr Tril(const std::shared_ptr &input, int64_t diagonal = 0); - -// Returns the upper triangular part of a 2D tensor or a batch of matrices. -// -// The upper triangular part includes elements on and above the specified -// diagonal. Elements below the diagonal are set to zero. -// -// Args: -// input: The input tensor. -// diagonal: Diagonal offset (default 0). Positive means above the main diagonal, -// negative means below. -// -// Returns: -// A tensor with the same shape as input, with lower-triangular values zeroed. -std::shared_ptr Triu(const std::shared_ptr &input, int64_t diagonal = 0); - -// Returns a tensor filled with ones of the specified shape. -// -// Args: -// size: A vector specifying the shape of the output tensor. -// -// Returns: -// A tensor of the given shape filled with the scalar value 1. -std::shared_ptr Ones(const std::vector size); - -// Returns a new tensor with the reciprocal of the elements of input. -// -// Args: -// input: The input tensor. -// -// Returns: -// A tensor containing reciprocal applied element-wise to the input. -std::shared_ptr Reciprocal(const std::shared_ptr &input); - -// Returns a new tensor with the sine of each element in the input. -// -// Args: -// input: The input tensor. -// -// Returns: -// A tensor containing sin applied element-wise to the input. -std::shared_ptr Sin(const std::shared_ptr &input); - -// Returns a new tensor with the cosine of each element in the input. -// -// Args: -// input: The input tensor. -// -// Returns: -// A tensor containing cos applied element-wise to the input. -std::shared_ptr Cos(const std::shared_ptr &input); - -// Returns a new tensor with the hyperbolic tangent of each element in the input. -// -// Args: -// input: The input tensor. -// -// Returns: -// A tensor containing tanh applied element-wise to the input. -std::shared_ptr Tanh(const std::shared_ptr &input); - -// Raises each element of the input tensor to the specified power. -// -// Args: -// input: The input tensor. -// exponent: The exponent to apply to each element. -// -// Returns: -// A tensor with each element raised to the given exponent. -std::shared_ptr Pow(const std::shared_ptr &input, float exponent); - -// Raises the specified base to the power of each element in the input tensor. -// -// Args: -// base: The scalar base value. -// input: The input tensor providing the exponents. -// -// Returns: -// A tensor where each element is computed as base raised to the power of the corresponding input element. -std::shared_ptr Pow(float base, const std::shared_ptr &input); - -// Returns a new tensor with reciprocal of the square-root of each element in the input. -// -// Args: -// input: The input tensor. -// -// Returns: -// A tensor containing reciprocal square-root applied element-wise to the input. -std::shared_ptr Rsqrt(const std::shared_ptr &input); - -// Returns the mean of all elements in the input tensor. -// -// Args: -// input: The input tensor. -// dim: The dimension to reduce. -// keep_dim: Whether the output tensor has dim retained or not (default false). -// -// Returns: -// A new tensor with the mean values computed along the specified dimension. -std::shared_ptr Mean(const std::shared_ptr &input, int64_t dim, bool keep_dim = false); - -// Returns a new tensor with the sigmoid of each element in the input. -// -// The sigmoid function is defined as 1 / (1 + exp(-x)). -// -// Args: -// input: The input tensor. -// -// Returns: -// A tensor containing sigmoid applied element-wise to the input. -std::shared_ptr Sigmoid(const std::shared_ptr &input); - -// Applies the softmax function along the specified dimension. -// -// The softmax function maps input values to the range [0, 1] and ensures they sum to 1. -// -// Args: -// input: The input tensor. -// dim: The dimension along which softmax is computed (default -1). -// -// Returns: -// A tensor with softmax applied along the specified dimension. -std::shared_ptr Softmax(const std::shared_ptr &input, int64_t dim = -1); - -// Returns a slice of the input tensor defined by start, end, and step per dimension. -// -// Args: -// input: The input tensor. -// starts: Start indices for each dimension. -// ends: End indices for each dimension (exclusive). -// steps: Step sizes for each dimension. -// -// Returns: -// A sliced view of the input tensor. -std::shared_ptr Slice(const std::shared_ptr &input, const std::vector &starts, - const std::vector &ends, const std::vector &steps); - -// Concatenates a sequence of tensors along a new dimension. -// -// Args: -// inputs: The sequence of tensors to concatenate. All tensors need to be of the same size. -// dim: dimension to insert (defualt 0). -// -// Returns: -// Concatenation of the input tensors. -std::shared_ptr Stack(const std::vector> &inputs, int64_t dim = 0); - -} // namespace infini_train::nn::function +#pragma once + +#include +#include +#include + +#include "infini_train/include/tensor.h" + +namespace infini_train::nn::function { + +// Returns the lower triangular part of a 2D tensor or a batch of matrices. +// +// The lower triangular part includes elements on and below the specified +// diagonal. Elements above the diagonal are set to zero. +// +// Args: +// input: The input tensor. +// diagonal: Diagonal offset (default 0). Positive means above the main diagonal, +// negative means below. +// +// Returns: +// A tensor with the same shape as input, with upper-triangular values zeroed. +std::shared_ptr Tril(const std::shared_ptr &input, int64_t diagonal = 0); + +// Returns the upper triangular part of a 2D tensor or a batch of matrices. +// +// The upper triangular part includes elements on and above the specified +// diagonal. Elements below the diagonal are set to zero. +// +// Args: +// input: The input tensor. +// diagonal: Diagonal offset (default 0). Positive means above the main diagonal, +// negative means below. +// +// Returns: +// A tensor with the same shape as input, with lower-triangular values zeroed. +std::shared_ptr Triu(const std::shared_ptr &input, int64_t diagonal = 0); + +// Returns a tensor filled with ones of the specified shape. +// +// Args: +// size: A vector specifying the shape of the output tensor. +// +// Returns: +// A tensor of the given shape filled with the scalar value 1. +std::shared_ptr Ones(const std::vector size); + +// Returns a new tensor with the reciprocal of the elements of input. +// +// Args: +// input: The input tensor. +// +// Returns: +// A tensor containing reciprocal applied element-wise to the input. +std::shared_ptr Reciprocal(const std::shared_ptr &input); + +// Returns a new tensor with the sine of each element in the input. +// +// Args: +// input: The input tensor. +// +// Returns: +// A tensor containing sin applied element-wise to the input. +std::shared_ptr Sin(const std::shared_ptr &input); + +// Returns a new tensor with the cosine of each element in the input. +// +// Args: +// input: The input tensor. +// +// Returns: +// A tensor containing cos applied element-wise to the input. +std::shared_ptr Cos(const std::shared_ptr &input); + +// Returns a new tensor with the hyperbolic tangent of each element in the input. +// +// Args: +// input: The input tensor. +// +// Returns: +// A tensor containing tanh applied element-wise to the input. +std::shared_ptr Tanh(const std::shared_ptr &input); + +// Raises each element of the input tensor to the specified power. +// +// Args: +// input: The input tensor. +// exponent: The exponent to apply to each element. +// +// Returns: +// A tensor with each element raised to the given exponent. +std::shared_ptr Pow(const std::shared_ptr &input, float exponent); + +// Raises the specified base to the power of each element in the input tensor. +// +// Args: +// base: The scalar base value. +// input: The input tensor providing the exponents. +// +// Returns: +// A tensor where each element is computed as base raised to the power of the corresponding input element. +std::shared_ptr Pow(float base, const std::shared_ptr &input); + +// Returns a new tensor with reciprocal of the square-root of each element in the input. +// +// Args: +// input: The input tensor. +// +// Returns: +// A tensor containing reciprocal square-root applied element-wise to the input. +std::shared_ptr Rsqrt(const std::shared_ptr &input); + +// Returns the mean of all elements in the input tensor. +// +// Args: +// input: The input tensor. +// dim: The dimension to reduce. +// keep_dim: Whether the output tensor has dim retained or not (default false). +// +// Returns: +// A new tensor with the mean values computed along the specified dimension. +std::shared_ptr Mean(const std::shared_ptr &input, int64_t dim, bool keep_dim = false); + +// Returns a new tensor with the sigmoid of each element in the input. +// +// The sigmoid function is defined as 1 / (1 + exp(-x)). +// +// Args: +// input: The input tensor. +// +// Returns: +// A tensor containing sigmoid applied element-wise to the input. +std::shared_ptr Sigmoid(const std::shared_ptr &input); + +// Applies the softmax function along the specified dimension. +// +// The softmax function maps input values to the range [0, 1] and ensures they sum to 1. +// +// Args: +// input: The input tensor. +// dim: The dimension along which softmax is computed (default -1). +// +// Returns: +// A tensor with softmax applied along the specified dimension. +std::shared_ptr Softmax(const std::shared_ptr &input, int64_t dim = -1); + +// Returns a slice of the input tensor defined by start, end, and step per dimension. +// +// Args: +// input: The input tensor. +// starts: Start indices for each dimension. +// ends: End indices for each dimension (exclusive). +// steps: Step sizes for each dimension. +// +// Returns: +// A sliced view of the input tensor. +std::shared_ptr Slice(const std::shared_ptr &input, const std::vector &starts, + const std::vector &ends, const std::vector &steps); + +// Concatenates a sequence of tensors along a new dimension. +// +// Args: +// inputs: The sequence of tensors to concatenate. All tensors need to be of the same size. +// dim: dimension to insert (defualt 0). +// +// Returns: +// Concatenation of the input tensors. +std::shared_ptr Stack(const std::vector> &inputs, int64_t dim = 0); + +} // namespace infini_train::nn::function diff --git a/infini_train/include/nn/init.h b/infini_train/include/nn/init.h index 169aba5..948b820 100644 --- a/infini_train/include/nn/init.h +++ b/infini_train/include/nn/init.h @@ -1,50 +1,50 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "infini_train/include/tensor.h" - -namespace infini_train::nn::init { -std::shared_ptr Normal(const std::shared_ptr &tensor, float mean = 0.0, float std = 1.0, - std::optional generator = std::nullopt); - -std::pair CalculateFanInAndFanOut(const std::shared_ptr &tensor); - -enum class KaimingMode : int8_t { - kFanIn, - kFanOut, -}; - -enum class NonLinearityType : int8_t { - kLinear, - kConv1D, - kConv2D, - kConv3D, - kConvTransposed1d, - kConvTransposed2d, - kConvTransposed3d, - kSigmoid, - kTanh, - kReLU, - kLeakyReLU, - kSELU, -}; - -std::shared_ptr KaimingUniform(const std::shared_ptr &tensor, float a = 0.0f, - KaimingMode mode = KaimingMode::kFanIn, - NonLinearityType non_linearity = NonLinearityType::kLeakyReLU, - std::optional generator = std::nullopt); - -std::shared_ptr Uniform(const std::shared_ptr &tensor, float a = 0.0f, float b = 1.0f, - std::optional generator = std::nullopt); - -std::shared_ptr Ones(const std::shared_ptr &tensor); - -std::shared_ptr Zeros(const std::shared_ptr &tensor); - -std::shared_ptr Arange(int64_t start, int64_t end, DataType dtype, Device device = Device()); -} // namespace infini_train::nn::init +#pragma once + +#include +#include +#include +#include +#include + +#include "infini_train/include/tensor.h" + +namespace infini_train::nn::init { +std::shared_ptr Normal(const std::shared_ptr &tensor, float mean = 0.0, float std = 1.0, + std::optional generator = std::nullopt); + +std::pair CalculateFanInAndFanOut(const std::shared_ptr &tensor); + +enum class KaimingMode : int8_t { + kFanIn, + kFanOut, +}; + +enum class NonLinearityType : int8_t { + kLinear, + kConv1D, + kConv2D, + kConv3D, + kConvTransposed1d, + kConvTransposed2d, + kConvTransposed3d, + kSigmoid, + kTanh, + kReLU, + kLeakyReLU, + kSELU, +}; + +std::shared_ptr KaimingUniform(const std::shared_ptr &tensor, float a = 0.0f, + KaimingMode mode = KaimingMode::kFanIn, + NonLinearityType non_linearity = NonLinearityType::kLeakyReLU, + std::optional generator = std::nullopt); + +std::shared_ptr Uniform(const std::shared_ptr &tensor, float a = 0.0f, float b = 1.0f, + std::optional generator = std::nullopt); + +std::shared_ptr Ones(const std::shared_ptr &tensor); + +std::shared_ptr Zeros(const std::shared_ptr &tensor); + +std::shared_ptr Arange(int64_t start, int64_t end, DataType dtype, Device device = Device()); +} // namespace infini_train::nn::init diff --git a/infini_train/include/nn/modules/activations.h b/infini_train/include/nn/modules/activations.h index e6f65ae..f887ceb 100644 --- a/infini_train/include/nn/modules/activations.h +++ b/infini_train/include/nn/modules/activations.h @@ -1,15 +1,15 @@ -#pragma once - -#include -#include - -#include "infini_train/include/nn/modules/module.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -class Sigmoid : public Module { -public: - Sigmoid() = default; - std::vector> Forward(const std::vector> &input_tensors) override; -}; -} // namespace infini_train::nn +#pragma once + +#include +#include + +#include "infini_train/include/nn/modules/module.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +class Sigmoid : public Module { +public: + Sigmoid() = default; + std::vector> Forward(const std::vector> &input_tensors) override; +}; +} // namespace infini_train::nn diff --git a/infini_train/include/nn/modules/container.h b/infini_train/include/nn/modules/container.h index fe8697c..7b774f2 100644 --- a/infini_train/include/nn/modules/container.h +++ b/infini_train/include/nn/modules/container.h @@ -1,24 +1,24 @@ -#pragma once - -#include -#include -#include - -#include "infini_train/include/nn/modules/module.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -class Sequential : public Module { -public: - explicit Sequential(std::vector> &&layers); - - std::vector> Forward(const std::vector> &input_tensors) override; -}; - -class ModuleDict : public Module { -public: - explicit ModuleDict(std::unordered_map> &&modules); - - std::vector> Forward(const std::vector> &input_tensors) override; -}; -} // namespace infini_train::nn +#pragma once + +#include +#include +#include + +#include "infini_train/include/nn/modules/module.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +class Sequential : public Module { +public: + explicit Sequential(std::vector> &&layers); + + std::vector> Forward(const std::vector> &input_tensors) override; +}; + +class ModuleDict : public Module { +public: + explicit ModuleDict(std::unordered_map> &&modules); + + std::vector> Forward(const std::vector> &input_tensors) override; +}; +} // namespace infini_train::nn diff --git a/infini_train/include/nn/modules/linear.h b/infini_train/include/nn/modules/linear.h index 500ae71..b86d3bc 100644 --- a/infini_train/include/nn/modules/linear.h +++ b/infini_train/include/nn/modules/linear.h @@ -1,25 +1,25 @@ -#pragma once - -#include -#include - -#include "infini_train/include/device.h" -#include "infini_train/include/nn/modules/module.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -class Linear : public Module { -public: - static constexpr char kType[] = "Linear"; - - static constexpr char kParamWeightName[] = "weight"; - static constexpr char kParamBiasName[] = "bias"; - - Linear(int64_t in_features, int64_t out_features, bool bias = true, Device device = Device()); - std::vector> Forward(const std::vector> &input_tensors) override; - -private: - void ResetParameters(); - bool bias_ = true; -}; -} // namespace infini_train::nn +#pragma once + +#include +#include + +#include "infini_train/include/device.h" +#include "infini_train/include/nn/modules/module.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +class Linear : public Module { +public: + static constexpr char kType[] = "Linear"; + + static constexpr char kParamWeightName[] = "weight"; + static constexpr char kParamBiasName[] = "bias"; + + Linear(int64_t in_features, int64_t out_features, bool bias = true, Device device = Device()); + std::vector> Forward(const std::vector> &input_tensors) override; + +private: + void ResetParameters(); + bool bias_ = true; +}; +} // namespace infini_train::nn diff --git a/infini_train/include/nn/modules/loss.h b/infini_train/include/nn/modules/loss.h index 0b4a831..e4ab912 100644 --- a/infini_train/include/nn/modules/loss.h +++ b/infini_train/include/nn/modules/loss.h @@ -1,16 +1,16 @@ -#pragma once - -#include -#include - -#include "infini_train/include/nn/modules/module.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -class CrossEntropyLoss : public Module { -public: - CrossEntropyLoss() = default; - - std::vector> Forward(const std::vector> &input_tensors) override; -}; -} // namespace infini_train::nn +#pragma once + +#include +#include + +#include "infini_train/include/nn/modules/module.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +class CrossEntropyLoss : public Module { +public: + CrossEntropyLoss() = default; + + std::vector> Forward(const std::vector> &input_tensors) override; +}; +} // namespace infini_train::nn diff --git a/infini_train/include/nn/modules/module.h b/infini_train/include/nn/modules/module.h index 3722bd7..e3b2c59 100644 --- a/infini_train/include/nn/modules/module.h +++ b/infini_train/include/nn/modules/module.h @@ -1,46 +1,46 @@ -#pragma once - -#include -#include -#include -#include - -#include "infini_train/include/device.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -class Module { -public: - static constexpr char kUndefinedType[] = "Undefined"; - - Module() : type_(kUndefinedType) {} - explicit Module(const std::string &type) : type_(type) {} - - virtual ~Module(){}; - - const std::string &type() const; - - std::vector> Parameters() const; - bool has_parameter(const std::string &name) const; - std::shared_ptr *mutable_parameter(const std::string &name); - const std::shared_ptr ¶meter(const std::string &name) const; - - std::vector modules() const; - Module *mutable_module(const std::string &name); - const Module &module(const std::string &name) const; - - std::unordered_map> StateDict() const; - - virtual std::vector> Forward(const std::vector> &input_tensors) = 0; - - virtual void To(Device device); - - void Apply(std::function fn); - -protected: - Device device_; // CPU by default - const std::string type_ = kUndefinedType; - std::unordered_map> modules_; - std::unordered_map> parameters_; -}; -} // namespace infini_train::nn +#pragma once + +#include +#include +#include +#include + +#include "infini_train/include/device.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +class Module { +public: + static constexpr char kUndefinedType[] = "Undefined"; + + Module() : type_(kUndefinedType) {} + explicit Module(const std::string &type) : type_(type) {} + + virtual ~Module(){}; + + const std::string &type() const; + + std::vector> Parameters() const; + bool has_parameter(const std::string &name) const; + std::shared_ptr *mutable_parameter(const std::string &name); + const std::shared_ptr ¶meter(const std::string &name) const; + + std::vector modules() const; + Module *mutable_module(const std::string &name); + const Module &module(const std::string &name) const; + + std::unordered_map> StateDict() const; + + virtual std::vector> Forward(const std::vector> &input_tensors) = 0; + + virtual void To(Device device); + + void Apply(std::function fn); + +protected: + Device device_; // CPU by default + const std::string type_ = kUndefinedType; + std::unordered_map> modules_; + std::unordered_map> parameters_; +}; +} // namespace infini_train::nn diff --git a/infini_train/include/nn/modules/normalization.h b/infini_train/include/nn/modules/normalization.h index cf536c6..3279d8c 100644 --- a/infini_train/include/nn/modules/normalization.h +++ b/infini_train/include/nn/modules/normalization.h @@ -1,24 +1,24 @@ -#pragma once - -#include -#include - -#include "infini_train/include/device.h" -#include "infini_train/include/nn/modules/module.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -class LayerNorm : public Module { -public: - static constexpr char kParamWeightName[] = "weight"; - static constexpr char kParamBiasName[] = "bias"; - - LayerNorm(const std::vector &normalized_shape, float eps = 1e-5f, Device device = Device()); - std::vector> Forward(const std::vector> &input_tensors) override; - -private: - void ResetParameters(); - - const float eps_ = 1e-5f; -}; -} // namespace infini_train::nn +#pragma once + +#include +#include + +#include "infini_train/include/device.h" +#include "infini_train/include/nn/modules/module.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +class LayerNorm : public Module { +public: + static constexpr char kParamWeightName[] = "weight"; + static constexpr char kParamBiasName[] = "bias"; + + LayerNorm(const std::vector &normalized_shape, float eps = 1e-5f, Device device = Device()); + std::vector> Forward(const std::vector> &input_tensors) override; + +private: + void ResetParameters(); + + const float eps_ = 1e-5f; +}; +} // namespace infini_train::nn diff --git a/infini_train/include/nn/modules/sparse.h b/infini_train/include/nn/modules/sparse.h index ea9b654..98ffd5c 100644 --- a/infini_train/include/nn/modules/sparse.h +++ b/infini_train/include/nn/modules/sparse.h @@ -1,22 +1,22 @@ -#pragma once - -#include -#include - -#include "infini_train/include/nn/modules/module.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -class Embedding : public Module { -public: - static constexpr char kType[] = "Embedding"; - - static constexpr char kParamWeightName[] = "weight"; - - Embedding(int num_embeddings, int embedding_dim, Device device = Device()); - std::vector> Forward(const std::vector> &input_tensors) override; - -private: - void ResetParameters(); -}; -} // namespace infini_train::nn +#pragma once + +#include +#include + +#include "infini_train/include/nn/modules/module.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +class Embedding : public Module { +public: + static constexpr char kType[] = "Embedding"; + + static constexpr char kParamWeightName[] = "weight"; + + Embedding(int num_embeddings, int embedding_dim, Device device = Device()); + std::vector> Forward(const std::vector> &input_tensors) override; + +private: + void ResetParameters(); +}; +} // namespace infini_train::nn diff --git a/infini_train/include/optimizer.h b/infini_train/include/optimizer.h index 241aeb9..2712bcc 100644 --- a/infini_train/include/optimizer.h +++ b/infini_train/include/optimizer.h @@ -1,49 +1,49 @@ -#pragma once - -#include -#include - -#include "infini_train/include/tensor.h" - -namespace infini_train { -class Optimizer { -public: - explicit Optimizer(const std::vector> ¶ms); - - void ZeroGrad(); - - virtual void Step() = 0; - -protected: - std::vector> params_; -}; - -namespace optimizers { -class SGD : public Optimizer { -public: - SGD(const std::vector> ¶ms, float learning_rate); - - void Step() override; - -private: - const float learning_rate_ = 0.0; -}; - -class Adam : public Optimizer { -public: - Adam(const std::vector> ¶ms, float learning_rate = 1e-3, float beta1 = 0.9, - float beta2 = 0.999, float eps = 1e-8); - - void Step() override; - -private: - int64_t t_; - const float learning_rate_; - const float beta1_; - const float beta2_; - const float eps_; - std::vector> m_; - std::vector> v_; -}; -} // namespace optimizers -} // namespace infini_train +#pragma once + +#include +#include + +#include "infini_train/include/tensor.h" + +namespace infini_train { +class Optimizer { +public: + explicit Optimizer(const std::vector> ¶ms); + + void ZeroGrad(); + + virtual void Step() = 0; + +protected: + std::vector> params_; +}; + +namespace optimizers { +class SGD : public Optimizer { +public: + SGD(const std::vector> ¶ms, float learning_rate); + + void Step() override; + +private: + const float learning_rate_ = 0.0; +}; + +class Adam : public Optimizer { +public: + Adam(const std::vector> ¶ms, float learning_rate = 1e-3, float beta1 = 0.9, + float beta2 = 0.999, float eps = 1e-8); + + void Step() override; + +private: + int64_t t_; + const float learning_rate_; + const float beta1_; + const float beta2_; + const float eps_; + std::vector> m_; + std::vector> v_; +}; +} // namespace optimizers +} // namespace infini_train diff --git a/infini_train/include/tensor.h b/infini_train/include/tensor.h index a6479de..9398de4 100644 --- a/infini_train/include/tensor.h +++ b/infini_train/include/tensor.h @@ -1,198 +1,198 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "Eigen/Dense" -#include "glog/logging.h" - -#include "infini_train/include/device.h" - -namespace infini_train { -namespace autograd { -class Function; -} - -namespace { -struct PrintOptions { - // ref: https://pytorch.org/docs/stable/generated/torch.set_printoptions.html#torch.set_printoptions - int64_t precision = 4; - int64_t threshold = 1000; - int64_t edge_items = 3; - int64_t linewidth = 80; - std::optional sci_mode; - - static PrintOptions &Get() { - static PrintOptions instance; - return instance; - } -}; -} // namespace - -enum class DataType : int8_t { - kUINT8, - kINT8, - kUINT16, - kINT16, - kUINT32, - kINT32, - kUINT64, - kINT64, - kBFLOAT16, - kFLOAT16, - kFLOAT32, - kFLOAT64, -}; - -class TensorBuffer { -public: - TensorBuffer(Device device, size_t size); - ~TensorBuffer(); - - void *DataPtr(); - const void *DataPtr() const; - - Device GetDevice() const; - size_t Size() const; - -private: - Device device_; - size_t size_ = 0; - void *data_ = nullptr; -}; - -class Tensor : public std::enable_shared_from_this { -public: - Tensor() = default; - - Tensor(const std::vector &dims, DataType dtype, Device device); - Tensor(const std::vector &dims, DataType dtype) : Tensor(dims, dtype, Device(DeviceType::kCPU, 0)) {} - Tensor(const Tensor &tensor, size_t offset, const std::vector &dims); - - Device GetDevice() const; - - void *DataPtr(); - const void *DataPtr() const; - - size_t SizeInBytes() const; - - const std::vector &Dims() const; - size_t NumElements() const; - DataType Dtype() const; - - template void Fill(T value); - - Eigen::Map> EigenMatrix(); - Eigen::Map> EigenVector(); - - Tensor To(Device device); - - // operator overloading - std::shared_ptr Equals(float scalar); - std::shared_ptr Add(const std::shared_ptr &other); - std::shared_ptr Add(float scalar); - std::shared_ptr Sub(const std::shared_ptr &other); - std::shared_ptr Mul(const std::shared_ptr &other); - std::shared_ptr Mul(float scalar); - std::shared_ptr Div(const std::shared_ptr &other); - std::shared_ptr Neg(); - std::shared_ptr Reciprocal(); - std::shared_ptr Sin(); - std::shared_ptr Cos(); - std::shared_ptr Tanh(); - std::shared_ptr Pow(float exponent); - std::shared_ptr Rsqrt(); - - std::vector> Split(int split_size, int dim = 0); - std::shared_ptr Transpose(int dim0, int dim1); - std::shared_ptr Slice(const std::vector &starts, const std::vector &ends, - const std::vector &steps); - std::shared_ptr Slice(int64_t dim, int64_t start, int64_t end, int64_t step = 1); - - std::shared_ptr RepeatInterleave(int64_t repeat, int64_t dim); - - std::shared_ptr View(const std::vector &dims); - std::shared_ptr Contiguous(); - std::shared_ptr Flatten(int64_t start = 0, int64_t end = -1); - std::shared_ptr Squeeze(int64_t dim); - - // distribution - std::shared_ptr Uniform(float from = 0.0f, float to = 1.0f, - std::optional generator = std::nullopt); - - std::shared_ptr Matmul(const std::shared_ptr &other); - std::shared_ptr Outer(const std::shared_ptr &other); - std::shared_ptr MaskedFill(const std::shared_ptr &mask, float value); - - friend std::shared_ptr operator==(const std::shared_ptr &t, float scalar); - friend std::shared_ptr operator+(const std::shared_ptr &t1, const std::shared_ptr &t2); - friend std::shared_ptr operator+(float scalar, const std::shared_ptr &t); - friend std::shared_ptr operator+(const std::shared_ptr &t, float scalar); - friend std::shared_ptr operator-(const std::shared_ptr &t1, const std::shared_ptr &t2); - friend std::shared_ptr operator-(float scalar, const std::shared_ptr &t); - friend std::shared_ptr operator-(const std::shared_ptr &t, float scalar); - friend std::shared_ptr operator-(const std::shared_ptr &t); - friend std::shared_ptr operator*(const std::shared_ptr &t1, const std::shared_ptr &t2); - friend std::shared_ptr operator*(float scalar, const std::shared_ptr &t); - friend std::shared_ptr operator*(const std::shared_ptr &t, float scalar); - friend std::shared_ptr operator/(const std::shared_ptr &t1, const std::shared_ptr &t2); - friend std::shared_ptr operator/(float scalar, const std::shared_ptr &t); - friend std::shared_ptr operator/(const std::shared_ptr &t, float scalar); - - friend std::ostream &operator<<(std::ostream &os, const Tensor &tensor); - - void SaveAsNpy(const std::string &path) const; - void Print(std::ostream &os = std::cout) const; - static void - SetPrintOptions(std::optional precision = std::nullopt, std::optional threshold = std::nullopt, - std::optional edge_items = std::nullopt, std::optional linewidth = std::nullopt, - std::optional profile = std::nullopt, std::optional sci_mode = std::nullopt); - -private: - std::shared_ptr buffer_; - size_t offset_ = 0; - std::vector dims_; - size_t num_elements_ = 0; - DataType dtype_; - - // autograd related -public: - std::shared_ptr RequiresGrad(); - - std::shared_ptr grad() const { return grad_; }; - bool requires_grad() const { return requires_grad_; } - void set_requires_grad(bool requires_grad) { requires_grad_ = requires_grad; } - - bool is_leaf() const { return is_leaf_; } - void set_is_leaf(bool is_leaf) { is_leaf_ = is_leaf; } - - std::shared_ptr grad_fn() const { return grad_fn_; } - void set_grad_fn(std::shared_ptr grad_fn) { grad_fn_ = grad_fn; } - - int output_idx() const { return output_idx_; } - void set_output_idx(int output_idx) { output_idx_ = output_idx; } - - void ZeroGrad(); - - void Backward(std::shared_ptr gradient = nullptr, bool retain_graph = false, - bool create_graph = false) const; - -private: - std::shared_ptr grad_ = nullptr; - bool requires_grad_ = false; - bool is_leaf_ = true; - std::shared_ptr grad_fn_ = nullptr; - int output_idx_ = -1; -}; - -std::shared_ptr operator==(const std::shared_ptr &t, float scalar); -std::shared_ptr operator+(const std::shared_ptr &t1, const std::shared_ptr &t2); -std::shared_ptr operator+(float scalar, const std::shared_ptr &t); -std::shared_ptr operator*(const std::shared_ptr &t1, const std::shared_ptr &t2); -std::shared_ptr operator*(float scalar, const std::shared_ptr &t); -std::shared_ptr operator*(const std::shared_ptr &t, float scalar); -} // namespace infini_train +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Eigen/Dense" +#include "glog/logging.h" + +#include "infini_train/include/device.h" + +namespace infini_train { +namespace autograd { +class Function; +} + +namespace { +struct PrintOptions { + // ref: https://pytorch.org/docs/stable/generated/torch.set_printoptions.html#torch.set_printoptions + int64_t precision = 4; + int64_t threshold = 1000; + int64_t edge_items = 3; + int64_t linewidth = 80; + std::optional sci_mode; + + static PrintOptions &Get() { + static PrintOptions instance; + return instance; + } +}; +} // namespace + +enum class DataType : int8_t { + kUINT8, + kINT8, + kUINT16, + kINT16, + kUINT32, + kINT32, + kUINT64, + kINT64, + kBFLOAT16, + kFLOAT16, + kFLOAT32, + kFLOAT64, +}; + +class TensorBuffer { +public: + TensorBuffer(Device device, size_t size); + ~TensorBuffer(); + + void *DataPtr(); + const void *DataPtr() const; + + Device GetDevice() const; + size_t Size() const; + +private: + Device device_; + size_t size_ = 0; + void *data_ = nullptr; +}; + +class Tensor : public std::enable_shared_from_this { +public: + Tensor() = default; + + Tensor(const std::vector &dims, DataType dtype, Device device); + Tensor(const std::vector &dims, DataType dtype) : Tensor(dims, dtype, Device(DeviceType::kCPU, 0)) {} + Tensor(const Tensor &tensor, size_t offset, const std::vector &dims); + + Device GetDevice() const; + + void *DataPtr(); + const void *DataPtr() const; + + size_t SizeInBytes() const; + + const std::vector &Dims() const; + size_t NumElements() const; + DataType Dtype() const; + + template void Fill(T value); + + Eigen::Map> EigenMatrix(); + Eigen::Map> EigenVector(); + + Tensor To(Device device); + + // operator overloading + std::shared_ptr Equals(float scalar); + std::shared_ptr Add(const std::shared_ptr &other); + std::shared_ptr Add(float scalar); + std::shared_ptr Sub(const std::shared_ptr &other); + std::shared_ptr Mul(const std::shared_ptr &other); + std::shared_ptr Mul(float scalar); + std::shared_ptr Div(const std::shared_ptr &other); + std::shared_ptr Neg(); + std::shared_ptr Reciprocal(); + std::shared_ptr Sin(); + std::shared_ptr Cos(); + std::shared_ptr Tanh(); + std::shared_ptr Pow(float exponent); + std::shared_ptr Rsqrt(); + + std::vector> Split(int split_size, int dim = 0); + std::shared_ptr Transpose(int dim0, int dim1); + std::shared_ptr Slice(const std::vector &starts, const std::vector &ends, + const std::vector &steps); + std::shared_ptr Slice(int64_t dim, int64_t start, int64_t end, int64_t step = 1); + + std::shared_ptr RepeatInterleave(int64_t repeat, int64_t dim); + + std::shared_ptr View(const std::vector &dims); + std::shared_ptr Contiguous(); + std::shared_ptr Flatten(int64_t start = 0, int64_t end = -1); + std::shared_ptr Squeeze(int64_t dim); + + // distribution + std::shared_ptr Uniform(float from = 0.0f, float to = 1.0f, + std::optional generator = std::nullopt); + + std::shared_ptr Matmul(const std::shared_ptr &other); + std::shared_ptr Outer(const std::shared_ptr &other); + std::shared_ptr MaskedFill(const std::shared_ptr &mask, float value); + + friend std::shared_ptr operator==(const std::shared_ptr &t, float scalar); + friend std::shared_ptr operator+(const std::shared_ptr &t1, const std::shared_ptr &t2); + friend std::shared_ptr operator+(float scalar, const std::shared_ptr &t); + friend std::shared_ptr operator+(const std::shared_ptr &t, float scalar); + friend std::shared_ptr operator-(const std::shared_ptr &t1, const std::shared_ptr &t2); + friend std::shared_ptr operator-(float scalar, const std::shared_ptr &t); + friend std::shared_ptr operator-(const std::shared_ptr &t, float scalar); + friend std::shared_ptr operator-(const std::shared_ptr &t); + friend std::shared_ptr operator*(const std::shared_ptr &t1, const std::shared_ptr &t2); + friend std::shared_ptr operator*(float scalar, const std::shared_ptr &t); + friend std::shared_ptr operator*(const std::shared_ptr &t, float scalar); + friend std::shared_ptr operator/(const std::shared_ptr &t1, const std::shared_ptr &t2); + friend std::shared_ptr operator/(float scalar, const std::shared_ptr &t); + friend std::shared_ptr operator/(const std::shared_ptr &t, float scalar); + + friend std::ostream &operator<<(std::ostream &os, const Tensor &tensor); + + void SaveAsNpy(const std::string &path) const; + void Print(std::ostream &os = std::cout) const; + static void + SetPrintOptions(std::optional precision = std::nullopt, std::optional threshold = std::nullopt, + std::optional edge_items = std::nullopt, std::optional linewidth = std::nullopt, + std::optional profile = std::nullopt, std::optional sci_mode = std::nullopt); + +private: + std::shared_ptr buffer_; + size_t offset_ = 0; + std::vector dims_; + size_t num_elements_ = 0; + DataType dtype_; + + // autograd related +public: + std::shared_ptr RequiresGrad(); + + std::shared_ptr grad() const { return grad_; }; + bool requires_grad() const { return requires_grad_; } + void set_requires_grad(bool requires_grad) { requires_grad_ = requires_grad; } + + bool is_leaf() const { return is_leaf_; } + void set_is_leaf(bool is_leaf) { is_leaf_ = is_leaf; } + + std::shared_ptr grad_fn() const { return grad_fn_; } + void set_grad_fn(std::shared_ptr grad_fn) { grad_fn_ = grad_fn; } + + int output_idx() const { return output_idx_; } + void set_output_idx(int output_idx) { output_idx_ = output_idx; } + + void ZeroGrad(); + + void Backward(std::shared_ptr gradient = nullptr, bool retain_graph = false, + bool create_graph = false) const; + +private: + std::shared_ptr grad_ = nullptr; + bool requires_grad_ = false; + bool is_leaf_ = true; + std::shared_ptr grad_fn_ = nullptr; + int output_idx_ = -1; +}; + +std::shared_ptr operator==(const std::shared_ptr &t, float scalar); +std::shared_ptr operator+(const std::shared_ptr &t1, const std::shared_ptr &t2); +std::shared_ptr operator+(float scalar, const std::shared_ptr &t); +std::shared_ptr operator*(const std::shared_ptr &t1, const std::shared_ptr &t2); +std::shared_ptr operator*(float scalar, const std::shared_ptr &t); +std::shared_ptr operator*(const std::shared_ptr &t, float scalar); +} // namespace infini_train diff --git a/infini_train/src/autograd/activations.cc b/infini_train/src/autograd/activations.cc index 7fdb4d2..c4283dc 100644 --- a/infini_train/src/autograd/activations.cc +++ b/infini_train/src/autograd/activations.cc @@ -1,34 +1,34 @@ -#include "infini_train/include/autograd/activations.h" - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -std::vector> Sigmoid::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SigmoidForward"}); - return {kernel.Call>(input)}; -} - -void Sigmoid::SetupContext(const std::vector> &, - const std::vector> &output_tensors) { - const auto &output = output_tensors[0]; - saved_tensors_ = {output}; -} - -std::vector> Sigmoid::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 1); - const auto &output = saved_tensors_[0]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SigmoidBackward"}); - return {kernel.Call>(output, grad_output)}; -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/activations.h" + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +std::vector> Sigmoid::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SigmoidForward"}); + return {kernel.Call>(input)}; +} + +void Sigmoid::SetupContext(const std::vector> &, + const std::vector> &output_tensors) { + const auto &output = output_tensors[0]; + saved_tensors_ = {output}; +} + +std::vector> Sigmoid::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 1); + const auto &output = saved_tensors_[0]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SigmoidBackward"}); + return {kernel.Call>(output, grad_output)}; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/elementwise.cc b/infini_train/src/autograd/elementwise.cc index 5a790a5..e82e3d0 100644 --- a/infini_train/src/autograd/elementwise.cc +++ b/infini_train/src/autograd/elementwise.cc @@ -1,345 +1,355 @@ -#include "infini_train/include/autograd/elementwise.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" -#include - -namespace infini_train::autograd { -std::vector> Neg::Forward(const std::vector> &input_tensors) { - // =================================== 作业 =================================== - // TODO:通过Dispatcher获取设备专属kernel,对输入张量进行取反操作 - // NOTES: 依赖test_dispatcher,Neg kernel实现已给出 - // =================================== 作业 =================================== - - return std::vector>(); -} - -std::vector> Neg::Backward(const std::vector> &grad_outputs) { - // =================================== 作业 =================================== - // TODO:通过Dispatcher获取设备专属的反向传播kernel,计算梯度 - // NOTES: 依赖test_dispatcher,Neg的kernel实现已给出 - // =================================== 作业 =================================== - - return std::vector>(); -} - -std::vector> Reciprocal::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "ReciprocalForward"}); - return {kernel.Call>(input)}; -} - -void Reciprocal::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &input = input_tensors[0]; - saved_tensors_ = {input}; -} - -std::vector> Reciprocal::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 1); - const auto &input = saved_tensors_[0]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "ReciprocalBackward"}); - return {kernel.Call>(grad_output, input)}; -} - -std::vector> Sin::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SinForward"}); - return {kernel.Call>(input)}; -} - -void Sin::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &input = input_tensors[0]; - saved_tensors_ = {input}; -} - -std::vector> Sin::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 1); - const auto &input = saved_tensors_[0]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SinBackward"}); - return {kernel.Call>(grad_output, input)}; -} - -std::vector> Cos::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "CosForward"}); - return {kernel.Call>(input)}; -} - -void Cos::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &input = input_tensors[0]; - saved_tensors_ = {input}; -} - -std::vector> Cos::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 1); - const auto &input = saved_tensors_[0]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "CosBackward"}); - return {kernel.Call>(grad_output, input)}; -} - -std::vector> Tanh::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "TanhForward"}); - return {kernel.Call>(input)}; -} - -void Tanh::SetupContext(const std::vector> &, - const std::vector> &output_tensors) { - const auto &output = output_tensors[0]; - saved_tensors_ = {output}; -} - -std::vector> Tanh::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 1); - const auto &output = saved_tensors_[0]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "TanhBackward"}); - return {kernel.Call>(grad_output, output)}; -} - -std::vector> Pow::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "PowForward"}); - return {kernel.Call>(input, exponent_, scalar_is_base_)}; -} - -void Pow::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &input = input_tensors[0]; - saved_tensors_ = {input}; -} - -std::vector> Pow::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 1); - const auto &input = saved_tensors_[0]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "PowBackward"}); - return {kernel.Call>(grad_output, input, exponent_, scalar_is_base_)}; -} - -std::vector> Rsqrt::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "RsqrtForward"}); - return {kernel.Call>(input)}; -} - -void Rsqrt::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &input = input_tensors[0]; - saved_tensors_ = {input}; -} - -std::vector> Rsqrt::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 1); - const auto &input = saved_tensors_[0]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "RsqrtBackward"}); - return {kernel.Call>(grad_output, input)}; -} - -std::vector> EqualsScalar::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "EqualsScalarForward"}); - return {kernel.Call>(input, scalar_)}; -} - -std::vector> EqualsScalar::Backward(const std::vector> &grad_outputs) { - LOG(FATAL) << "EqualsScalar::Backward shall not be called anytime"; - return {}; -} - -std::vector> Add::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 2); - const auto &a = input_tensors[0]; - const auto &b = input_tensors[1]; - - auto device = a->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "AddForward"}); - return {kernel.Call>(a, b)}; -} - -void Add::SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) { - a_dims_ = input_tensors[0]->Dims(); - b_dims_ = input_tensors[1]->Dims(); -} - -std::vector> Add::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "AddBackward"}); - auto [grad_a, grad_b] - = kernel.Call, std::shared_ptr>>(grad_output, a_dims_, b_dims_); - return {grad_a, grad_b}; -} - -std::vector> AddScalar::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "AddScalarForward"}); - return {kernel.Call>(input, scalar_)}; -} - -std::vector> AddScalar::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "AddScalarBackward"}); - return {kernel.Call>(grad_output)}; -} - -std::vector> Sub::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 2); - const auto &a = input_tensors[0]; - const auto &b = input_tensors[1]; - - auto device = a->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SubForward"}); - return {kernel.Call>(a, b)}; -} - -void Sub::SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) { - a_dims_ = input_tensors[0]->Dims(); - b_dims_ = input_tensors[1]->Dims(); -} - -std::vector> Sub::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SubBackward"}); - auto [grad_a, grad_b] - = kernel.Call, std::shared_ptr>>(grad_output, a_dims_, b_dims_); - return {grad_a, grad_b}; -} - -std::vector> Mul::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 2); - const auto &a = input_tensors[0]; - const auto &b = input_tensors[1]; - - auto device = a->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MulForward"}); - return {kernel.Call>(a, b)}; -} - -void Mul::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &a = input_tensors[0]; - const auto &b = input_tensors[1]; - saved_tensors_ = {a, b}; -} - -std::vector> Mul::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 2); - const auto &a = saved_tensors_[0]; - const auto &b = saved_tensors_[1]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MulBackward"}); - auto [grad_a, grad_b] = kernel.Call, std::shared_ptr>>(grad_output, a, b); - return {grad_a, grad_b}; -} - -std::vector> MulScalar::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MulScalarForward"}); - return {kernel.Call>(input, scalar_)}; -} - -std::vector> MulScalar::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MulScalarBackward"}); - return {kernel.Call>(grad_output, scalar_)}; -} - -std::vector> Div::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 2); - const auto &a = input_tensors[0]; - const auto &b = input_tensors[1]; - - auto device = a->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "DivForward"}); - return {kernel.Call>(a, b)}; -} - -void Div::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &a = input_tensors[0]; - const auto &b = input_tensors[1]; - saved_tensors_ = {a, b}; -} - -std::vector> Div::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 2); - const auto &a = saved_tensors_[0]; - const auto &b = saved_tensors_[1]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "DivBackward"}); - auto [grad_a, grad_b] = kernel.Call, std::shared_ptr>>(grad_output, a, b); - return {grad_a, grad_b}; -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/elementwise.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" +#include + +namespace infini_train::autograd { +std::vector> Neg::Forward(const std::vector> &input_tensors) { + // =================================== 作业 =================================== + // TODO:通过Dispatcher获取设备专属kernel,对输入张量进行取反操作 + // NOTES: 依赖test_dispatcher,Neg kernel实现已给出 + // =================================== 作业 =================================== + + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "NegForward"}); + return {kernel.Call>(input)}; +} + +std::vector> Neg::Backward(const std::vector> &grad_outputs) { + // =================================== 作业 =================================== + // TODO:通过Dispatcher获取设备专属的反向传播kernel,计算梯度 + // NOTES: 依赖test_dispatcher,Neg的kernel实现已给出 + // =================================== 作业 =================================== + + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "NegBackward"}); + return {kernel.Call>(grad_output)}; +} + +std::vector> Reciprocal::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "ReciprocalForward"}); + return {kernel.Call>(input)}; +} + +void Reciprocal::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &input = input_tensors[0]; + saved_tensors_ = {input}; +} + +std::vector> Reciprocal::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 1); + const auto &input = saved_tensors_[0]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "ReciprocalBackward"}); + return {kernel.Call>(grad_output, input)}; +} + +std::vector> Sin::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SinForward"}); + return {kernel.Call>(input)}; +} + +void Sin::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &input = input_tensors[0]; + saved_tensors_ = {input}; +} + +std::vector> Sin::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 1); + const auto &input = saved_tensors_[0]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SinBackward"}); + return {kernel.Call>(grad_output, input)}; +} + +std::vector> Cos::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "CosForward"}); + return {kernel.Call>(input)}; +} + +void Cos::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &input = input_tensors[0]; + saved_tensors_ = {input}; +} + +std::vector> Cos::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 1); + const auto &input = saved_tensors_[0]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "CosBackward"}); + return {kernel.Call>(grad_output, input)}; +} + +std::vector> Tanh::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "TanhForward"}); + return {kernel.Call>(input)}; +} + +void Tanh::SetupContext(const std::vector> &, + const std::vector> &output_tensors) { + const auto &output = output_tensors[0]; + saved_tensors_ = {output}; +} + +std::vector> Tanh::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 1); + const auto &output = saved_tensors_[0]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "TanhBackward"}); + return {kernel.Call>(grad_output, output)}; +} + +std::vector> Pow::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "PowForward"}); + return {kernel.Call>(input, exponent_, scalar_is_base_)}; +} + +void Pow::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &input = input_tensors[0]; + saved_tensors_ = {input}; +} + +std::vector> Pow::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 1); + const auto &input = saved_tensors_[0]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "PowBackward"}); + return {kernel.Call>(grad_output, input, exponent_, scalar_is_base_)}; +} + +std::vector> Rsqrt::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "RsqrtForward"}); + return {kernel.Call>(input)}; +} + +void Rsqrt::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &input = input_tensors[0]; + saved_tensors_ = {input}; +} + +std::vector> Rsqrt::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 1); + const auto &input = saved_tensors_[0]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "RsqrtBackward"}); + return {kernel.Call>(grad_output, input)}; +} + +std::vector> EqualsScalar::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "EqualsScalarForward"}); + return {kernel.Call>(input, scalar_)}; +} + +std::vector> EqualsScalar::Backward(const std::vector> &grad_outputs) { + LOG(FATAL) << "EqualsScalar::Backward shall not be called anytime"; + return {}; +} + +std::vector> Add::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 2); + const auto &a = input_tensors[0]; + const auto &b = input_tensors[1]; + + auto device = a->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "AddForward"}); + return {kernel.Call>(a, b)}; +} + +void Add::SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) { + a_dims_ = input_tensors[0]->Dims(); + b_dims_ = input_tensors[1]->Dims(); +} + +std::vector> Add::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "AddBackward"}); + auto [grad_a, grad_b] + = kernel.Call, std::shared_ptr>>(grad_output, a_dims_, b_dims_); + return {grad_a, grad_b}; +} + +std::vector> AddScalar::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "AddScalarForward"}); + return {kernel.Call>(input, scalar_)}; +} + +std::vector> AddScalar::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "AddScalarBackward"}); + return {kernel.Call>(grad_output)}; +} + +std::vector> Sub::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 2); + const auto &a = input_tensors[0]; + const auto &b = input_tensors[1]; + + auto device = a->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SubForward"}); + return {kernel.Call>(a, b)}; +} + +void Sub::SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) { + a_dims_ = input_tensors[0]->Dims(); + b_dims_ = input_tensors[1]->Dims(); +} + +std::vector> Sub::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SubBackward"}); + auto [grad_a, grad_b] + = kernel.Call, std::shared_ptr>>(grad_output, a_dims_, b_dims_); + return {grad_a, grad_b}; +} + +std::vector> Mul::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 2); + const auto &a = input_tensors[0]; + const auto &b = input_tensors[1]; + + auto device = a->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MulForward"}); + return {kernel.Call>(a, b)}; +} + +void Mul::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &a = input_tensors[0]; + const auto &b = input_tensors[1]; + saved_tensors_ = {a, b}; +} + +std::vector> Mul::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 2); + const auto &a = saved_tensors_[0]; + const auto &b = saved_tensors_[1]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MulBackward"}); + auto [grad_a, grad_b] = kernel.Call, std::shared_ptr>>(grad_output, a, b); + return {grad_a, grad_b}; +} + +std::vector> MulScalar::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MulScalarForward"}); + return {kernel.Call>(input, scalar_)}; +} + +std::vector> MulScalar::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MulScalarBackward"}); + return {kernel.Call>(grad_output, scalar_)}; +} + +std::vector> Div::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 2); + const auto &a = input_tensors[0]; + const auto &b = input_tensors[1]; + + auto device = a->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "DivForward"}); + return {kernel.Call>(a, b)}; +} + +void Div::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &a = input_tensors[0]; + const auto &b = input_tensors[1]; + saved_tensors_ = {a, b}; +} + +std::vector> Div::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 2); + const auto &a = saved_tensors_[0]; + const auto &b = saved_tensors_[1]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "DivBackward"}); + auto [grad_a, grad_b] = kernel.Call, std::shared_ptr>>(grad_output, a, b); + return {grad_a, grad_b}; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/function.cc b/infini_train/src/autograd/function.cc index 6edf723..6cc9f9e 100644 --- a/infini_train/src/autograd/function.cc +++ b/infini_train/src/autograd/function.cc @@ -1,93 +1,101 @@ -#include "infini_train/include/autograd/function.h" - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -namespace { -class AccumulateGrad final : public Function { -public: - explicit AccumulateGrad(std::shared_ptr grad) : grad_(grad) {} - - std::vector> Forward(const std::vector> &) override { - LOG(FATAL) << "AccumulateGrad::Forward shall not be called directly!"; - return {}; - } - - std::vector> Backward(const std::vector> &) override { - LOG(FATAL) << "AccumulateGrad::Backward shall not be called directly!"; - return {}; - } - - void BackwardPartial(const std::shared_ptr &grad_output, int) override { - if (grad_output) { - auto device = grad_->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "AccumulateGrad"}); - kernel.Call(grad_output, 1.0f, grad_); - } - } - -private: - std::shared_ptr grad_ = nullptr; -}; -} // namespace - -std::vector> Function::Apply(const std::vector> &input_tensors) { - auto output_tensors = Forward(input_tensors); - SetupContext(input_tensors, output_tensors); - - bool output_requires_grad = false; - for (int idx = 0; idx < input_tensors.size(); ++idx) { - const auto &input_tensor = input_tensors[idx]; - if (input_tensor->requires_grad() && input_tensor->is_leaf()) { - next_functions_.emplace_back(std::make_shared(input_tensor->grad()), 0); - } else { - next_functions_.emplace_back(input_tensor->grad_fn(), input_tensor->output_idx()); - if (input_tensor->grad_fn()) { - input_tensor->grad_fn()->IncreaseDependenciesNumber(); - } - } - output_requires_grad |= input_tensor->requires_grad(); - } - - grad_outputs_reached_ = 0; - grad_outputs_.resize(output_tensors.size(), nullptr); - for (int output_idx = 0; output_idx < output_tensors.size(); ++output_idx) { - auto &output_tensor = output_tensors[output_idx]; - output_tensor->set_requires_grad(output_requires_grad); - output_tensor->set_is_leaf(false); - output_tensor->set_grad_fn(output_requires_grad ? shared_from_this() : nullptr); - output_tensor->set_output_idx(output_idx); - } - - return output_tensors; -} - -void Function::BackwardPartial(const std::shared_ptr &grad_output, int grad_output_idx) { - if (!grad_outputs_[grad_output_idx]) { - grad_outputs_[grad_output_idx] = grad_output; - ++grad_outputs_reached_; - } else { - auto accumulate_function = std::make_shared(grad_outputs_[grad_output_idx]); - accumulate_function->BackwardPartial(grad_output, 0); - } - ++dependencies_reached_; - if (grad_outputs_reached_ == grad_outputs_.size() - && (dependencies_reached_ == dependencies_number_ || dependencies_number_ == 0)) { - auto grad_inputs = Backward(grad_outputs_); - saved_tensors_.clear(); - grad_outputs_.clear(); - CHECK_EQ(grad_inputs.size(), next_functions_.size()); - for (int idx = 0; idx < grad_inputs.size(); ++idx) { - auto &grad_input = grad_inputs[idx]; - auto &[next_function, output_idx] = next_functions_[idx]; - if (grad_input && next_function) { - next_function->BackwardPartial(grad_input, output_idx); - } - } - } -} -void Function::IncreaseDependenciesNumber() { ++dependencies_number_; } -} // namespace infini_train::autograd +#include "infini_train/include/autograd/function.h" + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { + +thread_local int NoGradGuard::depth_ = 0; + +namespace { + +class AccumulateGrad final : public Function { +public: + explicit AccumulateGrad(std::shared_ptr grad) : grad_(grad) {} + + std::vector> Forward(const std::vector> &) override { + LOG(FATAL) << "AccumulateGrad::Forward shall not be called directly!"; + return {}; + } + + std::vector> Backward(const std::vector> &) override { + LOG(FATAL) << "AccumulateGrad::Backward shall not be called directly!"; + return {}; + } + + void BackwardPartial(const std::shared_ptr &grad_output, int) override { + if (grad_output) { + auto device = grad_->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "AccumulateGrad"}); + kernel.Call(grad_output, 1.0f, grad_); + } + } + +private: + std::shared_ptr grad_ = nullptr; +}; +} // namespace + +std::vector> Function::Apply(const std::vector> &input_tensors) { + if (NoGradGuard::is_enabled()) { + return Forward(input_tensors); + } + + auto output_tensors = Forward(input_tensors); + SetupContext(input_tensors, output_tensors); + + bool output_requires_grad = false; + for (int idx = 0; idx < input_tensors.size(); ++idx) { + const auto &input_tensor = input_tensors[idx]; + if (input_tensor->requires_grad() && input_tensor->is_leaf()) { + next_functions_.emplace_back(std::make_shared(input_tensor->grad()), 0); + } else { + next_functions_.emplace_back(input_tensor->grad_fn(), input_tensor->output_idx()); + if (input_tensor->grad_fn()) { + input_tensor->grad_fn()->IncreaseDependenciesNumber(); + } + } + output_requires_grad |= input_tensor->requires_grad(); + } + + grad_outputs_reached_ = 0; + grad_outputs_.resize(output_tensors.size(), nullptr); + for (int output_idx = 0; output_idx < output_tensors.size(); ++output_idx) { + auto &output_tensor = output_tensors[output_idx]; + output_tensor->set_requires_grad(output_requires_grad); + output_tensor->set_is_leaf(false); + output_tensor->set_grad_fn(output_requires_grad ? shared_from_this() : nullptr); + output_tensor->set_output_idx(output_idx); + } + + return output_tensors; +} + +void Function::BackwardPartial(const std::shared_ptr &grad_output, int grad_output_idx) { + if (!grad_outputs_[grad_output_idx]) { + grad_outputs_[grad_output_idx] = grad_output; + ++grad_outputs_reached_; + } else { + auto accumulate_function = std::make_shared(grad_outputs_[grad_output_idx]); + accumulate_function->BackwardPartial(grad_output, 0); + } + ++dependencies_reached_; + if (grad_outputs_reached_ == grad_outputs_.size() + && (dependencies_reached_ == dependencies_number_ || dependencies_number_ == 0)) { + auto grad_inputs = Backward(grad_outputs_); + saved_tensors_.clear(); + grad_outputs_.clear(); + CHECK_EQ(grad_inputs.size(), next_functions_.size()); + for (int idx = 0; idx < grad_inputs.size(); ++idx) { + auto &grad_input = grad_inputs[idx]; + auto &[next_function, output_idx] = next_functions_[idx]; + if (grad_input && next_function) { + next_function->BackwardPartial(grad_input, output_idx); + } + } + } +} +void Function::IncreaseDependenciesNumber() { ++dependencies_number_; } +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/linear.cc b/infini_train/src/autograd/linear.cc index 7748e59..bac8620 100644 --- a/infini_train/src/autograd/linear.cc +++ b/infini_train/src/autograd/linear.cc @@ -1,45 +1,45 @@ -#include "infini_train/include/autograd/linear.h" - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -std::vector> Linear::Forward(const std::vector> &input_tensors) { - CHECK_GE(input_tensors.size(), 2); - const auto &input = input_tensors[0]; - const auto &weight = input_tensors[1]; - const auto &bias = input_tensors.size() == 3 ? input_tensors[2] : nullptr; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "LinearForward"}); - return {kernel.Call>(input, weight, true, bias)}; -} - -void Linear::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &input = input_tensors[0]; - const auto &weight = input_tensors[1]; - saved_tensors_ = {input, weight}; - bias_ = input_tensors.size() == 3; - out_features_ = weight->Dims()[0]; -} - -std::vector> Linear::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 2); - const auto &input = saved_tensors_[0]; - const auto &weight = saved_tensors_[1]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "LinearBackward"}); - auto [grad_input, grad_weight, grad_bias] - = kernel.Call, std::shared_ptr, std::shared_ptr>>( - input, weight, true, out_features_, grad_output, bias_); - return bias_ ? std::vector>{grad_input, grad_weight, grad_bias} - : std::vector>{grad_input, grad_weight}; - ; -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/linear.h" + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +std::vector> Linear::Forward(const std::vector> &input_tensors) { + CHECK_GE(input_tensors.size(), 2); + const auto &input = input_tensors[0]; + const auto &weight = input_tensors[1]; + const auto &bias = input_tensors.size() == 3 ? input_tensors[2] : nullptr; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "LinearForward"}); + return {kernel.Call>(input, weight, true, bias)}; +} + +void Linear::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &input = input_tensors[0]; + const auto &weight = input_tensors[1]; + saved_tensors_ = {input, weight}; + bias_ = input_tensors.size() == 3; + out_features_ = weight->Dims()[0]; +} + +std::vector> Linear::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 2); + const auto &input = saved_tensors_[0]; + const auto &weight = saved_tensors_[1]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "LinearBackward"}); + auto [grad_input, grad_weight, grad_bias] + = kernel.Call, std::shared_ptr, std::shared_ptr>>( + input, weight, true, out_features_, grad_output, bias_); + return bias_ ? std::vector>{grad_input, grad_weight, grad_bias} + : std::vector>{grad_input, grad_weight}; + ; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/loss.cc b/infini_train/src/autograd/loss.cc index b3824bd..a9145ab 100644 --- a/infini_train/src/autograd/loss.cc +++ b/infini_train/src/autograd/loss.cc @@ -1,38 +1,38 @@ -#include "infini_train/include/autograd/loss.h" - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -std::vector> CrossEntropy::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 2); - const auto &input = input_tensors[0]; - const auto &target = input_tensors[1]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "CrossEntropyForward"}); - return {kernel.Call>(input, target)}; -} - -void CrossEntropy::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &input = input_tensors[0]; - const auto &target = input_tensors[1]; - saved_tensors_ = {input, target}; -} - -std::vector> CrossEntropy::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 2); - const auto &input = saved_tensors_[0]; - const auto &target = saved_tensors_[1]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "CrossEntropyBackward"}); - auto grad_input = kernel.Call>(input, target, grad_output); - return {grad_input, nullptr}; -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/loss.h" + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +std::vector> CrossEntropy::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 2); + const auto &input = input_tensors[0]; + const auto &target = input_tensors[1]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "CrossEntropyForward"}); + return {kernel.Call>(input, target)}; +} + +void CrossEntropy::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &input = input_tensors[0]; + const auto &target = input_tensors[1]; + saved_tensors_ = {input, target}; +} + +std::vector> CrossEntropy::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 2); + const auto &input = saved_tensors_[0]; + const auto &target = saved_tensors_[1]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "CrossEntropyBackward"}); + auto grad_input = kernel.Call>(input, target, grad_output); + return {grad_input, nullptr}; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/matmul.cc b/infini_train/src/autograd/matmul.cc index 9bca403..198f904 100644 --- a/infini_train/src/autograd/matmul.cc +++ b/infini_train/src/autograd/matmul.cc @@ -1,41 +1,41 @@ -#include "infini_train/include/autograd/matmul.h" - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -std::vector> Matmul::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 2); - const auto &input1 = input_tensors[0]; - const auto &input2 = input_tensors[1]; - - auto device = input1->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MatmulForward"}); - return {kernel.Call>(input1, input2)}; -} - -void Matmul::SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) { - const auto &input1 = input_tensors[0]; - const auto &input2 = input_tensors[1]; - const auto &output = output_tensors[0]; - saved_tensors_ = {input1, input2}; - out_features_ = output->Dims()[0]; -} - -std::vector> Matmul::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 2); - const auto &input1 = saved_tensors_[0]; - const auto &input2 = saved_tensors_[1]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = input1->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MatmulBackward"}); - auto [grad_input1, grad_input2] - = kernel.Call, std::shared_ptr>>(input1, input2, grad_output); - return {grad_input1, grad_input2}; -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/matmul.h" + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +std::vector> Matmul::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 2); + const auto &input1 = input_tensors[0]; + const auto &input2 = input_tensors[1]; + + auto device = input1->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MatmulForward"}); + return {kernel.Call>(input1, input2)}; +} + +void Matmul::SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) { + const auto &input1 = input_tensors[0]; + const auto &input2 = input_tensors[1]; + const auto &output = output_tensors[0]; + saved_tensors_ = {input1, input2}; + out_features_ = output->Dims()[0]; +} + +std::vector> Matmul::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 2); + const auto &input1 = saved_tensors_[0]; + const auto &input2 = saved_tensors_[1]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = input1->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MatmulBackward"}); + auto [grad_input1, grad_input2] + = kernel.Call, std::shared_ptr>>(input1, input2, grad_output); + return {grad_input1, grad_input2}; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/misc.cc b/infini_train/src/autograd/misc.cc index 59d395e..759d0c6 100644 --- a/infini_train/src/autograd/misc.cc +++ b/infini_train/src/autograd/misc.cc @@ -1,101 +1,101 @@ -#include "infini_train/include/autograd/misc.h" - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -std::vector> Split::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SplitForward"}); - return {kernel.Call>>(input, split_size_, dim_)}; -} - -void Split::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &input = input_tensors[0]; - input_dims_ = input->Dims(); -} - -std::vector> Split::Backward(const std::vector> &grad_outputs) { - auto device = grad_outputs[0]->GetDevice(); - auto kernel = Dispatcher::Instance().GetKernel({device.Type(), "SplitBackward"}); - return {kernel.Call>(input_dims_, split_size_, dim_, grad_outputs)}; -} - -std::vector> NoOp::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "NoOpForward"}); - return {kernel.Call>(input, output_dims_)}; -} - -void NoOp::SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) { - const auto &input = input_tensors[0]; - input_dims_ = input->Dims(); -} - -std::vector> NoOp::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "NoOpBackward"}); - return {kernel.Call>(input_dims_, grad_output)}; -} - -std::vector> Slice::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SliceForward"}); - return {kernel.Call>(input, starts_, ends_, steps_)}; -} - -void Slice::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - // FIXME(dcj): only input's dim need to be saved - const auto &input = input_tensors[0]; - saved_tensors_ = {input}; -} - -std::vector> Slice::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 1); - const auto &input = saved_tensors_[0]; - const auto &grad_output = grad_outputs[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SliceBackward"}); - return {kernel.Call>(grad_output, input, starts_, ends_, steps_)}; -} - -std::vector> Stack::Forward(const std::vector> &input_tensors) { - CHECK_GE(input_tensors.size(), 2); - const auto device = input_tensors[0]->GetDevice().Type(); - - auto kernel = Dispatcher::Instance().GetKernel({device, "StackForward"}); - return {kernel.Call>(input_tensors, dim_)}; -} - -void Stack::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &input = input_tensors[0]; - input_dims_ = input->Dims(); -} - -std::vector> Stack::Backward(const std::vector> &grad_outputs) { - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "StackBackward"}); - return kernel.Call>>(input_dims_, dim_, grad_output); -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/misc.h" + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +std::vector> Split::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SplitForward"}); + return {kernel.Call>>(input, split_size_, dim_)}; +} + +void Split::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &input = input_tensors[0]; + input_dims_ = input->Dims(); +} + +std::vector> Split::Backward(const std::vector> &grad_outputs) { + auto device = grad_outputs[0]->GetDevice(); + auto kernel = Dispatcher::Instance().GetKernel({device.Type(), "SplitBackward"}); + return {kernel.Call>(input_dims_, split_size_, dim_, grad_outputs)}; +} + +std::vector> NoOp::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "NoOpForward"}); + return {kernel.Call>(input, output_dims_)}; +} + +void NoOp::SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) { + const auto &input = input_tensors[0]; + input_dims_ = input->Dims(); +} + +std::vector> NoOp::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "NoOpBackward"}); + return {kernel.Call>(input_dims_, grad_output)}; +} + +std::vector> Slice::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SliceForward"}); + return {kernel.Call>(input, starts_, ends_, steps_)}; +} + +void Slice::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + // FIXME(dcj): only input's dim need to be saved + const auto &input = input_tensors[0]; + saved_tensors_ = {input}; +} + +std::vector> Slice::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 1); + const auto &input = saved_tensors_[0]; + const auto &grad_output = grad_outputs[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SliceBackward"}); + return {kernel.Call>(grad_output, input, starts_, ends_, steps_)}; +} + +std::vector> Stack::Forward(const std::vector> &input_tensors) { + CHECK_GE(input_tensors.size(), 2); + const auto device = input_tensors[0]->GetDevice().Type(); + + auto kernel = Dispatcher::Instance().GetKernel({device, "StackForward"}); + return {kernel.Call>(input_tensors, dim_)}; +} + +void Stack::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &input = input_tensors[0]; + input_dims_ = input->Dims(); +} + +std::vector> Stack::Backward(const std::vector> &grad_outputs) { + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "StackBackward"}); + return kernel.Call>>(input_dims_, dim_, grad_output); +} +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/normalization.cc b/infini_train/src/autograd/normalization.cc index 95b64f9..a7adce3 100644 --- a/infini_train/src/autograd/normalization.cc +++ b/infini_train/src/autograd/normalization.cc @@ -1,50 +1,50 @@ -#include "infini_train/include/autograd/normalization.h" - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { - -std::vector> LayerNorm::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 3); - const auto &input = input_tensors[0]; - const auto &weight = input_tensors[1]; - const auto &bias = input_tensors[2]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "LayerNormForward"}); - auto [output, mean, rstd] - = kernel.Call, std::shared_ptr, std::shared_ptr>>( - input, weight, bias, eps_); - saved_tensors_ = {mean, rstd}; - return {output}; -} - -void LayerNorm::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &input = input_tensors[0]; - const auto &weight = input_tensors[1]; - const auto &bias = input_tensors[2]; - saved_tensors_.insert(saved_tensors_.begin(), {input, weight, bias}); -} - -std::vector> LayerNorm::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 5); - const auto &input = saved_tensors_[0]; - const auto &weight = saved_tensors_[1]; - const auto &bias = saved_tensors_[2]; - const auto &mean = saved_tensors_[3]; - const auto &rstd = saved_tensors_[4]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "LayerNormBackward"}); - auto [grad_input, grad_weight, grad_bias] - = kernel.Call, std::shared_ptr, std::shared_ptr>>( - input, weight, bias, mean, rstd, grad_output); - return {grad_input, grad_weight, grad_bias}; -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/normalization.h" + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { + +std::vector> LayerNorm::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 3); + const auto &input = input_tensors[0]; + const auto &weight = input_tensors[1]; + const auto &bias = input_tensors[2]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "LayerNormForward"}); + auto [output, mean, rstd] + = kernel.Call, std::shared_ptr, std::shared_ptr>>( + input, weight, bias, eps_); + saved_tensors_ = {mean, rstd}; + return {output}; +} + +void LayerNorm::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &input = input_tensors[0]; + const auto &weight = input_tensors[1]; + const auto &bias = input_tensors[2]; + saved_tensors_.insert(saved_tensors_.begin(), {input, weight, bias}); +} + +std::vector> LayerNorm::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 5); + const auto &input = saved_tensors_[0]; + const auto &weight = saved_tensors_[1]; + const auto &bias = saved_tensors_[2]; + const auto &mean = saved_tensors_[3]; + const auto &rstd = saved_tensors_[4]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "LayerNormBackward"}); + auto [grad_input, grad_weight, grad_bias] + = kernel.Call, std::shared_ptr, std::shared_ptr>>( + input, weight, bias, mean, rstd, grad_output); + return {grad_input, grad_weight, grad_bias}; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/outer.cc b/infini_train/src/autograd/outer.cc index 7d9339f..4b414f2 100644 --- a/infini_train/src/autograd/outer.cc +++ b/infini_train/src/autograd/outer.cc @@ -1,43 +1,43 @@ -#include "infini_train/include/autograd/outer.h" - -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -std::vector> Outer::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 2); - const auto &input1 = input_tensors[0]; - const auto &input2 = input_tensors[1]; - - auto device = input1->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "OuterForward"}); - return {kernel.Call>(input1, input2)}; -} - -void Outer::SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) { - const auto &input1 = input_tensors[0]; - const auto &input2 = input_tensors[1]; - saved_tensors_ = {input1, input2}; -} - -std::vector> Outer::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 2); - const auto &input1 = saved_tensors_[0]; - const auto &input2 = saved_tensors_[1]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = input1->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "OuterBackward"}); - auto [grad_input1, grad_input2] - = kernel.Call, std::shared_ptr>>(input1, input2, grad_output); - return {grad_input1, grad_input2}; -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/outer.h" + +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +std::vector> Outer::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 2); + const auto &input1 = input_tensors[0]; + const auto &input2 = input_tensors[1]; + + auto device = input1->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "OuterForward"}); + return {kernel.Call>(input1, input2)}; +} + +void Outer::SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) { + const auto &input1 = input_tensors[0]; + const auto &input2 = input_tensors[1]; + saved_tensors_ = {input1, input2}; +} + +std::vector> Outer::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 2); + const auto &input1 = saved_tensors_[0]; + const auto &input2 = saved_tensors_[1]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = input1->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "OuterBackward"}); + auto [grad_input1, grad_input2] + = kernel.Call, std::shared_ptr>>(input1, input2, grad_output); + return {grad_input1, grad_input2}; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/reduction.cc b/infini_train/src/autograd/reduction.cc index cbe3b21..fde90d5 100644 --- a/infini_train/src/autograd/reduction.cc +++ b/infini_train/src/autograd/reduction.cc @@ -1,115 +1,115 @@ -#include "infini_train/include/autograd/reduction.h" - -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -std::vector> Mean::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MeanForward"}); - return {kernel.Call>(input, dim_, keep_dim_)}; -} - -void Mean::SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) { - const auto &input = input_tensors[0]; - input_dims_ = input->Dims(); -} - -std::vector> Mean::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MeanBackward"}); - return {kernel.Call>(grad_output, input_dims_, dim_, keep_dim_)}; -} - -std::vector> Sum::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SumForward"}); - return {kernel.Call>(input, dim_, keep_dim_)}; -} - -void Sum::SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) { - const auto &input = input_tensors[0]; - input_dims_ = input->Dims(); -} - -std::vector> Sum::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SumBackward"}); - return {kernel.Call>(grad_output, input_dims_, dim_, keep_dim_)}; -} - -std::vector> Max::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MaxForward"}); - return {kernel.Call>(input, dim_, keep_dim_)}; -} - -void Max::SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) { - const auto &input = input_tensors[0]; - const auto &output = output_tensors[0]; - saved_tensors_ = {input, output}; -} - -std::vector> Max::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(grad_outputs.size(), 1); - CHECK_EQ(saved_tensors_.size(), 2); - const auto &grad_output = grad_outputs[0]; - const auto &input = saved_tensors_[0]; - const auto &reduced = saved_tensors_[1]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MaxBackward"}); - return {kernel.Call>(grad_output, input, reduced, dim_, keep_dim_)}; -} - -std::vector> Min::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MinForward"}); - return {kernel.Call>(input, dim_, keep_dim_)}; -} - -void Min::SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) { - const auto &input = input_tensors[0]; - const auto &output = output_tensors[0]; - saved_tensors_ = {input, output}; -} - -std::vector> Min::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(grad_outputs.size(), 1); - CHECK_EQ(saved_tensors_.size(), 2); - const auto &grad_output = grad_outputs[0]; - const auto &input = saved_tensors_[0]; - const auto &reduced = saved_tensors_[1]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MinBackward"}); - return {kernel.Call>(grad_output, input, reduced, dim_, keep_dim_)}; -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/reduction.h" + +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +std::vector> Mean::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MeanForward"}); + return {kernel.Call>(input, dim_, keep_dim_)}; +} + +void Mean::SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) { + const auto &input = input_tensors[0]; + input_dims_ = input->Dims(); +} + +std::vector> Mean::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MeanBackward"}); + return {kernel.Call>(grad_output, input_dims_, dim_, keep_dim_)}; +} + +std::vector> Sum::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SumForward"}); + return {kernel.Call>(input, dim_, keep_dim_)}; +} + +void Sum::SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) { + const auto &input = input_tensors[0]; + input_dims_ = input->Dims(); +} + +std::vector> Sum::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SumBackward"}); + return {kernel.Call>(grad_output, input_dims_, dim_, keep_dim_)}; +} + +std::vector> Max::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MaxForward"}); + return {kernel.Call>(input, dim_, keep_dim_)}; +} + +void Max::SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) { + const auto &input = input_tensors[0]; + const auto &output = output_tensors[0]; + saved_tensors_ = {input, output}; +} + +std::vector> Max::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(grad_outputs.size(), 1); + CHECK_EQ(saved_tensors_.size(), 2); + const auto &grad_output = grad_outputs[0]; + const auto &input = saved_tensors_[0]; + const auto &reduced = saved_tensors_[1]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MaxBackward"}); + return {kernel.Call>(grad_output, input, reduced, dim_, keep_dim_)}; +} + +std::vector> Min::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MinForward"}); + return {kernel.Call>(input, dim_, keep_dim_)}; +} + +void Min::SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) { + const auto &input = input_tensors[0]; + const auto &output = output_tensors[0]; + saved_tensors_ = {input, output}; +} + +std::vector> Min::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(grad_outputs.size(), 1); + CHECK_EQ(saved_tensors_.size(), 2); + const auto &grad_output = grad_outputs[0]; + const auto &input = saved_tensors_[0]; + const auto &reduced = saved_tensors_[1]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MinBackward"}); + return {kernel.Call>(grad_output, input, reduced, dim_, keep_dim_)}; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/softmax.cc b/infini_train/src/autograd/softmax.cc index cd0e42a..45f770f 100644 --- a/infini_train/src/autograd/softmax.cc +++ b/infini_train/src/autograd/softmax.cc @@ -1,34 +1,34 @@ -#include "infini_train/include/autograd/softmax.h" - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -std::vector> Softmax::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SoftmaxForward"}); - return {kernel.Call>(input, dim_)}; -} - -void Softmax::SetupContext(const std::vector> &, - const std::vector> &output_tensors) { - const auto &output = output_tensors[0]; - saved_tensors_ = {output}; -} - -std::vector> Softmax::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(saved_tensors_.size(), 1); - const auto &output = saved_tensors_[0]; - CHECK_EQ(grad_outputs.size(), 1); - const auto &grad_output = grad_outputs[0]; - - auto device = output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "SoftmaxBackward"}); - return {kernel.Call>(grad_output, output, dim_)}; -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/softmax.h" + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +std::vector> Softmax::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SoftmaxForward"}); + return {kernel.Call>(input, dim_)}; +} + +void Softmax::SetupContext(const std::vector> &, + const std::vector> &output_tensors) { + const auto &output = output_tensors[0]; + saved_tensors_ = {output}; +} + +std::vector> Softmax::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(saved_tensors_.size(), 1); + const auto &output = saved_tensors_[0]; + CHECK_EQ(grad_outputs.size(), 1); + const auto &grad_output = grad_outputs[0]; + + auto device = output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "SoftmaxBackward"}); + return {kernel.Call>(grad_output, output, dim_)}; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/sparse.cc b/infini_train/src/autograd/sparse.cc index 39a0e4f..a439bc4 100644 --- a/infini_train/src/autograd/sparse.cc +++ b/infini_train/src/autograd/sparse.cc @@ -1,37 +1,37 @@ -#include "infini_train/include/autograd/sparse.h" - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::autograd { -std::vector> Embedding::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 2); - const auto &input = input_tensors[0]; - const auto &weight = input_tensors[1]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "EmbeddingForward"}); - return {kernel.Call>(input, weight)}; -} - -void Embedding::SetupContext(const std::vector> &input_tensors, - const std::vector> &output_tensors) { - const auto &input = input_tensors[0]; - const auto &weight = input_tensors[1]; - weight_dims_ = weight->Dims(); - saved_tensors_ = {input}; -} - -std::vector> Embedding::Backward(const std::vector> &grad_outputs) { - CHECK_EQ(grad_outputs.size(), 1); - const auto &input = saved_tensors_[0]; - const auto &grad_output = grad_outputs[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "EmbeddingBackward"}); - auto grad_weight = kernel.Call>(input, weight_dims_, grad_output); - return {nullptr, grad_weight}; -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/sparse.h" + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::autograd { +std::vector> Embedding::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 2); + const auto &input = input_tensors[0]; + const auto &weight = input_tensors[1]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "EmbeddingForward"}); + return {kernel.Call>(input, weight)}; +} + +void Embedding::SetupContext(const std::vector> &input_tensors, + const std::vector> &output_tensors) { + const auto &input = input_tensors[0]; + const auto &weight = input_tensors[1]; + weight_dims_ = weight->Dims(); + saved_tensors_ = {input}; +} + +std::vector> Embedding::Backward(const std::vector> &grad_outputs) { + CHECK_EQ(grad_outputs.size(), 1); + const auto &input = saved_tensors_[0]; + const auto &grad_output = grad_outputs[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "EmbeddingBackward"}); + auto grad_weight = kernel.Call>(input, weight_dims_, grad_output); + return {nullptr, grad_weight}; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/autograd/transform.cc b/infini_train/src/autograd/transform.cc index aad7edc..cdcfb8d 100644 --- a/infini_train/src/autograd/transform.cc +++ b/infini_train/src/autograd/transform.cc @@ -1,98 +1,98 @@ -#include "infini_train/include/autograd/transform.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" -namespace infini_train::autograd { -std::vector> Tril::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "TrilForward"}); - return {kernel.Call>(input, diagonal_)}; -} - -std::vector> Tril::Backward(const std::vector> &grad_outputs) { - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "TrilBackward"}); - return {kernel.Call>(grad_output, diagonal_)}; -} - -std::vector> Triu::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "TriuForward"}); - return {kernel.Call>(input, diagonal_)}; -} - -std::vector> Triu::Backward(const std::vector> &grad_outputs) { - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "TriuBackward"}); - return {kernel.Call>(grad_output, diagonal_)}; -} - -std::vector> Transpose::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "TransposeForward"}); - return {kernel.Call>(input, dim0_, dim1_)}; -} - -std::vector> Transpose::Backward(const std::vector> &grad_outputs) { - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "TransposeBackward"}); - return {kernel.Call>(grad_output, dim0_, dim1_)}; -} - -std::vector> Mask::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MaskForward"}); - return {kernel.Call>(input, mask_, value_)}; -} - -std::vector> Mask::Backward(const std::vector> &grad_outputs) { - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "MaskBackward"}); - return {kernel.Call>(grad_output, mask_)}; -} - -std::vector> -RepeatInterleave::Forward(const std::vector> &input_tensors) { - CHECK_EQ(input_tensors.size(), 1); - const auto &input = input_tensors[0]; - - auto device = input->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "RepeatInterleaveForward"}); - return {kernel.Call>(input, repeat_, dim_)}; -} - -void RepeatInterleave::SetupContext(const std::vector> &input_tensors, - const std::vector> &) { - const auto &input = input_tensors[0]; - input_dims_ = input->Dims(); -} - -std::vector> -RepeatInterleave::Backward(const std::vector> &grad_outputs) { - const auto &grad_output = grad_outputs[0]; - - auto device = grad_output->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "RepeatInterleaveBackward"}); - return {kernel.Call>(grad_output, input_dims_, dim_)}; -} -} // namespace infini_train::autograd +#include "infini_train/include/autograd/transform.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" +namespace infini_train::autograd { +std::vector> Tril::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "TrilForward"}); + return {kernel.Call>(input, diagonal_)}; +} + +std::vector> Tril::Backward(const std::vector> &grad_outputs) { + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "TrilBackward"}); + return {kernel.Call>(grad_output, diagonal_)}; +} + +std::vector> Triu::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "TriuForward"}); + return {kernel.Call>(input, diagonal_)}; +} + +std::vector> Triu::Backward(const std::vector> &grad_outputs) { + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "TriuBackward"}); + return {kernel.Call>(grad_output, diagonal_)}; +} + +std::vector> Transpose::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "TransposeForward"}); + return {kernel.Call>(input, dim0_, dim1_)}; +} + +std::vector> Transpose::Backward(const std::vector> &grad_outputs) { + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "TransposeBackward"}); + return {kernel.Call>(grad_output, dim0_, dim1_)}; +} + +std::vector> Mask::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MaskForward"}); + return {kernel.Call>(input, mask_, value_)}; +} + +std::vector> Mask::Backward(const std::vector> &grad_outputs) { + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "MaskBackward"}); + return {kernel.Call>(grad_output, mask_)}; +} + +std::vector> +RepeatInterleave::Forward(const std::vector> &input_tensors) { + CHECK_EQ(input_tensors.size(), 1); + const auto &input = input_tensors[0]; + + auto device = input->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "RepeatInterleaveForward"}); + return {kernel.Call>(input, repeat_, dim_)}; +} + +void RepeatInterleave::SetupContext(const std::vector> &input_tensors, + const std::vector> &) { + const auto &input = input_tensors[0]; + input_dims_ = input->Dims(); +} + +std::vector> +RepeatInterleave::Backward(const std::vector> &grad_outputs) { + const auto &grad_output = grad_outputs[0]; + + auto device = grad_output->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "RepeatInterleaveBackward"}); + return {kernel.Call>(grad_output, input_dims_, dim_)}; +} +} // namespace infini_train::autograd diff --git a/infini_train/src/dataloader.cc b/infini_train/src/dataloader.cc index 9b022f6..e827bc6 100644 --- a/infini_train/src/dataloader.cc +++ b/infini_train/src/dataloader.cc @@ -1,87 +1,87 @@ -#include "infini_train/include/dataloader.h" - -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dataset.h" -#include "infini_train/include/tensor.h" - -namespace infini_train { -namespace { -std::shared_ptr Stack(const std::vector> &tensors) { - const int batch_size = tensors.size(); - const auto &dims = tensors[0]->Dims(); - const int stacked_dim = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); - auto stacked_tensor = std::make_shared(std::vector{batch_size, stacked_dim}, tensors[0]->Dtype()); - for (const auto &tensor : tensors) { - CHECK_EQ(static_cast(tensors[0]->Dtype()), static_cast(tensor->Dtype())); - const auto &dims = tensor->Dims(); - CHECK_EQ(stacked_dim, std::accumulate(dims.begin(), dims.end(), 1, std::multiplies())); - } - - size_t offset = 0; - for (const auto &tensor : tensors) { - memcpy(reinterpret_cast(stacked_tensor->DataPtr()) + offset, tensor->DataPtr(), - tensor->SizeInBytes()); - offset += tensor->SizeInBytes(); - } - return stacked_tensor; -} -} // namespace - -DataLoaderIterator::DataLoaderIterator(const Dataset &dataset, size_t batch_size, size_t batch_idx, - size_t max_batch_idx) - : dataset_(&dataset), batch_size_(batch_size), batch_idx_(batch_idx), max_batch_idx_(max_batch_idx){}; - -std::pair, std::shared_ptr> DataLoaderIterator::operator*() const { - /* - 0, 1, ..., x, ... - [0, bs-1], [bs, 2*bs-1], ..., [x*bs, (x+1)*bs-1], ... - ^ - batch_idx - */ - std::vector> data_vec; - std::vector> label_vec; - for (int idx = batch_idx_ * batch_size_; idx < (batch_idx_ + 1) * batch_size_ && idx < dataset_->Size(); ++idx) { - auto &&[data, label] = dataset_->operator[](idx); - data_vec.push_back(std::move(data)); - label_vec.push_back(std::move(label)); - } - return {Stack(std::move(data_vec)), Stack(std::move(label_vec))}; -} - -DataLoaderIterator &DataLoaderIterator::operator++() { - batch_idx_ = std::min(batch_idx_ + 1, max_batch_idx_); - return *this; -} - -DataLoaderIterator DataLoaderIterator::operator++(int) { - DataLoaderIterator tmp(*this); - ++(*this); - return tmp; -} - -bool operator<(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs) { return lhs.batch_idx_ < rhs.batch_idx_; } - -bool operator!=(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs) { - return lhs.batch_idx_ != rhs.batch_idx_; -} - -bool operator==(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs) { - return lhs.batch_idx_ == rhs.batch_idx_; -} - -DataLoader::DataLoader(const std::shared_ptr &dataset, size_t batch_size) - : dataset_(dataset), batch_size_(batch_size), max_batch_idx_((dataset_->Size() + batch_size_ - 1) / batch_size_) {} - -DataLoaderIterator DataLoader::begin() const { return DataLoaderIterator(*dataset_, batch_size_, 0, max_batch_idx_); } - -DataLoaderIterator DataLoader::end() const { - return DataLoaderIterator(*dataset_, batch_size_, max_batch_idx_, max_batch_idx_); -} -} // namespace infini_train +#include "infini_train/include/dataloader.h" + +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dataset.h" +#include "infini_train/include/tensor.h" + +namespace infini_train { +namespace { +std::shared_ptr Stack(const std::vector> &tensors) { + const int batch_size = tensors.size(); + const auto &dims = tensors[0]->Dims(); + const int stacked_dim = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); + auto stacked_tensor = std::make_shared(std::vector{batch_size, stacked_dim}, tensors[0]->Dtype()); + for (const auto &tensor : tensors) { + CHECK_EQ(static_cast(tensors[0]->Dtype()), static_cast(tensor->Dtype())); + const auto &dims = tensor->Dims(); + CHECK_EQ(stacked_dim, std::accumulate(dims.begin(), dims.end(), 1, std::multiplies())); + } + + size_t offset = 0; + for (const auto &tensor : tensors) { + memcpy(reinterpret_cast(stacked_tensor->DataPtr()) + offset, tensor->DataPtr(), + tensor->SizeInBytes()); + offset += tensor->SizeInBytes(); + } + return stacked_tensor; +} +} // namespace + +DataLoaderIterator::DataLoaderIterator(const Dataset &dataset, size_t batch_size, size_t batch_idx, + size_t max_batch_idx) + : dataset_(&dataset), batch_size_(batch_size), batch_idx_(batch_idx), max_batch_idx_(max_batch_idx){}; + +std::pair, std::shared_ptr> DataLoaderIterator::operator*() const { + /* + 0, 1, ..., x, ... + [0, bs-1], [bs, 2*bs-1], ..., [x*bs, (x+1)*bs-1], ... + ^ + batch_idx + */ + std::vector> data_vec; + std::vector> label_vec; + for (int idx = batch_idx_ * batch_size_; idx < (batch_idx_ + 1) * batch_size_ && idx < dataset_->Size(); ++idx) { + auto &&[data, label] = dataset_->operator[](idx); + data_vec.push_back(std::move(data)); + label_vec.push_back(std::move(label)); + } + return {Stack(std::move(data_vec)), Stack(std::move(label_vec))}; +} + +DataLoaderIterator &DataLoaderIterator::operator++() { + batch_idx_ = std::min(batch_idx_ + 1, max_batch_idx_); + return *this; +} + +DataLoaderIterator DataLoaderIterator::operator++(int) { + DataLoaderIterator tmp(*this); + ++(*this); + return tmp; +} + +bool operator<(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs) { return lhs.batch_idx_ < rhs.batch_idx_; } + +bool operator!=(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs) { + return lhs.batch_idx_ != rhs.batch_idx_; +} + +bool operator==(const DataLoaderIterator &lhs, const DataLoaderIterator &rhs) { + return lhs.batch_idx_ == rhs.batch_idx_; +} + +DataLoader::DataLoader(const std::shared_ptr &dataset, size_t batch_size) + : dataset_(dataset), batch_size_(batch_size), max_batch_idx_((dataset_->Size() + batch_size_ - 1) / batch_size_) {} + +DataLoaderIterator DataLoader::begin() const { return DataLoaderIterator(*dataset_, batch_size_, 0, max_batch_idx_); } + +DataLoaderIterator DataLoader::end() const { + return DataLoaderIterator(*dataset_, batch_size_, max_batch_idx_, max_batch_idx_); +} +} // namespace infini_train diff --git a/infini_train/src/device.cc b/infini_train/src/device.cc index cc6842f..00bfe2d 100644 --- a/infini_train/src/device.cc +++ b/infini_train/src/device.cc @@ -1,41 +1,41 @@ -#include "infini_train/include/device.h" - -#include - -#include "glog/logging.h" - -namespace infini_train { -Device::Device() : type_(DeviceType::kCPU), index_(0) {} - -Device::Device(DeviceType type, int8_t index) : type_(type), index_(index) { - if (type_ == DeviceType::kCPU && index_ != 0) { - LOG(FATAL) << "CPU device index should be 0"; - } - - if (type_ == DeviceType::kCUDA && index_ != 0) { - LOG(FATAL) << "CUDA device index should be 0"; - } -} - -bool Device::operator==(const Device &other) const { return type_ == other.type_ && index_ == other.index_; } - -bool Device::operator!=(const Device &other) const { return !(*this == other); } - -DeviceType Device::Type() const { return type_; } -int8_t Device::Index() const { return index_; } - -bool Device::IsCPU() const { return type_ == DeviceType::kCPU; } -bool Device::IsCUDA() const { return type_ == DeviceType::kCUDA; } - -std::string Device::ToString() const { - std::ostringstream oss; - oss << "Device(" << (type_ == DeviceType::kCPU ? "CPU" : "CUDA") << ", " << static_cast(index_) << ")"; - return oss.str(); -} - -std::ostream &operator<<(std::ostream &os, const Device &device) { - os << device.ToString(); - return os; -} - -} // namespace infini_train +#include "infini_train/include/device.h" + +#include + +#include "glog/logging.h" + +namespace infini_train { +Device::Device() : type_(DeviceType::kCPU), index_(0) {} + +Device::Device(DeviceType type, int8_t index) : type_(type), index_(index) { + if (type_ == DeviceType::kCPU && index_ != 0) { + LOG(FATAL) << "CPU device index should be 0"; + } + + if (type_ == DeviceType::kCUDA && index_ != 0) { + LOG(FATAL) << "CUDA device index should be 0"; + } +} + +bool Device::operator==(const Device &other) const { return type_ == other.type_ && index_ == other.index_; } + +bool Device::operator!=(const Device &other) const { return !(*this == other); } + +DeviceType Device::Type() const { return type_; } +int8_t Device::Index() const { return index_; } + +bool Device::IsCPU() const { return type_ == DeviceType::kCPU; } +bool Device::IsCUDA() const { return type_ == DeviceType::kCUDA; } + +std::string Device::ToString() const { + std::ostringstream oss; + oss << "Device(" << (type_ == DeviceType::kCPU ? "CPU" : "CUDA") << ", " << static_cast(index_) << ")"; + return oss.str(); +} + +std::ostream &operator<<(std::ostream &os, const Device &device) { + os << device.ToString(); + return os; +} + +} // namespace infini_train diff --git a/infini_train/src/dispatcher.cc b/infini_train/src/dispatcher.cc index fe33b5d..b252ad9 100644 --- a/infini_train/src/dispatcher.cc +++ b/infini_train/src/dispatcher.cc @@ -1,23 +1,23 @@ -#include "infini_train/include/dispatcher.h" - -namespace infini_train { - -// Dispatcher &Dispatcher::Instance() { -// static Dispatcher instance; -// return instance; -// } - -// bool Dispatcher::RegisterImpl(const DispatchKey &key, const std::function &kernel) { -// CHECK(dispatch_table_.find(key) == dispatch_table_.end()) -// << "Kernel already registered for key " << std::get<1>(key); -// dispatch_table_.emplace(key, kernel); -// return true; -// } - -// std::function Dispatcher::GetImpl(const DispatchKey &key) const { -// auto it = dispatch_table_.find(key); -// CHECK(it != dispatch_table_.end()) << "No kernel registered for key " << std::get<1>(key); -// return it->second; -// } - -} // namespace infini_train +#include "infini_train/include/dispatcher.h" + +namespace infini_train { + +// Dispatcher &Dispatcher::Instance() { +// static Dispatcher instance; +// return instance; +// } + +// bool Dispatcher::RegisterImpl(const DispatchKey &key, const std::function &kernel) { +// CHECK(dispatch_table_.find(key) == dispatch_table_.end()) +// << "Kernel already registered for key " << std::get<1>(key); +// dispatch_table_.emplace(key, kernel); +// return true; +// } + +// std::function Dispatcher::GetImpl(const DispatchKey &key) const { +// auto it = dispatch_table_.find(key); +// CHECK(it != dispatch_table_.end()) << "No kernel registered for key " << std::get<1>(key); +// return it->second; +// } + +} // namespace infini_train diff --git a/infini_train/src/kernels/cpu/accumulate_grad.cc b/infini_train/src/kernels/cpu/accumulate_grad.cc index 55637cd..0232959 100644 --- a/infini_train/src/kernels/cpu/accumulate_grad.cc +++ b/infini_train/src/kernels/cpu/accumulate_grad.cc @@ -1,31 +1,48 @@ -#include -#include - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -void AccumulateGrad(const std::shared_ptr &gradient, float rate, const std::shared_ptr &tensor) { - for (int64_t idx = 0; idx < gradient->NumElements(); ++idx) { - static_cast(tensor->DataPtr())[idx] += rate * static_cast(gradient->DataPtr())[idx]; - } -} - -void AdamAccumulateGrad(const std::shared_ptr &grad, const std::shared_ptr ¶m, - const std::shared_ptr &m, const std::shared_ptr &v, float learning_rate, - float beta1, float beta2, float eps, int64_t t) { - // =================================== 作业 =================================== - // TODO:实现Adam优化器的梯度累积和参数更新 - // REF: - // =================================== 作业 =================================== -} - -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_ACCUMULATE_GRAD_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_ACCUMULATE_GRAD_KERNEL(AccumulateGrad) -REGISTER_CPU_ACCUMULATE_GRAD_KERNEL(AdamAccumulateGrad) - -#undef REGISTER_CPU_ACCUMULATE_GRAD_KERNEL +#include +#include + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +void AccumulateGrad(const std::shared_ptr &gradient, float rate, const std::shared_ptr &tensor) { + for (int64_t idx = 0; idx < gradient->NumElements(); ++idx) { + static_cast(tensor->DataPtr())[idx] += rate * static_cast(gradient->DataPtr())[idx]; + } +} + +void AdamAccumulateGrad(const std::shared_ptr &grad, const std::shared_ptr ¶m, + const std::shared_ptr &m, const std::shared_ptr &v, float learning_rate, + float beta1, float beta2, float eps, int64_t t) { + // =================================== 作业 =================================== + // TODO:实现Adam优化器的梯度累积和参数更新 + // REF: + // =================================== 作业 =================================== + + int64_t num_elements = grad->NumElements(); + float *grad_ptr = static_cast(grad->DataPtr()); + float *param_ptr = static_cast(param->DataPtr()); + float *m_ptr = static_cast(m->DataPtr()); + float *v_ptr = static_cast(v->DataPtr()); + + float beta1_t = std::pow(beta1, t); + float beta2_t = std::pow(beta2, t); + float alpha = learning_rate * std::sqrt(1.0f - beta2_t) / (1.0f - beta1_t); + + for (int64_t i = 0; i < num_elements; ++i) { + float g = grad_ptr[i]; + m_ptr[i] = beta1 * m_ptr[i] + (1.0f - beta1) * g; + v_ptr[i] = beta2 * v_ptr[i] + (1.0f - beta2) * g * g; + param_ptr[i] -= alpha * m_ptr[i] / (std::sqrt(v_ptr[i]) + eps); + } +} + +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_ACCUMULATE_GRAD_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_ACCUMULATE_GRAD_KERNEL(AccumulateGrad) +REGISTER_CPU_ACCUMULATE_GRAD_KERNEL(AdamAccumulateGrad) + +#undef REGISTER_CPU_ACCUMULATE_GRAD_KERNEL diff --git a/infini_train/src/kernels/cpu/cross_entropy.cc b/infini_train/src/kernels/cpu/cross_entropy.cc index 2f6b4cc..427c57a 100644 --- a/infini_train/src/kernels/cpu/cross_entropy.cc +++ b/infini_train/src/kernels/cpu/cross_entropy.cc @@ -1,105 +1,105 @@ -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -namespace { -constexpr float kNegativeInfinity = -std::numeric_limits::infinity(); -} - -std::shared_ptr CrossEntropyForward(const std::shared_ptr &input, - const std::shared_ptr &target) { - const auto &input_dims = input->Dims(); - CHECK_GE(input_dims.size(), 2); - const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); - const int num_classes = *input_dims.rbegin(); - - auto output = std::make_shared(std::vector{}, DataType::kFLOAT32); - static_cast(output->DataPtr())[0] = 0.0f; - for (int64_t i = 0; i < bs; ++i) { - float max_logit = kNegativeInfinity; - for (int64_t j = 0; j < num_classes; ++j) { - max_logit = std::max(max_logit, static_cast(input->DataPtr())[i * num_classes + j]); - } - float sum_exp = 0.0f; - for (int64_t j = 0; j < num_classes; ++j) { - sum_exp += exp(static_cast(input->DataPtr())[i * num_classes + j] - max_logit); - } - if (target->Dtype() == DataType::kUINT8) { - static_cast(output->DataPtr())[0] - -= log(exp(static_cast( - input->DataPtr())[i * num_classes + static_cast(target->DataPtr())[i]] - - max_logit) - / sum_exp); - } else if (target->Dtype() == DataType::kINT64) { - static_cast(output->DataPtr())[0] - -= log(exp(static_cast( - input->DataPtr())[i * num_classes + static_cast(target->DataPtr())[i]] - - max_logit) - / sum_exp); - } else { - LOG(FATAL) << "Unsupported target data type: " << static_cast(target->Dtype()); - } - } - static_cast(output->DataPtr())[0] /= bs; - return {output}; -} - -std::shared_ptr CrossEntropyBackward(const std::shared_ptr &input, - const std::shared_ptr &target, - const std::shared_ptr &grad_output) { - const auto &input_dims = input->Dims(); - CHECK_GE(input_dims.size(), 2); - const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); - const int num_classes = *input_dims.rbegin(); - - CHECK_EQ(grad_output->Dims().size(), 0); - auto grad_input = std::make_shared(input->Dims(), DataType::kFLOAT32); - std::vector softmax(bs * num_classes, 0.0f); - for (int64_t i = 0; i < bs; ++i) { - float max_logit = kNegativeInfinity; - for (int64_t j = 0; j < num_classes; ++j) { - max_logit = std::max(max_logit, static_cast(input->DataPtr())[i * num_classes + j]); - } - float sum_exp = 0.0f; - for (int64_t j = 0; j < num_classes; ++j) { - sum_exp += exp(static_cast(input->DataPtr())[i * num_classes + j] - max_logit); - } - for (int64_t j = 0; j < num_classes; ++j) { - const auto idx = i * num_classes + j; - softmax[idx] = exp(static_cast(input->DataPtr())[idx] - max_logit) / sum_exp; - } - } - for (int64_t i = 0; i < bs; ++i) { - auto target_idx = 0; - if (target->Dtype() == DataType::kUINT8) { - target_idx = static_cast(target->DataPtr())[i]; - } else if (target->Dtype() == DataType::kINT64) { - target_idx = static_cast(target->DataPtr())[i]; - } else { - LOG(FATAL) << "Unsupported target data type: " << static_cast(target->Dtype()); - } - for (int64_t j = 0; j < num_classes; ++j) { - const auto idx = i * num_classes + j; - static_cast(grad_input->DataPtr())[idx] = static_cast(grad_output->DataPtr())[0] - * (softmax[idx] - (j == target_idx ? 1.0f : 0.0f)) / bs; - } - } - return {grad_input}; -} -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_CROSS_ENTROPY_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_CROSS_ENTROPY_KERNEL(CrossEntropyForward) -REGISTER_CPU_CROSS_ENTROPY_KERNEL(CrossEntropyBackward) - -#undef REGISTER_CPU_CROSS_ENTROPY_KERNEL +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +namespace { +constexpr float kNegativeInfinity = -std::numeric_limits::infinity(); +} + +std::shared_ptr CrossEntropyForward(const std::shared_ptr &input, + const std::shared_ptr &target) { + const auto &input_dims = input->Dims(); + CHECK_GE(input_dims.size(), 2); + const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); + const int num_classes = *input_dims.rbegin(); + + auto output = std::make_shared(std::vector{}, DataType::kFLOAT32); + static_cast(output->DataPtr())[0] = 0.0f; + for (int64_t i = 0; i < bs; ++i) { + float max_logit = kNegativeInfinity; + for (int64_t j = 0; j < num_classes; ++j) { + max_logit = std::max(max_logit, static_cast(input->DataPtr())[i * num_classes + j]); + } + float sum_exp = 0.0f; + for (int64_t j = 0; j < num_classes; ++j) { + sum_exp += exp(static_cast(input->DataPtr())[i * num_classes + j] - max_logit); + } + if (target->Dtype() == DataType::kUINT8) { + static_cast(output->DataPtr())[0] + -= log(exp(static_cast( + input->DataPtr())[i * num_classes + static_cast(target->DataPtr())[i]] + - max_logit) + / sum_exp); + } else if (target->Dtype() == DataType::kINT64) { + static_cast(output->DataPtr())[0] + -= log(exp(static_cast( + input->DataPtr())[i * num_classes + static_cast(target->DataPtr())[i]] + - max_logit) + / sum_exp); + } else { + LOG(FATAL) << "Unsupported target data type: " << static_cast(target->Dtype()); + } + } + static_cast(output->DataPtr())[0] /= bs; + return {output}; +} + +std::shared_ptr CrossEntropyBackward(const std::shared_ptr &input, + const std::shared_ptr &target, + const std::shared_ptr &grad_output) { + const auto &input_dims = input->Dims(); + CHECK_GE(input_dims.size(), 2); + const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); + const int num_classes = *input_dims.rbegin(); + + CHECK_EQ(grad_output->Dims().size(), 0); + auto grad_input = std::make_shared(input->Dims(), DataType::kFLOAT32); + std::vector softmax(bs * num_classes, 0.0f); + for (int64_t i = 0; i < bs; ++i) { + float max_logit = kNegativeInfinity; + for (int64_t j = 0; j < num_classes; ++j) { + max_logit = std::max(max_logit, static_cast(input->DataPtr())[i * num_classes + j]); + } + float sum_exp = 0.0f; + for (int64_t j = 0; j < num_classes; ++j) { + sum_exp += exp(static_cast(input->DataPtr())[i * num_classes + j] - max_logit); + } + for (int64_t j = 0; j < num_classes; ++j) { + const auto idx = i * num_classes + j; + softmax[idx] = exp(static_cast(input->DataPtr())[idx] - max_logit) / sum_exp; + } + } + for (int64_t i = 0; i < bs; ++i) { + auto target_idx = 0; + if (target->Dtype() == DataType::kUINT8) { + target_idx = static_cast(target->DataPtr())[i]; + } else if (target->Dtype() == DataType::kINT64) { + target_idx = static_cast(target->DataPtr())[i]; + } else { + LOG(FATAL) << "Unsupported target data type: " << static_cast(target->Dtype()); + } + for (int64_t j = 0; j < num_classes; ++j) { + const auto idx = i * num_classes + j; + static_cast(grad_input->DataPtr())[idx] = static_cast(grad_output->DataPtr())[0] + * (softmax[idx] - (j == target_idx ? 1.0f : 0.0f)) / bs; + } + } + return {grad_input}; +} +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_CROSS_ENTROPY_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_CROSS_ENTROPY_KERNEL(CrossEntropyForward) +REGISTER_CPU_CROSS_ENTROPY_KERNEL(CrossEntropyBackward) + +#undef REGISTER_CPU_CROSS_ENTROPY_KERNEL diff --git a/infini_train/src/kernels/cpu/elementwise.cc b/infini_train/src/kernels/cpu/elementwise.cc index b9e02d4..00a21de 100644 --- a/infini_train/src/kernels/cpu/elementwise.cc +++ b/infini_train/src/kernels/cpu/elementwise.cc @@ -1,295 +1,295 @@ -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/device.h" -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -namespace { -std::vector ComputeStrides(const std::vector &dims) { - std::vector strides(dims.size()); - int64_t stride = 1; - for (int i = dims.size() - 1; i >= 0; --i) { - strides[i] = stride; - stride *= dims[i]; - } - return strides; -} - -std::shared_ptr UnaryForward(const std::shared_ptr &input, std::function unary_fn) { - auto output = std::make_shared(input->Dims(), DataType::kFLOAT32); - for (int64_t idx = 0; idx < output->NumElements(); ++idx) { - static_cast(output->DataPtr())[idx] = unary_fn(static_cast(input->DataPtr())[idx]); - } - return output; -} - -std::shared_ptr UnaryBackward(const std::shared_ptr &grad_output, const std::shared_ptr &a, - std::function unary_fn) { - auto grad_input = std::make_shared(grad_output->Dims(), DataType::kFLOAT32); - for (int idx = 0; idx < grad_input->NumElements(); ++idx) { - const float x = a ? static_cast(a->DataPtr())[idx] : 0.0f; - const float grad = static_cast(grad_output->DataPtr())[idx]; - static_cast(grad_input->DataPtr())[idx] = grad * unary_fn(x); - } - return grad_input; -} - -std::shared_ptr BinaryForward(const std::shared_ptr &a, const std::shared_ptr &b, - std::function binary_fn) { - // Currently a and b should have the same data type and only one-way broadcasting from b to a is assumed by default - CHECK(a->Dtype() == b->Dtype() && a->NumElements() >= b->NumElements() && a->NumElements() % b->NumElements() == 0); - auto out_dims = a->Dims(); - auto output = std::make_shared(out_dims, DataType::kFLOAT32); - int64_t num_elements = output->NumElements(); - - auto out_strides = ComputeStrides(out_dims); - auto b_strides = ComputeStrides(b->Dims()); - - int ndim = out_dims.size(); - std::vector b_padded_dims = b->Dims(); - b_padded_dims.insert(b_padded_dims.begin(), ndim - b_padded_dims.size(), 1); - b_strides.insert(b_strides.begin(), ndim - b_strides.size(), 0); - - float *out_ptr = static_cast(output->DataPtr()); - const float *a_ptr = static_cast(a->DataPtr()); - const float *b_ptr = static_cast(b->DataPtr()); - - for (int64_t idx = 0; idx < num_elements; ++idx) { - int64_t tmp = idx, b_offset = 0; - for (int i = 0; i < ndim; ++i) { - int64_t index = tmp / out_strides[i]; - tmp %= out_strides[i]; - b_offset += (b_padded_dims[i] == 1 ? 0 : index) * b_strides[i]; - } - out_ptr[idx] = binary_fn(a_ptr[idx], b_ptr[b_offset]); - } - - return output; -} - -std::pair, std::shared_ptr> -BinaryBackward(const std::shared_ptr &grad_output, const std::shared_ptr &a, - const std::shared_ptr &b, const std::vector &a_dims, const std::vector &b_dims, - std::function fn_a, std::function fn_b) { - // Currently a and b should have the same data type and only one-way broadcasting from b to a is assumed by default - CHECK(a_dims.size() >= b_dims.size()); - - const int64_t a_num_elements = std::accumulate(a_dims.begin(), a_dims.end(), 1LL, std::multiplies()); - const int64_t b_num_elements = std::accumulate(b_dims.begin(), b_dims.end(), 1LL, std::multiplies()); - - CHECK(a_num_elements == grad_output->NumElements()); - if (a) { - CHECK_EQ(a_num_elements, a->NumElements()); - } - if (b) { - CHECK_EQ(b_num_elements, b->NumElements()); - } - - auto grad_a = std::make_shared(a_dims, DataType::kFLOAT32); - auto grad_b = std::make_shared(b_dims, DataType::kFLOAT32); - grad_a->Fill(0.0f); - grad_b->Fill(0.0f); - - int ndim = a_dims.size(); - auto out_strides = ComputeStrides(a_dims); - auto b_strides = ComputeStrides(b_dims); - std::vector b_padded_dims = b_dims; - b_padded_dims.insert(b_padded_dims.begin(), ndim - b_dims.size(), 1); - b_strides.insert(b_strides.begin(), ndim - b_strides.size(), 0); - - for (int64_t idx = 0; idx < a_num_elements; ++idx) { - int64_t tmp = idx, b_offset = 0; - for (size_t i = 0; i < ndim; ++i) { - int64_t index = tmp / out_strides[i]; - tmp %= out_strides[i]; - b_offset += (b_padded_dims[i] == 1 ? 0 : index) * b_strides[i]; - } - - const float x = a ? static_cast(a->DataPtr())[idx] : 0.0f; - const float y = b ? static_cast(b->DataPtr())[b_offset] : 0.0f; - const float grad = static_cast(grad_output->DataPtr())[idx]; - - static_cast(grad_a->DataPtr())[idx] = grad * fn_a(x, y); - static_cast(grad_b->DataPtr())[b_offset] += grad * fn_b(x, y); - } - return {grad_a, grad_b}; -} -} // namespace - -std::shared_ptr NegForward(const std::shared_ptr &input) { - return UnaryForward(input, [](float x) { return -x; }); -} - -std::shared_ptr NegBackward(const std::shared_ptr &grad_output) { - return UnaryBackward(grad_output, nullptr, [](float) { return -1.0f; }); -} - -std::shared_ptr ReciprocalForward(const std::shared_ptr &input) { - return UnaryForward(input, [](float x) { return 1.0f / x; }); -} - -std::shared_ptr ReciprocalBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &input) { - return UnaryBackward(grad_output, input, [](float x) { return -1.0f / (x * x); }); -} - -std::shared_ptr SinForward(const std::shared_ptr &input) { - return UnaryForward(input, [](float x) { return sinf(x); }); -} - -std::shared_ptr SinBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input) { - return UnaryBackward(grad_output, input, [](float x) { return cosf(x); }); -} - -std::shared_ptr CosForward(const std::shared_ptr &input) { - return UnaryForward(input, [](float x) { return cosf(x); }); -} - -std::shared_ptr CosBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input) { - return UnaryBackward(grad_output, input, [](float x) { return -sinf(x); }); -} - -std::shared_ptr TanhForward(const std::shared_ptr &input) { - return UnaryForward(input, [](float x) { return tanhf(x); }); -} - -std::shared_ptr TanhBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &output) { - return UnaryBackward(grad_output, output, [](float x) { return 1.0 - x * x; }); -} - -std::shared_ptr PowForward(const std::shared_ptr &input, float scalar, bool scalar_is_base) { - if (scalar_is_base) { - return UnaryForward(input, [scalar](float x) { return powf(scalar, x); }); - } else { - return UnaryForward(input, [scalar](float x) { return powf(x, scalar); }); - } -} - -std::shared_ptr PowBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, - float scalar, bool scalar_is_base) { - if (scalar_is_base) { - return UnaryBackward(grad_output, input, [scalar](float x) { return std::log(scalar) * powf(scalar, x); }); - } else { - return UnaryBackward(grad_output, input, [scalar](float x) { return scalar * powf(x, scalar - 1.0f); }); - } -} - -std::shared_ptr RsqrtForward(const std::shared_ptr &input) { - return UnaryForward(input, [](float x) { return 1.0f / std::sqrt(x); }); -} - -std::shared_ptr RsqrtBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &input) { - return UnaryBackward(grad_output, input, [](float x) { return -0.5f / (x * std::sqrt(x)); }); -} - -std::shared_ptr EqualsScalarForward(const std::shared_ptr &a, float scalar) { - return UnaryForward(a, [scalar](float x) { return x == scalar ? 1.0f : 0.0f; }); -} - -std::shared_ptr AddForward(const std::shared_ptr &a, const std::shared_ptr &b) { - return BinaryForward(a, b, [](float x, float y) { return x + y; }); -} - -std::pair, std::shared_ptr> AddBackward(const std::shared_ptr &grad_output, - const std::vector &a_dims, - const std::vector &b_dims) { - - return BinaryBackward( - grad_output, nullptr, nullptr, a_dims, b_dims, [](float, float) { return 1.0f; }, - [](float, float) { return 1.0f; }); -} - -std::shared_ptr AddScalarForward(const std::shared_ptr &a, float scalar) { - return UnaryForward(a, [scalar](float x) { return x + scalar; }); -} - -std::shared_ptr AddScalarBackward(const std::shared_ptr &grad_output) { - return UnaryBackward(grad_output, nullptr, [](float) { return 1.0f; }); -} - -std::shared_ptr SubForward(const std::shared_ptr &a, const std::shared_ptr &b) { - return BinaryForward(a, b, [](float x, float y) { return x - y; }); -} - -std::pair, std::shared_ptr> SubBackward(const std::shared_ptr &grad_output, - const std::vector &a_dims, - const std::vector &b_dims) { - return BinaryBackward( - grad_output, nullptr, nullptr, a_dims, b_dims, [](float, float) { return 1.f; }, - [](float, float) { return -1.f; }); -} - -std::shared_ptr MulForward(const std::shared_ptr &a, const std::shared_ptr &b) { - return BinaryForward(a, b, [](float x, float y) { return x * y; }); -} - -std::pair, std::shared_ptr> MulBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &a, - const std::shared_ptr &b) { - return BinaryBackward( - grad_output, a, b, a->Dims(), b->Dims(), [](float, float y) { return y; }, [](float x, float) { return x; }); -} - -std::shared_ptr MulScalarForward(const std::shared_ptr &a, float scalar) { - return UnaryForward(a, [scalar](float x) { return x * scalar; }); -} - -std::shared_ptr MulScalarBackward(const std::shared_ptr &grad_output, float scalar) { - return UnaryBackward(grad_output, nullptr, [scalar](float) { return scalar; }); -} - -std::shared_ptr DivForward(const std::shared_ptr &a, const std::shared_ptr &b) { - return BinaryForward(a, b, [](float x, float y) { return x / y; }); -} - -std::pair, std::shared_ptr> DivBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &a, - const std::shared_ptr &b) { - return BinaryBackward( - grad_output, a, b, a->Dims(), b->Dims(), [](float, float y) { return 1 / y; }, - [](float x, float y) { return -x / (y * y); }); -} - -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_ELEMENTWISE_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_ELEMENTWISE_KERNEL(NegForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(NegBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(ReciprocalForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(ReciprocalBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(SinForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(SinBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(CosForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(CosBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(TanhForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(TanhBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(PowForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(PowBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(RsqrtForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(RsqrtBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(EqualsScalarForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(AddForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(AddBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(AddScalarForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(AddScalarBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(SubForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(SubBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(MulForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(MulBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(MulScalarForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(MulScalarBackward) -REGISTER_CPU_ELEMENTWISE_KERNEL(DivForward) -REGISTER_CPU_ELEMENTWISE_KERNEL(DivBackward) - -#undef REGISTER_CPU_ELEMENTWISE_KERNEL +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/device.h" +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +namespace { +std::vector ComputeStrides(const std::vector &dims) { + std::vector strides(dims.size()); + int64_t stride = 1; + for (int i = dims.size() - 1; i >= 0; --i) { + strides[i] = stride; + stride *= dims[i]; + } + return strides; +} + +std::shared_ptr UnaryForward(const std::shared_ptr &input, std::function unary_fn) { + auto output = std::make_shared(input->Dims(), DataType::kFLOAT32); + for (int64_t idx = 0; idx < output->NumElements(); ++idx) { + static_cast(output->DataPtr())[idx] = unary_fn(static_cast(input->DataPtr())[idx]); + } + return output; +} + +std::shared_ptr UnaryBackward(const std::shared_ptr &grad_output, const std::shared_ptr &a, + std::function unary_fn) { + auto grad_input = std::make_shared(grad_output->Dims(), DataType::kFLOAT32); + for (int idx = 0; idx < grad_input->NumElements(); ++idx) { + const float x = a ? static_cast(a->DataPtr())[idx] : 0.0f; + const float grad = static_cast(grad_output->DataPtr())[idx]; + static_cast(grad_input->DataPtr())[idx] = grad * unary_fn(x); + } + return grad_input; +} + +std::shared_ptr BinaryForward(const std::shared_ptr &a, const std::shared_ptr &b, + std::function binary_fn) { + // Currently a and b should have the same data type and only one-way broadcasting from b to a is assumed by default + CHECK(a->Dtype() == b->Dtype() && a->NumElements() >= b->NumElements() && a->NumElements() % b->NumElements() == 0); + auto out_dims = a->Dims(); + auto output = std::make_shared(out_dims, DataType::kFLOAT32); + int64_t num_elements = output->NumElements(); + + auto out_strides = ComputeStrides(out_dims); + auto b_strides = ComputeStrides(b->Dims()); + + int ndim = out_dims.size(); + std::vector b_padded_dims = b->Dims(); + b_padded_dims.insert(b_padded_dims.begin(), ndim - b_padded_dims.size(), 1); + b_strides.insert(b_strides.begin(), ndim - b_strides.size(), 0); + + float *out_ptr = static_cast(output->DataPtr()); + const float *a_ptr = static_cast(a->DataPtr()); + const float *b_ptr = static_cast(b->DataPtr()); + + for (int64_t idx = 0; idx < num_elements; ++idx) { + int64_t tmp = idx, b_offset = 0; + for (int i = 0; i < ndim; ++i) { + int64_t index = tmp / out_strides[i]; + tmp %= out_strides[i]; + b_offset += (b_padded_dims[i] == 1 ? 0 : index) * b_strides[i]; + } + out_ptr[idx] = binary_fn(a_ptr[idx], b_ptr[b_offset]); + } + + return output; +} + +std::pair, std::shared_ptr> +BinaryBackward(const std::shared_ptr &grad_output, const std::shared_ptr &a, + const std::shared_ptr &b, const std::vector &a_dims, const std::vector &b_dims, + std::function fn_a, std::function fn_b) { + // Currently a and b should have the same data type and only one-way broadcasting from b to a is assumed by default + CHECK(a_dims.size() >= b_dims.size()); + + const int64_t a_num_elements = std::accumulate(a_dims.begin(), a_dims.end(), 1LL, std::multiplies()); + const int64_t b_num_elements = std::accumulate(b_dims.begin(), b_dims.end(), 1LL, std::multiplies()); + + CHECK(a_num_elements == grad_output->NumElements()); + if (a) { + CHECK_EQ(a_num_elements, a->NumElements()); + } + if (b) { + CHECK_EQ(b_num_elements, b->NumElements()); + } + + auto grad_a = std::make_shared(a_dims, DataType::kFLOAT32); + auto grad_b = std::make_shared(b_dims, DataType::kFLOAT32); + grad_a->Fill(0.0f); + grad_b->Fill(0.0f); + + int ndim = a_dims.size(); + auto out_strides = ComputeStrides(a_dims); + auto b_strides = ComputeStrides(b_dims); + std::vector b_padded_dims = b_dims; + b_padded_dims.insert(b_padded_dims.begin(), ndim - b_dims.size(), 1); + b_strides.insert(b_strides.begin(), ndim - b_strides.size(), 0); + + for (int64_t idx = 0; idx < a_num_elements; ++idx) { + int64_t tmp = idx, b_offset = 0; + for (size_t i = 0; i < ndim; ++i) { + int64_t index = tmp / out_strides[i]; + tmp %= out_strides[i]; + b_offset += (b_padded_dims[i] == 1 ? 0 : index) * b_strides[i]; + } + + const float x = a ? static_cast(a->DataPtr())[idx] : 0.0f; + const float y = b ? static_cast(b->DataPtr())[b_offset] : 0.0f; + const float grad = static_cast(grad_output->DataPtr())[idx]; + + static_cast(grad_a->DataPtr())[idx] = grad * fn_a(x, y); + static_cast(grad_b->DataPtr())[b_offset] += grad * fn_b(x, y); + } + return {grad_a, grad_b}; +} +} // namespace + +std::shared_ptr NegForward(const std::shared_ptr &input) { + return UnaryForward(input, [](float x) { return -x; }); +} + +std::shared_ptr NegBackward(const std::shared_ptr &grad_output) { + return UnaryBackward(grad_output, nullptr, [](float) { return -1.0f; }); +} + +std::shared_ptr ReciprocalForward(const std::shared_ptr &input) { + return UnaryForward(input, [](float x) { return 1.0f / x; }); +} + +std::shared_ptr ReciprocalBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &input) { + return UnaryBackward(grad_output, input, [](float x) { return -1.0f / (x * x); }); +} + +std::shared_ptr SinForward(const std::shared_ptr &input) { + return UnaryForward(input, [](float x) { return sinf(x); }); +} + +std::shared_ptr SinBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input) { + return UnaryBackward(grad_output, input, [](float x) { return cosf(x); }); +} + +std::shared_ptr CosForward(const std::shared_ptr &input) { + return UnaryForward(input, [](float x) { return cosf(x); }); +} + +std::shared_ptr CosBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input) { + return UnaryBackward(grad_output, input, [](float x) { return -sinf(x); }); +} + +std::shared_ptr TanhForward(const std::shared_ptr &input) { + return UnaryForward(input, [](float x) { return tanhf(x); }); +} + +std::shared_ptr TanhBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &output) { + return UnaryBackward(grad_output, output, [](float x) { return 1.0 - x * x; }); +} + +std::shared_ptr PowForward(const std::shared_ptr &input, float scalar, bool scalar_is_base) { + if (scalar_is_base) { + return UnaryForward(input, [scalar](float x) { return powf(scalar, x); }); + } else { + return UnaryForward(input, [scalar](float x) { return powf(x, scalar); }); + } +} + +std::shared_ptr PowBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, + float scalar, bool scalar_is_base) { + if (scalar_is_base) { + return UnaryBackward(grad_output, input, [scalar](float x) { return std::log(scalar) * powf(scalar, x); }); + } else { + return UnaryBackward(grad_output, input, [scalar](float x) { return scalar * powf(x, scalar - 1.0f); }); + } +} + +std::shared_ptr RsqrtForward(const std::shared_ptr &input) { + return UnaryForward(input, [](float x) { return 1.0f / std::sqrt(x); }); +} + +std::shared_ptr RsqrtBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &input) { + return UnaryBackward(grad_output, input, [](float x) { return -0.5f / (x * std::sqrt(x)); }); +} + +std::shared_ptr EqualsScalarForward(const std::shared_ptr &a, float scalar) { + return UnaryForward(a, [scalar](float x) { return x == scalar ? 1.0f : 0.0f; }); +} + +std::shared_ptr AddForward(const std::shared_ptr &a, const std::shared_ptr &b) { + return BinaryForward(a, b, [](float x, float y) { return x + y; }); +} + +std::pair, std::shared_ptr> AddBackward(const std::shared_ptr &grad_output, + const std::vector &a_dims, + const std::vector &b_dims) { + + return BinaryBackward( + grad_output, nullptr, nullptr, a_dims, b_dims, [](float, float) { return 1.0f; }, + [](float, float) { return 1.0f; }); +} + +std::shared_ptr AddScalarForward(const std::shared_ptr &a, float scalar) { + return UnaryForward(a, [scalar](float x) { return x + scalar; }); +} + +std::shared_ptr AddScalarBackward(const std::shared_ptr &grad_output) { + return UnaryBackward(grad_output, nullptr, [](float) { return 1.0f; }); +} + +std::shared_ptr SubForward(const std::shared_ptr &a, const std::shared_ptr &b) { + return BinaryForward(a, b, [](float x, float y) { return x - y; }); +} + +std::pair, std::shared_ptr> SubBackward(const std::shared_ptr &grad_output, + const std::vector &a_dims, + const std::vector &b_dims) { + return BinaryBackward( + grad_output, nullptr, nullptr, a_dims, b_dims, [](float, float) { return 1.f; }, + [](float, float) { return -1.f; }); +} + +std::shared_ptr MulForward(const std::shared_ptr &a, const std::shared_ptr &b) { + return BinaryForward(a, b, [](float x, float y) { return x * y; }); +} + +std::pair, std::shared_ptr> MulBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &a, + const std::shared_ptr &b) { + return BinaryBackward( + grad_output, a, b, a->Dims(), b->Dims(), [](float, float y) { return y; }, [](float x, float) { return x; }); +} + +std::shared_ptr MulScalarForward(const std::shared_ptr &a, float scalar) { + return UnaryForward(a, [scalar](float x) { return x * scalar; }); +} + +std::shared_ptr MulScalarBackward(const std::shared_ptr &grad_output, float scalar) { + return UnaryBackward(grad_output, nullptr, [scalar](float) { return scalar; }); +} + +std::shared_ptr DivForward(const std::shared_ptr &a, const std::shared_ptr &b) { + return BinaryForward(a, b, [](float x, float y) { return x / y; }); +} + +std::pair, std::shared_ptr> DivBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &a, + const std::shared_ptr &b) { + return BinaryBackward( + grad_output, a, b, a->Dims(), b->Dims(), [](float, float y) { return 1 / y; }, + [](float x, float y) { return -x / (y * y); }); +} + +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_ELEMENTWISE_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_ELEMENTWISE_KERNEL(NegForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(NegBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(ReciprocalForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(ReciprocalBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(SinForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(SinBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(CosForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(CosBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(TanhForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(TanhBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(PowForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(PowBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(RsqrtForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(RsqrtBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(EqualsScalarForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(AddForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(AddBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(AddScalarForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(AddScalarBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(SubForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(SubBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(MulForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(MulBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(MulScalarForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(MulScalarBackward) +REGISTER_CPU_ELEMENTWISE_KERNEL(DivForward) +REGISTER_CPU_ELEMENTWISE_KERNEL(DivBackward) + +#undef REGISTER_CPU_ELEMENTWISE_KERNEL diff --git a/infini_train/src/kernels/cpu/embedding.cc b/infini_train/src/kernels/cpu/embedding.cc index f0462be..e742810 100644 --- a/infini_train/src/kernels/cpu/embedding.cc +++ b/infini_train/src/kernels/cpu/embedding.cc @@ -1,64 +1,64 @@ -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -std::shared_ptr EmbeddingForward(const std::shared_ptr &input, const std::shared_ptr &weight) { - /* - x: [*] - -> Embedding (weight: [num_embeddings, embedding_dim]) - -> o: [*, embedding_dim] - */ - CHECK(input->Dtype() == DataType::kINT64); - const auto &input_dims = input->Dims(); - CHECK_EQ(weight->Dims().size(), 2); - const int embedding_dim = weight->Dims()[1]; - auto output_dims = input_dims; - output_dims.push_back(embedding_dim); - auto output = std::make_shared(output_dims, DataType::kFLOAT32); - - for (int i = 0; i < input->NumElements(); i++) { - int idx = static_cast(static_cast(input->DataPtr())[i]); - for (int j = 0; j < embedding_dim; j++) { - static_cast(output->DataPtr())[i * embedding_dim + j] - = static_cast(weight->DataPtr())[idx * embedding_dim + j]; - } - } - - return output; -} - -std::shared_ptr EmbeddingBackward(const std::shared_ptr &input, const std::vector &weight_dims, - const std::shared_ptr &grad_output) { - CHECK(input->Dtype() == DataType::kINT64); - CHECK_EQ(weight_dims.size(), 2); - const int embedding_dim = weight_dims[1]; - CHECK_EQ(input->Dims().size() + 1, grad_output->Dims().size()); - for (int idx = 0; idx < input->Dims().size(); ++idx) { CHECK_EQ(input->Dims()[idx], grad_output->Dims()[idx]); } - CHECK_EQ(*grad_output->Dims().rbegin(), embedding_dim); - - auto grad_weight = std::make_shared(weight_dims, DataType::kFLOAT32); - grad_weight->Fill(0.0f); - - for (int i = 0; i < input->NumElements(); i++) { - int idx = static_cast(static_cast(input->DataPtr())[i]); - for (int j = 0; j < embedding_dim; j++) { - static_cast(grad_weight->DataPtr())[idx * embedding_dim + j] // <-- 修复这里 - += static_cast(grad_output->DataPtr())[i * embedding_dim + j]; - } - } - return grad_weight; -} - -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_EMBEDDING_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_EMBEDDING_KERNEL(EmbeddingForward) -REGISTER_CPU_EMBEDDING_KERNEL(EmbeddingBackward) - -#undef REGISTER_CPU_EMBEDDING_KERNEL +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +std::shared_ptr EmbeddingForward(const std::shared_ptr &input, const std::shared_ptr &weight) { + /* + x: [*] + -> Embedding (weight: [num_embeddings, embedding_dim]) + -> o: [*, embedding_dim] + */ + CHECK(input->Dtype() == DataType::kINT64); + const auto &input_dims = input->Dims(); + CHECK_EQ(weight->Dims().size(), 2); + const int embedding_dim = weight->Dims()[1]; + auto output_dims = input_dims; + output_dims.push_back(embedding_dim); + auto output = std::make_shared(output_dims, DataType::kFLOAT32); + + for (int i = 0; i < input->NumElements(); i++) { + int idx = static_cast(static_cast(input->DataPtr())[i]); + for (int j = 0; j < embedding_dim; j++) { + static_cast(output->DataPtr())[i * embedding_dim + j] + = static_cast(weight->DataPtr())[idx * embedding_dim + j]; + } + } + + return output; +} + +std::shared_ptr EmbeddingBackward(const std::shared_ptr &input, const std::vector &weight_dims, + const std::shared_ptr &grad_output) { + CHECK(input->Dtype() == DataType::kINT64); + CHECK_EQ(weight_dims.size(), 2); + const int embedding_dim = weight_dims[1]; + CHECK_EQ(input->Dims().size() + 1, grad_output->Dims().size()); + for (int idx = 0; idx < input->Dims().size(); ++idx) { CHECK_EQ(input->Dims()[idx], grad_output->Dims()[idx]); } + CHECK_EQ(*grad_output->Dims().rbegin(), embedding_dim); + + auto grad_weight = std::make_shared(weight_dims, DataType::kFLOAT32); + grad_weight->Fill(0.0f); + + for (int i = 0; i < input->NumElements(); i++) { + int idx = static_cast(static_cast(input->DataPtr())[i]); + for (int j = 0; j < embedding_dim; j++) { + static_cast(grad_weight->DataPtr())[idx * embedding_dim + j] // <-- 修复这里 + += static_cast(grad_output->DataPtr())[i * embedding_dim + j]; + } + } + return grad_weight; +} + +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_EMBEDDING_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_EMBEDDING_KERNEL(EmbeddingForward) +REGISTER_CPU_EMBEDDING_KERNEL(EmbeddingBackward) + +#undef REGISTER_CPU_EMBEDDING_KERNEL diff --git a/infini_train/src/kernels/cpu/fill.cc b/infini_train/src/kernels/cpu/fill.cc index 2e8fdbc..e2d4946 100644 --- a/infini_train/src/kernels/cpu/fill.cc +++ b/infini_train/src/kernels/cpu/fill.cc @@ -1,19 +1,19 @@ -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -void Fill(std::shared_ptr tensor, void *value_ptr) { - // FIXME(zbl): support other data types - auto data = reinterpret_cast(tensor->DataPtr()); - std::fill(data, data + tensor->NumElements(), *(static_cast(value_ptr))); -} -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_FILL_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_FILL_KERNEL(Fill) - -#undef REGISTER_CPU_FILL_KERNEL +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +void Fill(std::shared_ptr tensor, void *value_ptr) { + // FIXME(zbl): support other data types + auto data = reinterpret_cast(tensor->DataPtr()); + std::fill(data, data + tensor->NumElements(), *(static_cast(value_ptr))); +} +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_FILL_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_FILL_KERNEL(Fill) + +#undef REGISTER_CPU_FILL_KERNEL diff --git a/infini_train/src/kernels/cpu/layernorm.cc b/infini_train/src/kernels/cpu/layernorm.cc index f8fa458..192c262 100644 --- a/infini_train/src/kernels/cpu/layernorm.cc +++ b/infini_train/src/kernels/cpu/layernorm.cc @@ -1,146 +1,146 @@ -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -std::tuple, std::shared_ptr, std::shared_ptr> -LayerNormForward(const std::shared_ptr &input, const std::shared_ptr &weight, - const std::shared_ptr &bias, const float eps) { - /* - x: [bs, seq_len, embed_dim] - -> LayerNorm (w: [embed_dim], b: [embed_dim]) - -> o: [bs, seq_len, embed_dim] - */ - CHECK_EQ(input->Dims().size(), 3); - CHECK_LE(input->Dims()[2], weight->Dims()[0]); - CHECK_LE(input->Dims()[2], bias->Dims()[0]); - - const int batch_size = input->Dims()[0]; - const int max_seqlen = input->Dims()[1]; - const int embed_dim = input->Dims()[2]; - - auto output = std::make_shared(std::vector{batch_size, max_seqlen, embed_dim}, DataType::kFLOAT32); - auto mean = std::make_shared(std::vector{batch_size, max_seqlen}, DataType::kFLOAT32); - auto rstd = std::make_shared(std::vector{batch_size, max_seqlen}, DataType::kFLOAT32); - mean->Fill(0.0f); - rstd->Fill(0.0f); - - for (int b = 0; b < batch_size; b++) { - for (int t = 0; t < max_seqlen; t++) { - float m = 0.0f; - for (int i = 0; i < embed_dim; i++) { - m += static_cast(input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i]; - } - m = m / embed_dim; - - float v = 0.0f; - for (int i = 0; i < embed_dim; i++) { - float xshift - = static_cast(input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] - m; - v += xshift * xshift; - } - v = v / embed_dim; - - float s = 1.0f / sqrtf(v + eps); - - for (int i = 0; i < embed_dim; i++) { - float n = (s - * (static_cast(input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] - - m)); // normalize - float o = n * static_cast(weight->DataPtr())[i] - + static_cast(bias->DataPtr())[i]; // scale and shift - static_cast(output->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] = o; // write - } - // cache the mean and rstd for the backward pass later - static_cast(mean->DataPtr())[b * max_seqlen + t] = m; - static_cast(rstd->DataPtr())[b * max_seqlen + t] = s; - } - } - - return {output, mean, rstd}; -} - -std::tuple, std::shared_ptr, std::shared_ptr> -LayerNormBackward(const std::shared_ptr &input, const std::shared_ptr &weight, - const std::shared_ptr &bias, const std::shared_ptr &mean, - const std::shared_ptr &rstd, const std::shared_ptr &grad_output) { - CHECK_EQ(input->Dims().size(), 3); - CHECK_LE(input->Dims()[2], weight->Dims()[0]); - CHECK_LE(input->Dims()[2], bias->Dims()[0]); - CHECK_NE(mean, nullptr); - CHECK_NE(rstd, nullptr); - - const int batch_size = input->Dims()[0]; - const int max_seqlen = input->Dims()[1]; - const int embed_dim = input->Dims()[2]; - - auto grad_input = std::make_shared(input->Dims(), DataType::kFLOAT32); - auto grad_weight = std::make_shared(weight->Dims(), DataType::kFLOAT32); - auto grad_bias = std::make_shared(bias->Dims(), DataType::kFLOAT32); - - grad_input->Fill(0.0f); - grad_weight->Fill(0.0f); - grad_bias->Fill(0.0f); - - for (int b = 0; b < batch_size; b++) { - for (int t = 0; t < max_seqlen; t++) { - float mean_bt = static_cast(mean->DataPtr())[b * max_seqlen + t]; - float rstd_bt = static_cast(rstd->DataPtr())[b * max_seqlen + t]; - - // first: two reduce operations - float dnorm_mean = 0.0f; - float dnorm_norm_mean = 0.0f; - for (int i = 0; i < embed_dim; i++) { - float norm_bti - = (static_cast(input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] - mean_bt) - * rstd_bt; - float dnorm_i - = static_cast(weight->DataPtr())[i] - * static_cast(grad_output->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i]; - dnorm_mean += dnorm_i; - dnorm_norm_mean += dnorm_i * norm_bti; - } - dnorm_mean = dnorm_mean / embed_dim; - dnorm_norm_mean = dnorm_norm_mean / embed_dim; - - // now iterate again and accumulate all the gradients - for (int i = 0; i < embed_dim; i++) { - float norm_bti - = (static_cast(input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] - mean_bt) - * rstd_bt; - float dnorm_i - = static_cast(weight->DataPtr())[i] - * static_cast(grad_output->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i]; - // gradient contribution to bias - static_cast(grad_bias->DataPtr())[i] - += static_cast(grad_output->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i]; - // gradient contribution to weight - static_cast(grad_weight->DataPtr())[i] - += norm_bti - * static_cast(grad_output->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i]; - // gradient contribution to input - float dval = 0.0f; - dval += dnorm_i; // term 1 - dval -= dnorm_mean; // term 2 - dval -= norm_bti * dnorm_norm_mean; // term 3 - dval *= rstd_bt; // final scale - static_cast(grad_input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] += dval; - } - } - } - return {grad_input, grad_weight, grad_bias}; -} -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_LAYERNORM_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_LAYERNORM_KERNEL(LayerNormForward) -REGISTER_CPU_LAYERNORM_KERNEL(LayerNormBackward) - -#undef REGISTER_CPU_LAYERNORM_KERNEL +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +std::tuple, std::shared_ptr, std::shared_ptr> +LayerNormForward(const std::shared_ptr &input, const std::shared_ptr &weight, + const std::shared_ptr &bias, const float eps) { + /* + x: [bs, seq_len, embed_dim] + -> LayerNorm (w: [embed_dim], b: [embed_dim]) + -> o: [bs, seq_len, embed_dim] + */ + CHECK_EQ(input->Dims().size(), 3); + CHECK_LE(input->Dims()[2], weight->Dims()[0]); + CHECK_LE(input->Dims()[2], bias->Dims()[0]); + + const int batch_size = input->Dims()[0]; + const int max_seqlen = input->Dims()[1]; + const int embed_dim = input->Dims()[2]; + + auto output = std::make_shared(std::vector{batch_size, max_seqlen, embed_dim}, DataType::kFLOAT32); + auto mean = std::make_shared(std::vector{batch_size, max_seqlen}, DataType::kFLOAT32); + auto rstd = std::make_shared(std::vector{batch_size, max_seqlen}, DataType::kFLOAT32); + mean->Fill(0.0f); + rstd->Fill(0.0f); + + for (int b = 0; b < batch_size; b++) { + for (int t = 0; t < max_seqlen; t++) { + float m = 0.0f; + for (int i = 0; i < embed_dim; i++) { + m += static_cast(input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i]; + } + m = m / embed_dim; + + float v = 0.0f; + for (int i = 0; i < embed_dim; i++) { + float xshift + = static_cast(input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] - m; + v += xshift * xshift; + } + v = v / embed_dim; + + float s = 1.0f / sqrtf(v + eps); + + for (int i = 0; i < embed_dim; i++) { + float n = (s + * (static_cast(input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] + - m)); // normalize + float o = n * static_cast(weight->DataPtr())[i] + + static_cast(bias->DataPtr())[i]; // scale and shift + static_cast(output->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] = o; // write + } + // cache the mean and rstd for the backward pass later + static_cast(mean->DataPtr())[b * max_seqlen + t] = m; + static_cast(rstd->DataPtr())[b * max_seqlen + t] = s; + } + } + + return {output, mean, rstd}; +} + +std::tuple, std::shared_ptr, std::shared_ptr> +LayerNormBackward(const std::shared_ptr &input, const std::shared_ptr &weight, + const std::shared_ptr &bias, const std::shared_ptr &mean, + const std::shared_ptr &rstd, const std::shared_ptr &grad_output) { + CHECK_EQ(input->Dims().size(), 3); + CHECK_LE(input->Dims()[2], weight->Dims()[0]); + CHECK_LE(input->Dims()[2], bias->Dims()[0]); + CHECK_NE(mean, nullptr); + CHECK_NE(rstd, nullptr); + + const int batch_size = input->Dims()[0]; + const int max_seqlen = input->Dims()[1]; + const int embed_dim = input->Dims()[2]; + + auto grad_input = std::make_shared(input->Dims(), DataType::kFLOAT32); + auto grad_weight = std::make_shared(weight->Dims(), DataType::kFLOAT32); + auto grad_bias = std::make_shared(bias->Dims(), DataType::kFLOAT32); + + grad_input->Fill(0.0f); + grad_weight->Fill(0.0f); + grad_bias->Fill(0.0f); + + for (int b = 0; b < batch_size; b++) { + for (int t = 0; t < max_seqlen; t++) { + float mean_bt = static_cast(mean->DataPtr())[b * max_seqlen + t]; + float rstd_bt = static_cast(rstd->DataPtr())[b * max_seqlen + t]; + + // first: two reduce operations + float dnorm_mean = 0.0f; + float dnorm_norm_mean = 0.0f; + for (int i = 0; i < embed_dim; i++) { + float norm_bti + = (static_cast(input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] - mean_bt) + * rstd_bt; + float dnorm_i + = static_cast(weight->DataPtr())[i] + * static_cast(grad_output->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i]; + dnorm_mean += dnorm_i; + dnorm_norm_mean += dnorm_i * norm_bti; + } + dnorm_mean = dnorm_mean / embed_dim; + dnorm_norm_mean = dnorm_norm_mean / embed_dim; + + // now iterate again and accumulate all the gradients + for (int i = 0; i < embed_dim; i++) { + float norm_bti + = (static_cast(input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] - mean_bt) + * rstd_bt; + float dnorm_i + = static_cast(weight->DataPtr())[i] + * static_cast(grad_output->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i]; + // gradient contribution to bias + static_cast(grad_bias->DataPtr())[i] + += static_cast(grad_output->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i]; + // gradient contribution to weight + static_cast(grad_weight->DataPtr())[i] + += norm_bti + * static_cast(grad_output->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i]; + // gradient contribution to input + float dval = 0.0f; + dval += dnorm_i; // term 1 + dval -= dnorm_mean; // term 2 + dval -= norm_bti * dnorm_norm_mean; // term 3 + dval *= rstd_bt; // final scale + static_cast(grad_input->DataPtr())[b * max_seqlen * embed_dim + t * embed_dim + i] += dval; + } + } + } + return {grad_input, grad_weight, grad_bias}; +} +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_LAYERNORM_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_LAYERNORM_KERNEL(LayerNormForward) +REGISTER_CPU_LAYERNORM_KERNEL(LayerNormBackward) + +#undef REGISTER_CPU_LAYERNORM_KERNEL diff --git a/infini_train/src/kernels/cpu/linear.cc b/infini_train/src/kernels/cpu/linear.cc index 140e756..d72983e 100644 --- a/infini_train/src/kernels/cpu/linear.cc +++ b/infini_train/src/kernels/cpu/linear.cc @@ -1,134 +1,231 @@ -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -std::shared_ptr MatmulForward(const std::shared_ptr &input, const std::shared_ptr &other) { - // =================================== 作业 =================================== - // TODO:实现CPU上的矩阵乘法前向计算 - // REF: - // =================================== 作业 =================================== - - auto output = std::make_shared(); - return {output}; -} - -std::tuple, std::shared_ptr> -MatmulBackward(const std::shared_ptr &input, const std::shared_ptr &other, - const std::shared_ptr &grad_output) { - // =================================== 作业 =================================== - // TODO:实现CPU上的矩阵乘法反向传播 - // REF: - // =================================== 作业 =================================== - - auto grad_input = std::make_shared(); - auto grad_other = std::make_shared(); - return {grad_input, grad_other}; -} - -std::shared_ptr LinearForward(const std::shared_ptr &input, const std::shared_ptr &weight, - bool transpose, const std::shared_ptr &bias) { - /* - transpose: output = input * weight^T + bias - output[*, out_features] = input[*, in_features] * weight[out_features, in_features]^T + bias[out_features] - - !transpose: output = input * weight + bias - output[*, out_features] = input[*, in_features] * weight[in_features, out_features] + bias[out_features] - */ - - const auto &input_dims = input->Dims(); - CHECK_GE(input_dims.size(), 2); - const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); - const int64_t in_features = *input_dims.rbegin(); - - const auto &weight_dims = weight->Dims(); - CHECK_EQ(weight_dims.size(), 2); - CHECK_EQ(in_features, weight_dims[transpose ? 1 : 0]); - const int out_features = weight_dims[transpose ? 0 : 1]; - - if (bias) { - const auto &bias_dims = bias->Dims(); - CHECK_EQ(bias_dims.size(), 1); - CHECK_EQ(bias_dims[0], out_features); - } - - auto output_dims = input_dims; - *output_dims.rbegin() = out_features; - auto output = std::make_shared(output_dims, DataType::kFLOAT32); - - if (transpose) { - output->EigenMatrix() = input->EigenMatrix() * weight->EigenMatrix().transpose(); - } else { - output->EigenMatrix() = input->EigenMatrix() * weight->EigenMatrix(); - } - - if (bias) { - output->EigenMatrix().rowwise() += bias->EigenVector(); - } - - return output; -} - -std::tuple, std::shared_ptr, std::shared_ptr> -LinearBackward(const std::shared_ptr &input, const std::shared_ptr &weight, bool transpose, - int64_t out_features, const std::shared_ptr &grad_output, const bool bias) { - /* - transpose: grad_input = grad_output * weight - grad_input[*, in_features] = grad_output[*, out_features] * weight[out_features, in_features] - grad_weight[out_features, in_features] = grad_output[*, out_features]^T * input[*, in_features] - grad_bias[out_features] = grad_output[*, out_features].sum(axis=0) - - !transpose: grad_input = grad_output * weight^T - grad_input[*, in_features] = grad_output[_, out_features] * weight[in_features, out_features]^T - grad_weight[in_features, out_features] = input[*, in_features]^T * grad_output[*, out_features] - grad_bias[out_features] = grad_output[*, out_features].sum(axis=0) - */ - - const auto &input_dims = input->Dims(); - CHECK_GE(input_dims.size(), 2); - const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); - const int64_t in_features = *input_dims.rbegin(); - - const auto &weight_dims = weight->Dims(); - CHECK_EQ(weight_dims.size(), 2); - CHECK_EQ(in_features, weight_dims[transpose ? 1 : 0]); - CHECK_EQ(out_features, weight_dims[transpose ? 0 : 1]); - - auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32); - auto grad_weight = std::make_shared(weight_dims, DataType::kFLOAT32); - std::shared_ptr grad_bias = nullptr; - if (bias) { - grad_bias = std::make_shared(std::vector{out_features}, DataType::kFLOAT32); - } - - if (transpose) { - grad_input->EigenMatrix() = grad_output->EigenMatrix() * weight->EigenMatrix(); - grad_weight->EigenMatrix() = grad_output->EigenMatrix().transpose() * input->EigenMatrix(); - } else { - grad_input->EigenMatrix() = grad_output->EigenMatrix() * weight->EigenMatrix().transpose(); - grad_weight->EigenMatrix() = input->EigenMatrix().transpose() * grad_output->EigenMatrix(); - } - if (bias) { - grad_bias->EigenVector() = grad_output->EigenMatrix().colwise().sum(); - } - - return {grad_input, grad_weight, grad_bias}; -} -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_LINEAR_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_LINEAR_KERNEL(MatmulForward) -REGISTER_CPU_LINEAR_KERNEL(MatmulBackward) -REGISTER_CPU_LINEAR_KERNEL(LinearForward) -REGISTER_CPU_LINEAR_KERNEL(LinearBackward) - -#undef REGISTER_CPU_LINEAR_KERNEL +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +std::shared_ptr MatmulForward(const std::shared_ptr &input, const std::shared_ptr &other) { + // =================================== 作业 =================================== + // TODO:实现CPU上的矩阵乘法前向计算 + // REF: + // =================================== 作业 =================================== + + const auto &input_dims = input->Dims(); + const auto &other_dims = other->Dims(); + CHECK_GE(input_dims.size(), 2); + CHECK_GE(other_dims.size(), 2); + const int64_t M = input_dims[input_dims.size() - 2]; + const int64_t K = input_dims[input_dims.size() - 1]; + const int64_t N = other_dims[other_dims.size() - 1]; + CHECK_EQ(K, other_dims[other_dims.size() - 2]); + + // Determine batch dimensions + int64_t batch_input = 1; + for (size_t i = 0; i < input_dims.size() - 2; ++i) { + batch_input *= input_dims[i]; + } + int64_t batch_other = 1; + for (size_t i = 0; i < other_dims.size() - 2; ++i) { + batch_other *= other_dims[i]; + } + int64_t batch = std::max(batch_input, batch_other); + + // Build output dims + std::vector output_dims; + if (batch > 1) { + output_dims = (input_dims.size() >= other_dims.size()) ? input_dims : other_dims; + output_dims[output_dims.size() - 2] = M; + output_dims[output_dims.size() - 1] = N; + } else { + output_dims = {M, N}; + } + + auto output = std::make_shared(output_dims, DataType::kFLOAT32); + + float *input_ptr = static_cast(input->DataPtr()); + float *other_ptr = static_cast(other->DataPtr()); + float *output_ptr = static_cast(output->DataPtr()); + + for (int64_t b = 0; b < batch; ++b) { + int64_t input_offset = (batch_input == 1) ? 0 : b * M * K; + int64_t other_offset = (batch_other == 1) ? 0 : b * K * N; + int64_t output_offset = b * M * N; + + Eigen::Map> input_mat( + input_ptr + input_offset, M, K); + Eigen::Map> other_mat( + other_ptr + other_offset, K, N); + Eigen::Map> output_mat( + output_ptr + output_offset, M, N); + + output_mat = input_mat * other_mat; + } + + return output; +} + +std::tuple, std::shared_ptr> +MatmulBackward(const std::shared_ptr &input, const std::shared_ptr &other, + const std::shared_ptr &grad_output) { + // =================================== 作业 =================================== + // TODO:实现CPU上的矩阵乘法反向传播 + // REF: + // =================================== 作业 =================================== + + const auto &input_dims = input->Dims(); + const auto &other_dims = other->Dims(); + const int64_t M = input_dims[input_dims.size() - 2]; + const int64_t K = input_dims[input_dims.size() - 1]; + const int64_t N = other_dims[other_dims.size() - 1]; + + int64_t batch_input = 1; + for (size_t i = 0; i < input_dims.size() - 2; ++i) { + batch_input *= input_dims[i]; + } + int64_t batch_other = 1; + for (size_t i = 0; i < other_dims.size() - 2; ++i) { + batch_other *= other_dims[i]; + } + int64_t batch = std::max(batch_input, batch_other); + + auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32); + auto grad_other = std::make_shared(other_dims, DataType::kFLOAT32); + + float *input_ptr = static_cast(input->DataPtr()); + float *other_ptr = static_cast(other->DataPtr()); + float *grad_output_ptr = static_cast(grad_output->DataPtr()); + float *grad_input_ptr = static_cast(grad_input->DataPtr()); + float *grad_other_ptr = static_cast(grad_other->DataPtr()); + + for (int64_t b = 0; b < batch; ++b) { + int64_t input_offset = (batch_input == 1) ? 0 : b * M * K; + int64_t other_offset = (batch_other == 1) ? 0 : b * K * N; + int64_t grad_output_offset = b * M * N; + int64_t grad_input_offset = (batch_input == 1) ? 0 : b * M * K; + int64_t grad_other_offset = (batch_other == 1) ? 0 : b * K * N; + + Eigen::Map> input_mat( + input_ptr + input_offset, M, K); + Eigen::Map> other_mat( + other_ptr + other_offset, K, N); + Eigen::Map> grad_output_mat( + grad_output_ptr + grad_output_offset, M, N); + Eigen::Map> grad_input_mat( + grad_input_ptr + grad_input_offset, M, K); + Eigen::Map> grad_other_mat( + grad_other_ptr + grad_other_offset, K, N); + + // dX = dY * W^T + grad_input_mat = grad_output_mat * other_mat.transpose(); + // dW = X^T * dY + grad_other_mat = input_mat.transpose() * grad_output_mat; + } + + return {grad_input, grad_other}; +} + +std::shared_ptr LinearForward(const std::shared_ptr &input, const std::shared_ptr &weight, + bool transpose, const std::shared_ptr &bias) { + /* + transpose: output = input * weight^T + bias + output[*, out_features] = input[*, in_features] * weight[out_features, in_features]^T + bias[out_features] + + !transpose: output = input * weight + bias + output[*, out_features] = input[*, in_features] * weight[in_features, out_features] + bias[out_features] + */ + + const auto &input_dims = input->Dims(); + CHECK_GE(input_dims.size(), 2); + const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); + const int64_t in_features = *input_dims.rbegin(); + + const auto &weight_dims = weight->Dims(); + CHECK_EQ(weight_dims.size(), 2); + CHECK_EQ(in_features, weight_dims[transpose ? 1 : 0]); + const int out_features = weight_dims[transpose ? 0 : 1]; + + if (bias) { + const auto &bias_dims = bias->Dims(); + CHECK_EQ(bias_dims.size(), 1); + CHECK_EQ(bias_dims[0], out_features); + } + + auto output_dims = input_dims; + *output_dims.rbegin() = out_features; + auto output = std::make_shared(output_dims, DataType::kFLOAT32); + + if (transpose) { + output->EigenMatrix() = input->EigenMatrix() * weight->EigenMatrix().transpose(); + } else { + output->EigenMatrix() = input->EigenMatrix() * weight->EigenMatrix(); + } + + if (bias) { + output->EigenMatrix().rowwise() += bias->EigenVector(); + } + + return output; +} + +std::tuple, std::shared_ptr, std::shared_ptr> +LinearBackward(const std::shared_ptr &input, const std::shared_ptr &weight, bool transpose, + int64_t out_features, const std::shared_ptr &grad_output, const bool bias) { + /* + transpose: grad_input = grad_output * weight + grad_input[*, in_features] = grad_output[*, out_features] * weight[out_features, in_features] + grad_weight[out_features, in_features] = grad_output[*, out_features]^T * input[*, in_features] + grad_bias[out_features] = grad_output[*, out_features].sum(axis=0) + + !transpose: grad_input = grad_output * weight^T + grad_input[*, in_features] = grad_output[_, out_features] * weight[in_features, out_features]^T + grad_weight[in_features, out_features] = input[*, in_features]^T * grad_output[*, out_features] + grad_bias[out_features] = grad_output[*, out_features].sum(axis=0) + */ + + const auto &input_dims = input->Dims(); + CHECK_GE(input_dims.size(), 2); + const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); + const int64_t in_features = *input_dims.rbegin(); + + const auto &weight_dims = weight->Dims(); + CHECK_EQ(weight_dims.size(), 2); + CHECK_EQ(in_features, weight_dims[transpose ? 1 : 0]); + CHECK_EQ(out_features, weight_dims[transpose ? 0 : 1]); + + auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32); + auto grad_weight = std::make_shared(weight_dims, DataType::kFLOAT32); + std::shared_ptr grad_bias = nullptr; + if (bias) { + grad_bias = std::make_shared(std::vector{out_features}, DataType::kFLOAT32); + } + + if (transpose) { + grad_input->EigenMatrix() = grad_output->EigenMatrix() * weight->EigenMatrix(); + grad_weight->EigenMatrix() = grad_output->EigenMatrix().transpose() * input->EigenMatrix(); + } else { + grad_input->EigenMatrix() = grad_output->EigenMatrix() * weight->EigenMatrix().transpose(); + grad_weight->EigenMatrix() = input->EigenMatrix().transpose() * grad_output->EigenMatrix(); + } + if (bias) { + grad_bias->EigenVector() = grad_output->EigenMatrix().colwise().sum(); + } + + return {grad_input, grad_weight, grad_bias}; +} +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_LINEAR_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_LINEAR_KERNEL(MatmulForward) +REGISTER_CPU_LINEAR_KERNEL(MatmulBackward) +REGISTER_CPU_LINEAR_KERNEL(LinearForward) +REGISTER_CPU_LINEAR_KERNEL(LinearBackward) + +#undef REGISTER_CPU_LINEAR_KERNEL diff --git a/infini_train/src/kernels/cpu/no_op.cc b/infini_train/src/kernels/cpu/no_op.cc index 343d7a2..a99d12b 100644 --- a/infini_train/src/kernels/cpu/no_op.cc +++ b/infini_train/src/kernels/cpu/no_op.cc @@ -1,33 +1,33 @@ -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -std::shared_ptr NoOpForward(const std::shared_ptr &input, const std::vector &dims) { - const int64_t num_elements = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); - CHECK_EQ(input->NumElements(), num_elements); - - auto output = std::make_shared(*input, 0, dims); - return output; -} - -std::shared_ptr NoOpBackward(const std::vector &dims, const std::shared_ptr &grad_output) { - auto num_elements = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); - CHECK_EQ(num_elements, grad_output->NumElements()); - - auto grad_input = std::make_shared(*grad_output, 0, dims); - return grad_input; -} -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_NO_OP_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_NO_OP_KERNEL(NoOpForward) -REGISTER_CPU_NO_OP_KERNEL(NoOpBackward) - -#undef REGISTER_CPU_NO_OP_KERNEL +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +std::shared_ptr NoOpForward(const std::shared_ptr &input, const std::vector &dims) { + const int64_t num_elements = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); + CHECK_EQ(input->NumElements(), num_elements); + + auto output = std::make_shared(*input, 0, dims); + return output; +} + +std::shared_ptr NoOpBackward(const std::vector &dims, const std::shared_ptr &grad_output) { + auto num_elements = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); + CHECK_EQ(num_elements, grad_output->NumElements()); + + auto grad_input = std::make_shared(*grad_output, 0, dims); + return grad_input; +} +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_NO_OP_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_NO_OP_KERNEL(NoOpForward) +REGISTER_CPU_NO_OP_KERNEL(NoOpBackward) + +#undef REGISTER_CPU_NO_OP_KERNEL diff --git a/infini_train/src/kernels/cpu/outer.cc b/infini_train/src/kernels/cpu/outer.cc index 1a8d392..2ac4c95 100644 --- a/infini_train/src/kernels/cpu/outer.cc +++ b/infini_train/src/kernels/cpu/outer.cc @@ -1,66 +1,66 @@ -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -std::shared_ptr OuterForward(const std::shared_ptr &input, const std::shared_ptr &other) { - /* - output[i, j] = input[i] * other[j] - output shape: [input.size(0), other.size(0)] - */ - - const auto &input_dims = input->Dims(); - const auto &other_dims = other->Dims(); - CHECK_EQ(input_dims.size(), 1); - CHECK_EQ(other_dims.size(), 1); - - std::vector out_shape = {input_dims[0], other_dims[0]}; - auto output = std::make_shared(out_shape, DataType::kFLOAT32); - - // [M, N] = [M, 1] * [1, N] - output->EigenMatrix() = input->EigenVector().transpose() * other->EigenVector(); - - return output; -} - -std::tuple, std::shared_ptr> OuterBackward(const std::shared_ptr &input, - const std::shared_ptr &other, - const std::shared_ptr &grad_output) { - /* - grad_input[i] = sum_j(grad_output[i, j] * other[j]) - grad_other[j] = sum_i(grad_output[i, j] * input[i]) - */ - - const int64_t m = input->Dims()[0]; - const int64_t n = other->Dims()[0]; - CHECK_EQ(grad_output->Dims().size(), 2); - CHECK_EQ(grad_output->Dims()[0], m); - CHECK_EQ(grad_output->Dims()[1], n); - - auto grad_input = std::make_shared(std::vector{m}, DataType::kFLOAT32); - auto grad_other = std::make_shared(std::vector{n}, DataType::kFLOAT32); - grad_input->Fill(0.0f); - grad_other->Fill(0.0f); - - grad_input->EigenVector() = grad_output->EigenMatrix() * other->EigenVector().transpose(); - grad_other->EigenVector() = grad_output->EigenMatrix().transpose() * input->EigenVector().transpose(); - - return {grad_input, grad_other}; -} - -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_OUTER_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_OUTER_KERNEL(OuterForward) -REGISTER_CPU_OUTER_KERNEL(OuterBackward) - -#undef REGISTER_CPU_OUTER_KERNEL +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +std::shared_ptr OuterForward(const std::shared_ptr &input, const std::shared_ptr &other) { + /* + output[i, j] = input[i] * other[j] + output shape: [input.size(0), other.size(0)] + */ + + const auto &input_dims = input->Dims(); + const auto &other_dims = other->Dims(); + CHECK_EQ(input_dims.size(), 1); + CHECK_EQ(other_dims.size(), 1); + + std::vector out_shape = {input_dims[0], other_dims[0]}; + auto output = std::make_shared(out_shape, DataType::kFLOAT32); + + // [M, N] = [M, 1] * [1, N] + output->EigenMatrix() = input->EigenVector().transpose() * other->EigenVector(); + + return output; +} + +std::tuple, std::shared_ptr> OuterBackward(const std::shared_ptr &input, + const std::shared_ptr &other, + const std::shared_ptr &grad_output) { + /* + grad_input[i] = sum_j(grad_output[i, j] * other[j]) + grad_other[j] = sum_i(grad_output[i, j] * input[i]) + */ + + const int64_t m = input->Dims()[0]; + const int64_t n = other->Dims()[0]; + CHECK_EQ(grad_output->Dims().size(), 2); + CHECK_EQ(grad_output->Dims()[0], m); + CHECK_EQ(grad_output->Dims()[1], n); + + auto grad_input = std::make_shared(std::vector{m}, DataType::kFLOAT32); + auto grad_other = std::make_shared(std::vector{n}, DataType::kFLOAT32); + grad_input->Fill(0.0f); + grad_other->Fill(0.0f); + + grad_input->EigenVector() = grad_output->EigenMatrix() * other->EigenVector().transpose(); + grad_other->EigenVector() = grad_output->EigenMatrix().transpose() * input->EigenVector().transpose(); + + return {grad_input, grad_other}; +} + +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_OUTER_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_OUTER_KERNEL(OuterForward) +REGISTER_CPU_OUTER_KERNEL(OuterBackward) + +#undef REGISTER_CPU_OUTER_KERNEL diff --git a/infini_train/src/kernels/cpu/reduction.cc b/infini_train/src/kernels/cpu/reduction.cc index 87ed538..00be1ec 100644 --- a/infini_train/src/kernels/cpu/reduction.cc +++ b/infini_train/src/kernels/cpu/reduction.cc @@ -1,183 +1,183 @@ -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -std::shared_ptr ReduceOpForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim, - const std::function &reduce_fn) { - const auto &input_dims = input->Dims(); - int64_t actual_dim = dim < 0 ? dim + input_dims.size() : dim; - CHECK_GE(actual_dim, 0); - CHECK_LT(actual_dim, input_dims.size()); - - std::vector output_dims = input_dims; - const int64_t reduce_size = input_dims[dim]; - if (keep_dim) { - output_dims[actual_dim] = 1; - } else { - output_dims.erase(output_dims.begin() + actual_dim); - } - - auto output = std::make_shared(output_dims, DataType::kFLOAT32); - - int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + actual_dim, 1, std::multiplies()); - int64_t H = input_dims[actual_dim]; - int64_t W = std::accumulate(input_dims.begin() + actual_dim + 1, input_dims.end(), 1, std::multiplies()); - - const float *input_ptr = static_cast(input->DataPtr()); - float *output_ptr = static_cast(output->DataPtr()); - - for (int64_t n = 0; n < N; ++n) { - for (int64_t w = 0; w < W; ++w) { - const float *segment = &input_ptr[(n * H) * W + w]; - output_ptr[n * W + w] = reduce_fn(segment, H); - } - } - - return output; -} - -std::shared_ptr ReduceOpBackwardMask(const std::shared_ptr &input, - const std::shared_ptr &grad_output, - const std::shared_ptr &reduced, - const std::vector &input_dims, const int64_t dim, - const bool keep_dim, const std::function &mask_fn) { - std::vector grad_output_dims = grad_output->Dims(); - int64_t actual_dim = dim < 0 ? dim + input_dims.size() : dim; - if (!keep_dim) { - grad_output_dims.insert(grad_output_dims.begin() + actual_dim, 1); - } - - auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32); - - int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + actual_dim, 1, std::multiplies()); - int64_t H = input_dims[actual_dim]; - int64_t W = std::accumulate(input_dims.begin() + actual_dim + 1, input_dims.end(), 1, std::multiplies()); - - const float *input_ptr = static_cast(input->DataPtr()); - const float *grad_output_ptr = static_cast(grad_output->DataPtr()); - const float *reduced_ptr = static_cast(reduced->DataPtr()); - float *grad_input_ptr = static_cast(grad_input->DataPtr()); - - for (int64_t n = 0; n < N; ++n) { - for (int64_t w = 0; w < W; ++w) { - int64_t reduced_idx = n * W + w; - float target = reduced_ptr[reduced_idx]; - - for (int64_t h = 0; h < H; ++h) { - int64_t input_idx = (n * H + h) * W + w; - float value = input_ptr[input_idx]; - grad_input_ptr[input_idx] = (value == target) ? grad_output_ptr[reduced_idx] : 0.0f; - } - } - } - - return grad_input; -} - -std::shared_ptr ReduceOpBackward(const std::shared_ptr &grad_output, - const std::vector &input_dims, const int64_t dim, const bool keep_dim, - const std::function &scale_fn) { - int64_t actual_dim = dim < 0 ? dim + input_dims.size() : dim; - CHECK_GE(actual_dim, 0); - CHECK_LT(actual_dim, input_dims.size()); - - auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32); - - int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + actual_dim, 1, std::multiplies()); - int64_t H = input_dims[actual_dim]; - int64_t W = std::accumulate(input_dims.begin() + actual_dim + 1, input_dims.end(), 1, std::multiplies()); - - const float *grad_output_ptr = static_cast(grad_output->DataPtr()); - float *grad_input_ptr = static_cast(grad_input->DataPtr()); - - for (int64_t n = 0; n < N; ++n) { - for (int64_t h = 0; h < H; ++h) { - for (int64_t w = 0; w < W; ++w) { - int64_t output_idx = n * W + w; - int64_t input_idx = (n * H + h) * W + w; - grad_input_ptr[input_idx] = scale_fn(grad_output_ptr[output_idx], H); - } - } - } - - return grad_input; -} - -std::shared_ptr MeanForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { - return ReduceOpForward(input, dim, keep_dim, [](const float *data, int64_t len) { - float sum = 0.0f; - for (int64_t i = 0; i < len; ++i) { sum += data[i]; } - return sum / static_cast(len); - }); -} - -std::shared_ptr MeanBackward(const std::shared_ptr &grad_output, const std::vector &input_dims, - const int64_t dim, const bool keep_dim) { - return ReduceOpBackward(grad_output, input_dims, dim, keep_dim, - [](float grad, int64_t len) { return grad / static_cast(len); }); -} - -std::shared_ptr SumForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { - return ReduceOpForward(input, dim, keep_dim, [](const float *data, int64_t len) { - float sum = 0.0f; - for (int64_t i = 0; i < len; ++i) { sum += data[i]; } - return sum; - }); -} - -std::shared_ptr SumBackward(const std::shared_ptr &grad_output, const std::vector &input_dims, - const int64_t dim, const bool keep_dim) { - return ReduceOpBackward(grad_output, input_dims, dim, keep_dim, [](float grad, int64_t) { return grad; }); -} - -std::shared_ptr MaxForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { - return ReduceOpForward(input, dim, keep_dim, [](const float *data, int64_t len) { - float max_val = -std::numeric_limits::infinity(); - for (int64_t i = 0; i < len; ++i) { max_val = std::max(max_val, data[i]); } - return max_val; - }); -} - -std::shared_ptr MaxBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, - const std::shared_ptr &reduced, const int64_t dim, bool keep_dim) { - return ReduceOpBackwardMask(input, grad_output, reduced, input->Dims(), dim, keep_dim, - [](float val, float current) { return val > current; }); -} - -std::shared_ptr MinForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { - return ReduceOpForward(input, dim, keep_dim, [](const float *data, int64_t len) { - float min_val = std::numeric_limits::infinity(); - for (int64_t i = 0; i < len; ++i) { min_val = std::min(min_val, data[i]); } - return min_val; - }); -} - -std::shared_ptr MinBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, - const std::shared_ptr &reduced, const int64_t dim, bool keep_dim) { - return ReduceOpBackwardMask(input, grad_output, reduced, input->Dims(), dim, keep_dim, - [](float val, float current) { return val < current; }); -} - -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_REDUCTION_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_REDUCTION_KERNEL(MeanForward) -REGISTER_CPU_REDUCTION_KERNEL(MeanBackward) -REGISTER_CPU_REDUCTION_KERNEL(SumForward) -REGISTER_CPU_REDUCTION_KERNEL(SumBackward) -REGISTER_CPU_REDUCTION_KERNEL(MaxForward) -REGISTER_CPU_REDUCTION_KERNEL(MaxBackward) -REGISTER_CPU_REDUCTION_KERNEL(MinForward) -REGISTER_CPU_REDUCTION_KERNEL(MinBackward) - -#undef REGISTER_CPU_REDUCTION_KERNEL +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +std::shared_ptr ReduceOpForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim, + const std::function &reduce_fn) { + const auto &input_dims = input->Dims(); + int64_t actual_dim = dim < 0 ? dim + input_dims.size() : dim; + CHECK_GE(actual_dim, 0); + CHECK_LT(actual_dim, input_dims.size()); + + std::vector output_dims = input_dims; + const int64_t reduce_size = input_dims[dim]; + if (keep_dim) { + output_dims[actual_dim] = 1; + } else { + output_dims.erase(output_dims.begin() + actual_dim); + } + + auto output = std::make_shared(output_dims, DataType::kFLOAT32); + + int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + actual_dim, 1, std::multiplies()); + int64_t H = input_dims[actual_dim]; + int64_t W = std::accumulate(input_dims.begin() + actual_dim + 1, input_dims.end(), 1, std::multiplies()); + + const float *input_ptr = static_cast(input->DataPtr()); + float *output_ptr = static_cast(output->DataPtr()); + + for (int64_t n = 0; n < N; ++n) { + for (int64_t w = 0; w < W; ++w) { + const float *segment = &input_ptr[(n * H) * W + w]; + output_ptr[n * W + w] = reduce_fn(segment, H); + } + } + + return output; +} + +std::shared_ptr ReduceOpBackwardMask(const std::shared_ptr &input, + const std::shared_ptr &grad_output, + const std::shared_ptr &reduced, + const std::vector &input_dims, const int64_t dim, + const bool keep_dim, const std::function &mask_fn) { + std::vector grad_output_dims = grad_output->Dims(); + int64_t actual_dim = dim < 0 ? dim + input_dims.size() : dim; + if (!keep_dim) { + grad_output_dims.insert(grad_output_dims.begin() + actual_dim, 1); + } + + auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32); + + int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + actual_dim, 1, std::multiplies()); + int64_t H = input_dims[actual_dim]; + int64_t W = std::accumulate(input_dims.begin() + actual_dim + 1, input_dims.end(), 1, std::multiplies()); + + const float *input_ptr = static_cast(input->DataPtr()); + const float *grad_output_ptr = static_cast(grad_output->DataPtr()); + const float *reduced_ptr = static_cast(reduced->DataPtr()); + float *grad_input_ptr = static_cast(grad_input->DataPtr()); + + for (int64_t n = 0; n < N; ++n) { + for (int64_t w = 0; w < W; ++w) { + int64_t reduced_idx = n * W + w; + float target = reduced_ptr[reduced_idx]; + + for (int64_t h = 0; h < H; ++h) { + int64_t input_idx = (n * H + h) * W + w; + float value = input_ptr[input_idx]; + grad_input_ptr[input_idx] = (value == target) ? grad_output_ptr[reduced_idx] : 0.0f; + } + } + } + + return grad_input; +} + +std::shared_ptr ReduceOpBackward(const std::shared_ptr &grad_output, + const std::vector &input_dims, const int64_t dim, const bool keep_dim, + const std::function &scale_fn) { + int64_t actual_dim = dim < 0 ? dim + input_dims.size() : dim; + CHECK_GE(actual_dim, 0); + CHECK_LT(actual_dim, input_dims.size()); + + auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32); + + int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + actual_dim, 1, std::multiplies()); + int64_t H = input_dims[actual_dim]; + int64_t W = std::accumulate(input_dims.begin() + actual_dim + 1, input_dims.end(), 1, std::multiplies()); + + const float *grad_output_ptr = static_cast(grad_output->DataPtr()); + float *grad_input_ptr = static_cast(grad_input->DataPtr()); + + for (int64_t n = 0; n < N; ++n) { + for (int64_t h = 0; h < H; ++h) { + for (int64_t w = 0; w < W; ++w) { + int64_t output_idx = n * W + w; + int64_t input_idx = (n * H + h) * W + w; + grad_input_ptr[input_idx] = scale_fn(grad_output_ptr[output_idx], H); + } + } + } + + return grad_input; +} + +std::shared_ptr MeanForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { + return ReduceOpForward(input, dim, keep_dim, [](const float *data, int64_t len) { + float sum = 0.0f; + for (int64_t i = 0; i < len; ++i) { sum += data[i]; } + return sum / static_cast(len); + }); +} + +std::shared_ptr MeanBackward(const std::shared_ptr &grad_output, const std::vector &input_dims, + const int64_t dim, const bool keep_dim) { + return ReduceOpBackward(grad_output, input_dims, dim, keep_dim, + [](float grad, int64_t len) { return grad / static_cast(len); }); +} + +std::shared_ptr SumForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { + return ReduceOpForward(input, dim, keep_dim, [](const float *data, int64_t len) { + float sum = 0.0f; + for (int64_t i = 0; i < len; ++i) { sum += data[i]; } + return sum; + }); +} + +std::shared_ptr SumBackward(const std::shared_ptr &grad_output, const std::vector &input_dims, + const int64_t dim, const bool keep_dim) { + return ReduceOpBackward(grad_output, input_dims, dim, keep_dim, [](float grad, int64_t) { return grad; }); +} + +std::shared_ptr MaxForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { + return ReduceOpForward(input, dim, keep_dim, [](const float *data, int64_t len) { + float max_val = -std::numeric_limits::infinity(); + for (int64_t i = 0; i < len; ++i) { max_val = std::max(max_val, data[i]); } + return max_val; + }); +} + +std::shared_ptr MaxBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, + const std::shared_ptr &reduced, const int64_t dim, bool keep_dim) { + return ReduceOpBackwardMask(input, grad_output, reduced, input->Dims(), dim, keep_dim, + [](float val, float current) { return val > current; }); +} + +std::shared_ptr MinForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { + return ReduceOpForward(input, dim, keep_dim, [](const float *data, int64_t len) { + float min_val = std::numeric_limits::infinity(); + for (int64_t i = 0; i < len; ++i) { min_val = std::min(min_val, data[i]); } + return min_val; + }); +} + +std::shared_ptr MinBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, + const std::shared_ptr &reduced, const int64_t dim, bool keep_dim) { + return ReduceOpBackwardMask(input, grad_output, reduced, input->Dims(), dim, keep_dim, + [](float val, float current) { return val < current; }); +} + +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_REDUCTION_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_REDUCTION_KERNEL(MeanForward) +REGISTER_CPU_REDUCTION_KERNEL(MeanBackward) +REGISTER_CPU_REDUCTION_KERNEL(SumForward) +REGISTER_CPU_REDUCTION_KERNEL(SumBackward) +REGISTER_CPU_REDUCTION_KERNEL(MaxForward) +REGISTER_CPU_REDUCTION_KERNEL(MaxBackward) +REGISTER_CPU_REDUCTION_KERNEL(MinForward) +REGISTER_CPU_REDUCTION_KERNEL(MinBackward) + +#undef REGISTER_CPU_REDUCTION_KERNEL diff --git a/infini_train/src/kernels/cpu/sigmoid.cc b/infini_train/src/kernels/cpu/sigmoid.cc index d4bc05d..7ef6e4b 100644 --- a/infini_train/src/kernels/cpu/sigmoid.cc +++ b/infini_train/src/kernels/cpu/sigmoid.cc @@ -1,43 +1,43 @@ -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -std::shared_ptr SigmoidForward(const std::shared_ptr &input) { - auto output = std::make_shared(input->Dims(), DataType::kFLOAT32); - const float *input_ptr = static_cast(input->DataPtr()); - float *output_ptr = static_cast(output->DataPtr()); - - const int64_t numel = input->NumElements(); - for (int64_t idx = 0; idx < numel; ++idx) { output_ptr[idx] = 1.0f / (1.0f + exp(-input_ptr[idx])); } - - return output; -} - -std::shared_ptr SigmoidBackward(const std::shared_ptr &output, - const std::shared_ptr &grad_output) { - auto grad_input = std::make_shared(output->Dims(), DataType::kFLOAT32); - const float *output_ptr = static_cast(output->DataPtr()); - const float *grad_output_ptr = static_cast(grad_output->DataPtr()); - float *grad_input_ptr = static_cast(grad_input->DataPtr()); - - const int64_t numel = output->NumElements(); - for (int64_t idx = 0; idx < numel; ++idx) { - const float y = output_ptr[idx]; - grad_input_ptr[idx] = grad_output_ptr[idx] * y * (1.0f - y); - } - return grad_input; -} -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_SIGMOID_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_SIGMOID_KERNEL(SigmoidForward) -REGISTER_CPU_SIGMOID_KERNEL(SigmoidBackward) - -#undef REGISTER_CPU_SIGMOID_KERNEL +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +std::shared_ptr SigmoidForward(const std::shared_ptr &input) { + auto output = std::make_shared(input->Dims(), DataType::kFLOAT32); + const float *input_ptr = static_cast(input->DataPtr()); + float *output_ptr = static_cast(output->DataPtr()); + + const int64_t numel = input->NumElements(); + for (int64_t idx = 0; idx < numel; ++idx) { output_ptr[idx] = 1.0f / (1.0f + exp(-input_ptr[idx])); } + + return output; +} + +std::shared_ptr SigmoidBackward(const std::shared_ptr &output, + const std::shared_ptr &grad_output) { + auto grad_input = std::make_shared(output->Dims(), DataType::kFLOAT32); + const float *output_ptr = static_cast(output->DataPtr()); + const float *grad_output_ptr = static_cast(grad_output->DataPtr()); + float *grad_input_ptr = static_cast(grad_input->DataPtr()); + + const int64_t numel = output->NumElements(); + for (int64_t idx = 0; idx < numel; ++idx) { + const float y = output_ptr[idx]; + grad_input_ptr[idx] = grad_output_ptr[idx] * y * (1.0f - y); + } + return grad_input; +} +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_SIGMOID_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_SIGMOID_KERNEL(SigmoidForward) +REGISTER_CPU_SIGMOID_KERNEL(SigmoidBackward) + +#undef REGISTER_CPU_SIGMOID_KERNEL diff --git a/infini_train/src/kernels/cpu/slice.cc b/infini_train/src/kernels/cpu/slice.cc index d0f4946..3d2d0de 100644 --- a/infini_train/src/kernels/cpu/slice.cc +++ b/infini_train/src/kernels/cpu/slice.cc @@ -1,138 +1,138 @@ -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -std::shared_ptr SliceForward(const std::shared_ptr &input, const std::vector &starts, - const std::vector &ends, const std::vector &steps) { - CHECK_EQ(starts.size(), ends.size()); - CHECK_EQ(starts.size(), steps.size()); - auto &dims = input->Dims(); - CHECK_EQ(starts.size(), dims.size()); - - std::vector new_dims; - for (int i = 0; i < starts.size(); i++) { - CHECK_LE(starts[i], ends[i]); - CHECK_LE(0, steps[i]); - new_dims.push_back((ends[i] - starts[i] + steps[i] - 1) / steps[i]); - } - - auto new_tensor = std::make_shared(new_dims, input->Dtype(), input->GetDevice()); - - std::vector src_strides(dims.size()); - int64_t stride = 1; - for (int i = src_strides.size() - 1; i >= 0; --i) { - src_strides[i] = stride; - stride *= dims[i]; - } - - std::vector dst_strides(new_dims.size()); - stride = 1; - for (int i = dst_strides.size() - 1; i >= 0; --i) { - dst_strides[i] = stride; - stride *= new_dims[i]; - } - - std::vector dst_index(new_dims.size(), 0); - std::vector src_index(dims.size(), 0); - - std::function recurse = [&](int d) { - if (d == dims.size()) { - int64_t src_offset = 0; - int64_t dst_offset = 0; - for (int i = dims.size() - 1; i >= 0; --i) { - src_offset += src_index[i] * src_strides[i]; - dst_offset += dst_index[i] * dst_strides[i]; - } - static_cast(new_tensor->DataPtr())[dst_offset] - = static_cast(input->DataPtr())[src_offset]; - return; - } - - // fill in the src_index and dst_index - int64_t out_i = 0; - for (int64_t i = starts[d]; i < ends[d]; i += steps[d]) { - src_index[d] = i; - dst_index[d] = out_i++; - recurse(d + 1); - } - }; - - recurse(0); - return new_tensor; -} - -std::shared_ptr SliceBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, - const std::vector &starts, const std::vector &ends, - const std::vector &steps) { - CHECK_EQ(starts.size(), ends.size()); - CHECK_EQ(starts.size(), steps.size()); - auto &dims = input->Dims(); - CHECK_EQ(starts.size(), dims.size()); - - std::vector new_dims; - for (int i = 0; i < starts.size(); i++) { - CHECK_LE(starts[i], ends[i]); - CHECK_LE(0, steps[i]); - new_dims.push_back((ends[i] - starts[i] + steps[i] - 1) / steps[i]); - } - - auto new_tensor = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); - new_tensor->Fill(0.0); - - std::vector src_strides(dims.size()); - int64_t stride = 1; - for (int i = src_strides.size() - 1; i >= 0; --i) { - src_strides[i] = stride; - stride *= dims[i]; - } - - std::vector dst_strides(new_dims.size()); - stride = 1; - for (int i = dst_strides.size() - 1; i >= 0; --i) { - dst_strides[i] = stride; - stride *= new_dims[i]; - } - - std::vector dst_index(new_dims.size(), 0); - std::vector src_index(dims.size(), 0); - - std::function recurse = [&](int d) { - if (d == dims.size()) { - int64_t src_offset = 0; - int64_t dst_offset = 0; - for (int i = dims.size() - 1; i >= 0; --i) { - src_offset += src_index[i] * src_strides[i]; - dst_offset += dst_index[i] * dst_strides[i]; - } - static_cast(new_tensor->DataPtr())[src_offset] - = static_cast(grad_output->DataPtr())[dst_offset]; - return; - } - - // fill in the src_index and dst_index - int64_t out_i = 0; - for (int64_t i = starts[d]; i < ends[d]; i += steps[d]) { - src_index[d] = i; - dst_index[d] = out_i++; - recurse(d + 1); - } - }; - - recurse(0); - return new_tensor; -} -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_SLICE_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_SLICE_KERNEL(SliceForward) -REGISTER_CPU_SLICE_KERNEL(SliceBackward) - -#undef REGISTER_CPU_SLICE_KERNEL +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +std::shared_ptr SliceForward(const std::shared_ptr &input, const std::vector &starts, + const std::vector &ends, const std::vector &steps) { + CHECK_EQ(starts.size(), ends.size()); + CHECK_EQ(starts.size(), steps.size()); + auto &dims = input->Dims(); + CHECK_EQ(starts.size(), dims.size()); + + std::vector new_dims; + for (int i = 0; i < starts.size(); i++) { + CHECK_LE(starts[i], ends[i]); + CHECK_LE(0, steps[i]); + new_dims.push_back((ends[i] - starts[i] + steps[i] - 1) / steps[i]); + } + + auto new_tensor = std::make_shared(new_dims, input->Dtype(), input->GetDevice()); + + std::vector src_strides(dims.size()); + int64_t stride = 1; + for (int i = src_strides.size() - 1; i >= 0; --i) { + src_strides[i] = stride; + stride *= dims[i]; + } + + std::vector dst_strides(new_dims.size()); + stride = 1; + for (int i = dst_strides.size() - 1; i >= 0; --i) { + dst_strides[i] = stride; + stride *= new_dims[i]; + } + + std::vector dst_index(new_dims.size(), 0); + std::vector src_index(dims.size(), 0); + + std::function recurse = [&](int d) { + if (d == dims.size()) { + int64_t src_offset = 0; + int64_t dst_offset = 0; + for (int i = dims.size() - 1; i >= 0; --i) { + src_offset += src_index[i] * src_strides[i]; + dst_offset += dst_index[i] * dst_strides[i]; + } + static_cast(new_tensor->DataPtr())[dst_offset] + = static_cast(input->DataPtr())[src_offset]; + return; + } + + // fill in the src_index and dst_index + int64_t out_i = 0; + for (int64_t i = starts[d]; i < ends[d]; i += steps[d]) { + src_index[d] = i; + dst_index[d] = out_i++; + recurse(d + 1); + } + }; + + recurse(0); + return new_tensor; +} + +std::shared_ptr SliceBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, + const std::vector &starts, const std::vector &ends, + const std::vector &steps) { + CHECK_EQ(starts.size(), ends.size()); + CHECK_EQ(starts.size(), steps.size()); + auto &dims = input->Dims(); + CHECK_EQ(starts.size(), dims.size()); + + std::vector new_dims; + for (int i = 0; i < starts.size(); i++) { + CHECK_LE(starts[i], ends[i]); + CHECK_LE(0, steps[i]); + new_dims.push_back((ends[i] - starts[i] + steps[i] - 1) / steps[i]); + } + + auto new_tensor = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); + new_tensor->Fill(0.0); + + std::vector src_strides(dims.size()); + int64_t stride = 1; + for (int i = src_strides.size() - 1; i >= 0; --i) { + src_strides[i] = stride; + stride *= dims[i]; + } + + std::vector dst_strides(new_dims.size()); + stride = 1; + for (int i = dst_strides.size() - 1; i >= 0; --i) { + dst_strides[i] = stride; + stride *= new_dims[i]; + } + + std::vector dst_index(new_dims.size(), 0); + std::vector src_index(dims.size(), 0); + + std::function recurse = [&](int d) { + if (d == dims.size()) { + int64_t src_offset = 0; + int64_t dst_offset = 0; + for (int i = dims.size() - 1; i >= 0; --i) { + src_offset += src_index[i] * src_strides[i]; + dst_offset += dst_index[i] * dst_strides[i]; + } + static_cast(new_tensor->DataPtr())[src_offset] + = static_cast(grad_output->DataPtr())[dst_offset]; + return; + } + + // fill in the src_index and dst_index + int64_t out_i = 0; + for (int64_t i = starts[d]; i < ends[d]; i += steps[d]) { + src_index[d] = i; + dst_index[d] = out_i++; + recurse(d + 1); + } + }; + + recurse(0); + return new_tensor; +} +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_SLICE_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_SLICE_KERNEL(SliceForward) +REGISTER_CPU_SLICE_KERNEL(SliceBackward) + +#undef REGISTER_CPU_SLICE_KERNEL diff --git a/infini_train/src/kernels/cpu/softmax.cc b/infini_train/src/kernels/cpu/softmax.cc index 454bdc2..1ea24c7 100644 --- a/infini_train/src/kernels/cpu/softmax.cc +++ b/infini_train/src/kernels/cpu/softmax.cc @@ -1,89 +1,89 @@ -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -std::shared_ptr SoftmaxForward(const std::shared_ptr &input, int64_t dim) { - dim = dim < 0 ? input->Dims().size() + dim : dim; - auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); - - const float *input_data = static_cast(input->DataPtr()); - float *output_data = static_cast(output->DataPtr()); - - const auto &dims = input->Dims(); - int outer = 1; - int axis = dims[dim]; - int inner = 1; - - for (int i = 0; i < dim; ++i) { outer *= dims[i]; } - for (int i = dim + 1; i < dims.size(); ++i) { inner *= dims[i]; } - - for (int o = 0; o < outer; ++o) { - for (int i = 0; i < inner; ++i) { - int offset = o * axis * inner + i; - - // find max value in the axis dimension - float max_val = input_data[offset]; - for (int j = 1; j < axis; ++j) { max_val = std::max(max_val, input_data[offset + j * inner]); } - - // compute sum_exp - float sum_exp = 0.0f; - for (int j = 0; j < axis; ++j) { - output_data[offset + j * inner] = std::exp(input_data[offset + j * inner] - max_val); - sum_exp += output_data[offset + j * inner]; - } - - // normalize - for (int j = 0; j < axis; ++j) { output_data[offset + j * inner] /= sum_exp; } - } - } - return output; -} - -std::shared_ptr SoftmaxBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &output, int64_t dim) { - dim = dim < 0 ? output->Dims().size() + dim : dim; - auto grad_input = std::make_shared(output->Dims(), output->Dtype(), output->GetDevice()); - const float *grad_output_data = static_cast(grad_output->DataPtr()); - const float *output_data = static_cast(output->DataPtr()); - float *grad_input_data = static_cast(grad_input->DataPtr()); - - const auto &dims = output->Dims(); - int outer = 1; - int inner = 1; - int axis = dims[dim]; - - for (int i = 0; i < dim; ++i) { outer *= dims[i]; } - for (int i = dim + 1; i < dims.size(); ++i) { inner *= dims[i]; } - - for (int o = 0; o < outer; ++o) { - for (int i = 0; i < inner; ++i) { - int offset = o * axis * inner + i; - - // dot product: y * dy - float dot = 0.0f; - for (int j = 0; j < axis; ++j) { - dot += output_data[offset + j * inner] * grad_output_data[offset + j * inner]; - } - for (int j = 0; j < axis; ++j) { - grad_input_data[offset + j * inner] - = output_data[offset + j * inner] * (grad_output_data[offset + j * inner] - dot); - } - } - } - return grad_input; -} -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_SOFTMAX_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_SOFTMAX_KERNEL(SoftmaxForward) -REGISTER_CPU_SOFTMAX_KERNEL(SoftmaxBackward) - -#undef REGISTER_CPU_SOFTMAX_KERNEL +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +std::shared_ptr SoftmaxForward(const std::shared_ptr &input, int64_t dim) { + dim = dim < 0 ? input->Dims().size() + dim : dim; + auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); + + const float *input_data = static_cast(input->DataPtr()); + float *output_data = static_cast(output->DataPtr()); + + const auto &dims = input->Dims(); + int outer = 1; + int axis = dims[dim]; + int inner = 1; + + for (int i = 0; i < dim; ++i) { outer *= dims[i]; } + for (int i = dim + 1; i < dims.size(); ++i) { inner *= dims[i]; } + + for (int o = 0; o < outer; ++o) { + for (int i = 0; i < inner; ++i) { + int offset = o * axis * inner + i; + + // find max value in the axis dimension + float max_val = input_data[offset]; + for (int j = 1; j < axis; ++j) { max_val = std::max(max_val, input_data[offset + j * inner]); } + + // compute sum_exp + float sum_exp = 0.0f; + for (int j = 0; j < axis; ++j) { + output_data[offset + j * inner] = std::exp(input_data[offset + j * inner] - max_val); + sum_exp += output_data[offset + j * inner]; + } + + // normalize + for (int j = 0; j < axis; ++j) { output_data[offset + j * inner] /= sum_exp; } + } + } + return output; +} + +std::shared_ptr SoftmaxBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &output, int64_t dim) { + dim = dim < 0 ? output->Dims().size() + dim : dim; + auto grad_input = std::make_shared(output->Dims(), output->Dtype(), output->GetDevice()); + const float *grad_output_data = static_cast(grad_output->DataPtr()); + const float *output_data = static_cast(output->DataPtr()); + float *grad_input_data = static_cast(grad_input->DataPtr()); + + const auto &dims = output->Dims(); + int outer = 1; + int inner = 1; + int axis = dims[dim]; + + for (int i = 0; i < dim; ++i) { outer *= dims[i]; } + for (int i = dim + 1; i < dims.size(); ++i) { inner *= dims[i]; } + + for (int o = 0; o < outer; ++o) { + for (int i = 0; i < inner; ++i) { + int offset = o * axis * inner + i; + + // dot product: y * dy + float dot = 0.0f; + for (int j = 0; j < axis; ++j) { + dot += output_data[offset + j * inner] * grad_output_data[offset + j * inner]; + } + for (int j = 0; j < axis; ++j) { + grad_input_data[offset + j * inner] + = output_data[offset + j * inner] * (grad_output_data[offset + j * inner] - dot); + } + } + } + return grad_input; +} +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_SOFTMAX_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_SOFTMAX_KERNEL(SoftmaxForward) +REGISTER_CPU_SOFTMAX_KERNEL(SoftmaxBackward) + +#undef REGISTER_CPU_SOFTMAX_KERNEL diff --git a/infini_train/src/kernels/cpu/split.cc b/infini_train/src/kernels/cpu/split.cc index ddd335e..1aad785 100644 --- a/infini_train/src/kernels/cpu/split.cc +++ b/infini_train/src/kernels/cpu/split.cc @@ -1,80 +1,80 @@ -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -std::vector> SplitForward(const std::shared_ptr &input, int64_t split_size, int dim) { - CHECK_GT(split_size, 0); - CHECK_GE(dim, 0) << "Currently we do not support negative dimension"; - const auto &input_dims = input->Dims(); - CHECK_LT(dim, input_dims.size()); - - std::vector> outputs; - for (int64_t start = 0; start < input_dims[dim]; start += split_size) { - auto output_dims = input_dims; - output_dims[dim] = std::min(split_size, input_dims[dim] - start); - auto output = std::make_shared(output_dims, DataType::kFLOAT32); - const int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); - const int64_t W - = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); - for (int64_t n = 0; n < N; ++n) { - // out[0:N, :, 0:W] = in[0:N, start:end, 0:W] - const int64_t H_in = input_dims[dim]; - const int64_t H_out = output_dims[dim]; - const int64_t end = std::min(start + split_size, input_dims[dim]); - memcpy(static_cast(output->DataPtr()) + n * H_out * W, - static_cast(input->DataPtr()) + n * H_in * W + start * W, - (end - start) * W * sizeof(float)); - } - outputs.push_back(std::move(output)); - } - return outputs; -} - -std::shared_ptr SplitBackward(const std::vector &input_dims, int64_t split_size, int dim, - const std::vector> &grad_outputs) { - CHECK_GT(split_size, 0); - CHECK_GE(dim, 0) << "Currently we do not support negative dimension"; - CHECK_LT(dim, input_dims.size()); - CHECK_EQ(grad_outputs.size(), (input_dims[dim] + split_size - 1) / split_size); - - auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32); - grad_input->Fill(0.0f); - for (int64_t start = 0, idx = 0; start < input_dims[dim]; start += split_size, ++idx) { - auto output_dims = input_dims; - output_dims[dim] = std::min(split_size, input_dims[dim] - start); - const auto &grad_output = grad_outputs[idx]; - for (int dim_idx = 0; dim_idx < grad_input->Dims().size(); ++dim_idx) { - CHECK_EQ(output_dims[dim_idx], grad_output->Dims()[dim_idx]); - } - const int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); - const int64_t W - = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); - for (int64_t n = 0; n < N; ++n) { - // grad_input[0:N, start:end, 0:W] = grad_output[0:N, :, 0:W] - const int64_t H_in = input_dims[dim]; - const int64_t H_out = output_dims[dim]; - const int64_t end = std::min(start + split_size, input_dims[dim]); - memcpy(static_cast(grad_input->DataPtr()) + n * H_in * W + start * W, - static_cast(grad_output->DataPtr()) + n * H_out * W, - (end - start) * W * sizeof(float)); - } - } - return grad_input; -} -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_SPLIT_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_SPLIT_KERNEL(SplitForward) -REGISTER_CPU_SPLIT_KERNEL(SplitBackward) - -#undef REGISTER_CPU_SPLIT_KERNEL +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +std::vector> SplitForward(const std::shared_ptr &input, int64_t split_size, int dim) { + CHECK_GT(split_size, 0); + CHECK_GE(dim, 0) << "Currently we do not support negative dimension"; + const auto &input_dims = input->Dims(); + CHECK_LT(dim, input_dims.size()); + + std::vector> outputs; + for (int64_t start = 0; start < input_dims[dim]; start += split_size) { + auto output_dims = input_dims; + output_dims[dim] = std::min(split_size, input_dims[dim] - start); + auto output = std::make_shared(output_dims, DataType::kFLOAT32); + const int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); + const int64_t W + = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); + for (int64_t n = 0; n < N; ++n) { + // out[0:N, :, 0:W] = in[0:N, start:end, 0:W] + const int64_t H_in = input_dims[dim]; + const int64_t H_out = output_dims[dim]; + const int64_t end = std::min(start + split_size, input_dims[dim]); + memcpy(static_cast(output->DataPtr()) + n * H_out * W, + static_cast(input->DataPtr()) + n * H_in * W + start * W, + (end - start) * W * sizeof(float)); + } + outputs.push_back(std::move(output)); + } + return outputs; +} + +std::shared_ptr SplitBackward(const std::vector &input_dims, int64_t split_size, int dim, + const std::vector> &grad_outputs) { + CHECK_GT(split_size, 0); + CHECK_GE(dim, 0) << "Currently we do not support negative dimension"; + CHECK_LT(dim, input_dims.size()); + CHECK_EQ(grad_outputs.size(), (input_dims[dim] + split_size - 1) / split_size); + + auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32); + grad_input->Fill(0.0f); + for (int64_t start = 0, idx = 0; start < input_dims[dim]; start += split_size, ++idx) { + auto output_dims = input_dims; + output_dims[dim] = std::min(split_size, input_dims[dim] - start); + const auto &grad_output = grad_outputs[idx]; + for (int dim_idx = 0; dim_idx < grad_input->Dims().size(); ++dim_idx) { + CHECK_EQ(output_dims[dim_idx], grad_output->Dims()[dim_idx]); + } + const int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); + const int64_t W + = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); + for (int64_t n = 0; n < N; ++n) { + // grad_input[0:N, start:end, 0:W] = grad_output[0:N, :, 0:W] + const int64_t H_in = input_dims[dim]; + const int64_t H_out = output_dims[dim]; + const int64_t end = std::min(start + split_size, input_dims[dim]); + memcpy(static_cast(grad_input->DataPtr()) + n * H_in * W + start * W, + static_cast(grad_output->DataPtr()) + n * H_out * W, + (end - start) * W * sizeof(float)); + } + } + return grad_input; +} +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_SPLIT_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_SPLIT_KERNEL(SplitForward) +REGISTER_CPU_SPLIT_KERNEL(SplitBackward) + +#undef REGISTER_CPU_SPLIT_KERNEL diff --git a/infini_train/src/kernels/cpu/stack.cc b/infini_train/src/kernels/cpu/stack.cc index e672e6b..3f30d0d 100644 --- a/infini_train/src/kernels/cpu/stack.cc +++ b/infini_train/src/kernels/cpu/stack.cc @@ -1,91 +1,91 @@ -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { -std::shared_ptr StackForward(const std::vector> &inputs, int64_t dim) { - CHECK(!inputs.empty()); - - const auto &base_dims = inputs[0]->Dims(); - if (dim < 0) { - dim += base_dims.size() + 1; - } - CHECK_GE(dim, 0); - CHECK_LE(dim, base_dims.size()); - - for (const auto &input : inputs) { CHECK(input->Dims() == base_dims); } - - std::vector output_dims = base_dims; - output_dims.insert(output_dims.begin() + dim, inputs.size()); - - auto output = std::make_shared(output_dims, DataType::kFLOAT32); - - const int64_t outer_size - = std::accumulate(output_dims.begin(), output_dims.begin() + dim, 1, std::multiplies()); - const int64_t inner_size - = std::accumulate(output_dims.begin() + dim + 1, output_dims.end(), 1, std::multiplies()); - const size_t elem_size = sizeof(float); - - for (size_t i = 0; i < inputs.size(); ++i) { - const float *src_ptr = static_cast(inputs[i]->DataPtr()); - float *dst_ptr = static_cast(output->DataPtr()); - for (int64_t n = 0; n < outer_size; ++n) { - float *dst_block = dst_ptr + (n * inputs.size() + i) * inner_size; - const float *src_block = src_ptr + n * inner_size; - memcpy(dst_block, src_block, inner_size * elem_size); - } - } - - return output; -} - -std::vector> StackBackward(const std::vector &input_dims, int64_t dim, - const std::shared_ptr &grad_output) { - const auto &grad_dims = grad_output->Dims(); - int64_t actual_dim = dim < 0 ? dim + grad_dims.size() : dim; - CHECK_GE(actual_dim, 0); - CHECK_LT(actual_dim, grad_dims.size()); - - const int64_t num_inputs = grad_dims[actual_dim]; - std::vector> grads; - - std::vector out_dims = grad_dims; - out_dims.erase(out_dims.begin() + actual_dim); // remove stack dim - - const int64_t outer_size - = std::accumulate(out_dims.begin(), out_dims.begin() + actual_dim, 1, std::multiplies()); - const int64_t inner_size - = std::accumulate(out_dims.begin() + actual_dim, out_dims.end(), 1, std::multiplies()); - - const float *src_base = static_cast(grad_output->DataPtr()); - - for (int i = 0; i < num_inputs; ++i) { - auto grad = std::make_shared(input_dims, DataType::kFLOAT32); - float *dst_ptr = static_cast(grad->DataPtr()); - - for (int64_t n = 0; n < outer_size; ++n) { - const float *src_ptr = src_base + (n * num_inputs + i) * inner_size; - memcpy(dst_ptr + n * inner_size, src_ptr, inner_size * sizeof(float)); - } - grads.push_back(grad); - } - - return grads; -} - -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_STACK_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_STACK_KERNEL(StackForward) -REGISTER_CPU_STACK_KERNEL(StackBackward) - -#undef REGISTER_CPU_STACK_KERNEL +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { +std::shared_ptr StackForward(const std::vector> &inputs, int64_t dim) { + CHECK(!inputs.empty()); + + const auto &base_dims = inputs[0]->Dims(); + if (dim < 0) { + dim += base_dims.size() + 1; + } + CHECK_GE(dim, 0); + CHECK_LE(dim, base_dims.size()); + + for (const auto &input : inputs) { CHECK(input->Dims() == base_dims); } + + std::vector output_dims = base_dims; + output_dims.insert(output_dims.begin() + dim, inputs.size()); + + auto output = std::make_shared(output_dims, DataType::kFLOAT32); + + const int64_t outer_size + = std::accumulate(output_dims.begin(), output_dims.begin() + dim, 1, std::multiplies()); + const int64_t inner_size + = std::accumulate(output_dims.begin() + dim + 1, output_dims.end(), 1, std::multiplies()); + const size_t elem_size = sizeof(float); + + for (size_t i = 0; i < inputs.size(); ++i) { + const float *src_ptr = static_cast(inputs[i]->DataPtr()); + float *dst_ptr = static_cast(output->DataPtr()); + for (int64_t n = 0; n < outer_size; ++n) { + float *dst_block = dst_ptr + (n * inputs.size() + i) * inner_size; + const float *src_block = src_ptr + n * inner_size; + memcpy(dst_block, src_block, inner_size * elem_size); + } + } + + return output; +} + +std::vector> StackBackward(const std::vector &input_dims, int64_t dim, + const std::shared_ptr &grad_output) { + const auto &grad_dims = grad_output->Dims(); + int64_t actual_dim = dim < 0 ? dim + grad_dims.size() : dim; + CHECK_GE(actual_dim, 0); + CHECK_LT(actual_dim, grad_dims.size()); + + const int64_t num_inputs = grad_dims[actual_dim]; + std::vector> grads; + + std::vector out_dims = grad_dims; + out_dims.erase(out_dims.begin() + actual_dim); // remove stack dim + + const int64_t outer_size + = std::accumulate(out_dims.begin(), out_dims.begin() + actual_dim, 1, std::multiplies()); + const int64_t inner_size + = std::accumulate(out_dims.begin() + actual_dim, out_dims.end(), 1, std::multiplies()); + + const float *src_base = static_cast(grad_output->DataPtr()); + + for (int i = 0; i < num_inputs; ++i) { + auto grad = std::make_shared(input_dims, DataType::kFLOAT32); + float *dst_ptr = static_cast(grad->DataPtr()); + + for (int64_t n = 0; n < outer_size; ++n) { + const float *src_ptr = src_base + (n * num_inputs + i) * inner_size; + memcpy(dst_ptr + n * inner_size, src_ptr, inner_size * sizeof(float)); + } + grads.push_back(grad); + } + + return grads; +} + +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_STACK_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_STACK_KERNEL(StackForward) +REGISTER_CPU_STACK_KERNEL(StackBackward) + +#undef REGISTER_CPU_STACK_KERNEL diff --git a/infini_train/src/kernels/cpu/transform.cc b/infini_train/src/kernels/cpu/transform.cc index 5fa6c25..283bad5 100644 --- a/infini_train/src/kernels/cpu/transform.cc +++ b/infini_train/src/kernels/cpu/transform.cc @@ -1,234 +1,234 @@ -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cpu { - -std::shared_ptr TrilForward(const std::shared_ptr &input, int64_t diagonal) { - CHECK_EQ(input->Dims().size(), 2); - - auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); - for (int i = 0; i < input->NumElements(); ++i) { - int64_t row = i / input->Dims()[1]; - int64_t col = i % input->Dims()[1]; - if (row - col + diagonal >= 0) { - static_cast(output->DataPtr())[i] = static_cast(input->DataPtr())[i]; - } else { - static_cast(output->DataPtr())[i] = 0.0; - } - } - return output; -} - -std::shared_ptr TrilBackward(const std::shared_ptr &grad_output, int64_t diagonal) { - auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); - for (int i = 0; i < grad_output->NumElements(); ++i) { - int64_t row = i / grad_output->Dims()[1]; - int64_t col = i % grad_output->Dims()[1]; - if (row - col + diagonal >= 0) { - static_cast(grad_input->DataPtr())[i] = static_cast(grad_output->DataPtr())[i]; - } else { - static_cast(grad_input->DataPtr())[i] = 0.0; - } - } - return grad_input; -} - -std::shared_ptr TriuForward(const std::shared_ptr &input, int64_t diagonal) { - CHECK_EQ(input->Dims().size(), 2); - - auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); - for (int i = 0; i < input->NumElements(); ++i) { - int64_t row = i / input->Dims()[1]; - int64_t col = i % input->Dims()[1]; - if (row - col + diagonal <= 0) { - static_cast(output->DataPtr())[i] = static_cast(input->DataPtr())[i]; - } else { - static_cast(output->DataPtr())[i] = 0.0f; - } - } - return output; -} - -std::shared_ptr TriuBackward(const std::shared_ptr &grad_output, int64_t diagonal) { - CHECK_EQ(grad_output->Dims().size(), 2); - - auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); - for (int i = 0; i < grad_output->NumElements(); ++i) { - int64_t row = i / grad_output->Dims()[1]; - int64_t col = i % grad_output->Dims()[1]; - if (row - col + diagonal <= 0) { - static_cast(grad_input->DataPtr())[i] = static_cast(grad_output->DataPtr())[i]; - } else { - static_cast(grad_input->DataPtr())[i] = 0.0f; - } - } - return grad_input; -} - -std::shared_ptr TransposeForward(const std::shared_ptr &input, int64_t dim0, int64_t dim1) { - dim0 = dim0 < 0 ? dim0 + input->Dims().size() : dim0; - dim1 = dim1 < 0 ? dim1 + input->Dims().size() : dim1; - CHECK(dim0 >= 0 && dim0 < input->Dims().size() && dim1 >= 0 && dim1 < input->Dims().size()); - - auto in_dims = input->Dims(); - std::vector out_dims = in_dims; - std::swap(out_dims[dim0], out_dims[dim1]); - - auto output = std::make_shared(out_dims, input->Dtype(), input->GetDevice()); - - const float *in_ptr = static_cast(input->DataPtr()); - float *out_ptr = static_cast(output->DataPtr()); - - // compute strides of in_dims and out_dims - std::vector in_strides(in_dims.size(), 1); - std::vector out_strides(out_dims.size(), 1); - for (int i = in_dims.size() - 2; i >= 0; --i) { - in_strides[i] = in_strides[i + 1] * in_dims[i + 1]; - out_strides[i] = out_strides[i + 1] * out_dims[i + 1]; - } - - for (int64_t idx = 0; idx < output->NumElements(); ++idx) { - // multi-dimensional indices from flat index of input - int64_t temp = idx; - std::vector in_index(in_dims.size()); - for (int i = 0; i < in_dims.size(); ++i) { - in_index[i] = temp / in_strides[i]; - temp %= in_strides[i]; - } - - // swap indices at dim0 and dim1 - std::swap(in_index[dim0], in_index[dim1]); - - // flat index of output - int64_t out_idx = 0; - for (int i = 0; i < out_dims.size(); ++i) { out_idx += in_index[i] * out_strides[i]; } - - out_ptr[out_idx] = in_ptr[idx]; - } - - return output; -} - -std::shared_ptr TransposeBackward(const std::shared_ptr &grad_output, int64_t dim0, int64_t dim1) { - return TransposeForward(grad_output, dim1, dim0); -} - -std::shared_ptr MaskForward(const std::shared_ptr &input, const std::shared_ptr &mask, - float value) { - CHECK_EQ(input->NumElements() % mask->NumElements(), 0); - CHECK_EQ(static_cast(input->Dtype()), static_cast(mask->Dtype())); - auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); - - const float *in_ptr = static_cast(input->DataPtr()); - - for (int i = 0; i < input->NumElements(); ++i) { - if ((std::abs(static_cast(mask->DataPtr())[i % mask->NumElements()] - 1.0f) < 1e-5)) { - static_cast(output->DataPtr())[i] = value; - } else { - static_cast(output->DataPtr())[i] = in_ptr[i]; - } - } - return output; -} - -std::shared_ptr MaskBackward(const std::shared_ptr &grad_output, const std::shared_ptr &mask) { - CHECK_EQ(grad_output->NumElements() % mask->NumElements(), 0); - CHECK_EQ(static_cast(grad_output->Dtype()), static_cast(mask->Dtype())); - auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); - - for (int i = 0; i < grad_output->NumElements(); ++i) { - if ((std::abs(static_cast(mask->DataPtr())[i % mask->NumElements()] - 1.0f) < 1e-5)) { - static_cast(grad_input->DataPtr())[i] = 0.0; - } else { - static_cast(grad_input->DataPtr())[i] = static_cast(grad_output->DataPtr())[i]; - } - } - return grad_input; -} - -std::shared_ptr RepeatInterleaveForward(const std::shared_ptr &input, int64_t repeat, int64_t dim) { - CHECK_GT(repeat, 0); - CHECK_GE(dim, 0); - CHECK_LT(dim, input->Dims().size()); - - const auto &input_dims = input->Dims(); - const int64_t outer = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); - const int64_t inner - = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); - const int64_t dim_size = input_dims[dim]; - - std::vector output_dims = input_dims; - output_dims[dim] = dim_size * repeat; - auto output = std::make_shared(output_dims, input->Dtype(), input->GetDevice()); - - const float *input_ptr = static_cast(input->DataPtr()); - float *output_ptr = static_cast(output->DataPtr()); - - for (int64_t o = 0; o < outer; ++o) { - for (int64_t i = 0; i < dim_size; ++i) { - for (int r = 0; r < repeat; ++r) { - std::memcpy(output_ptr + ((o * dim_size * repeat + i * repeat + r) * inner), - input_ptr + ((o * dim_size + i) * inner), sizeof(float) * inner); - } - } - } - - return output; -} - -std::shared_ptr RepeatInterleaveBackward(const std::shared_ptr &grad_output, - const std::vector &input_dims, int64_t dim) { - CHECK_GE(dim, 0); - CHECK_LT(dim, input_dims.size()); - - const int64_t outer = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); - const int64_t inner - = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); - const int64_t dim_size = input_dims[dim]; - - int repeat = grad_output->Dims()[dim] / dim_size; - CHECK_EQ(grad_output->Dims()[dim], dim_size * repeat); - - auto grad_input = std::make_shared(input_dims, grad_output->Dtype(), grad_output->GetDevice()); - grad_input->Fill(0.0f); - - const float *grad_out_ptr = static_cast(grad_output->DataPtr()); - float *grad_in_ptr = static_cast(grad_input->DataPtr()); - - for (int64_t o = 0; o < outer; ++o) { - for (int64_t i = 0; i < dim_size; ++i) { - for (int64_t j = 0; j < inner; ++j) { - float sum = 0.0f; - for (int r = 0; r < repeat; ++r) { - sum += grad_out_ptr[((o * dim_size * repeat + i * repeat + r) * inner) + j]; - } - grad_in_ptr[(o * dim_size + i) * inner + j] = sum; - } - } - } - - return grad_input; -} - -} // namespace infini_train::kernels::cpu - -#define REGISTER_CPU_TRANSFORM_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) - -REGISTER_CPU_TRANSFORM_KERNEL(TrilForward) -REGISTER_CPU_TRANSFORM_KERNEL(TrilBackward) -REGISTER_CPU_TRANSFORM_KERNEL(TriuForward) -REGISTER_CPU_TRANSFORM_KERNEL(TriuBackward) -REGISTER_CPU_TRANSFORM_KERNEL(TransposeForward) -REGISTER_CPU_TRANSFORM_KERNEL(TransposeBackward) -REGISTER_CPU_TRANSFORM_KERNEL(MaskForward) -REGISTER_CPU_TRANSFORM_KERNEL(MaskBackward) -REGISTER_CPU_TRANSFORM_KERNEL(RepeatInterleaveForward) -REGISTER_CPU_TRANSFORM_KERNEL(RepeatInterleaveBackward) - -#undef REGISTER_CPU_TRANSFORM_KERNEL +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cpu { + +std::shared_ptr TrilForward(const std::shared_ptr &input, int64_t diagonal) { + CHECK_EQ(input->Dims().size(), 2); + + auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); + for (int i = 0; i < input->NumElements(); ++i) { + int64_t row = i / input->Dims()[1]; + int64_t col = i % input->Dims()[1]; + if (row - col + diagonal >= 0) { + static_cast(output->DataPtr())[i] = static_cast(input->DataPtr())[i]; + } else { + static_cast(output->DataPtr())[i] = 0.0; + } + } + return output; +} + +std::shared_ptr TrilBackward(const std::shared_ptr &grad_output, int64_t diagonal) { + auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); + for (int i = 0; i < grad_output->NumElements(); ++i) { + int64_t row = i / grad_output->Dims()[1]; + int64_t col = i % grad_output->Dims()[1]; + if (row - col + diagonal >= 0) { + static_cast(grad_input->DataPtr())[i] = static_cast(grad_output->DataPtr())[i]; + } else { + static_cast(grad_input->DataPtr())[i] = 0.0; + } + } + return grad_input; +} + +std::shared_ptr TriuForward(const std::shared_ptr &input, int64_t diagonal) { + CHECK_EQ(input->Dims().size(), 2); + + auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); + for (int i = 0; i < input->NumElements(); ++i) { + int64_t row = i / input->Dims()[1]; + int64_t col = i % input->Dims()[1]; + if (row - col + diagonal <= 0) { + static_cast(output->DataPtr())[i] = static_cast(input->DataPtr())[i]; + } else { + static_cast(output->DataPtr())[i] = 0.0f; + } + } + return output; +} + +std::shared_ptr TriuBackward(const std::shared_ptr &grad_output, int64_t diagonal) { + CHECK_EQ(grad_output->Dims().size(), 2); + + auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); + for (int i = 0; i < grad_output->NumElements(); ++i) { + int64_t row = i / grad_output->Dims()[1]; + int64_t col = i % grad_output->Dims()[1]; + if (row - col + diagonal <= 0) { + static_cast(grad_input->DataPtr())[i] = static_cast(grad_output->DataPtr())[i]; + } else { + static_cast(grad_input->DataPtr())[i] = 0.0f; + } + } + return grad_input; +} + +std::shared_ptr TransposeForward(const std::shared_ptr &input, int64_t dim0, int64_t dim1) { + dim0 = dim0 < 0 ? dim0 + input->Dims().size() : dim0; + dim1 = dim1 < 0 ? dim1 + input->Dims().size() : dim1; + CHECK(dim0 >= 0 && dim0 < input->Dims().size() && dim1 >= 0 && dim1 < input->Dims().size()); + + auto in_dims = input->Dims(); + std::vector out_dims = in_dims; + std::swap(out_dims[dim0], out_dims[dim1]); + + auto output = std::make_shared(out_dims, input->Dtype(), input->GetDevice()); + + const float *in_ptr = static_cast(input->DataPtr()); + float *out_ptr = static_cast(output->DataPtr()); + + // compute strides of in_dims and out_dims + std::vector in_strides(in_dims.size(), 1); + std::vector out_strides(out_dims.size(), 1); + for (int i = in_dims.size() - 2; i >= 0; --i) { + in_strides[i] = in_strides[i + 1] * in_dims[i + 1]; + out_strides[i] = out_strides[i + 1] * out_dims[i + 1]; + } + + for (int64_t idx = 0; idx < output->NumElements(); ++idx) { + // multi-dimensional indices from flat index of input + int64_t temp = idx; + std::vector in_index(in_dims.size()); + for (int i = 0; i < in_dims.size(); ++i) { + in_index[i] = temp / in_strides[i]; + temp %= in_strides[i]; + } + + // swap indices at dim0 and dim1 + std::swap(in_index[dim0], in_index[dim1]); + + // flat index of output + int64_t out_idx = 0; + for (int i = 0; i < out_dims.size(); ++i) { out_idx += in_index[i] * out_strides[i]; } + + out_ptr[out_idx] = in_ptr[idx]; + } + + return output; +} + +std::shared_ptr TransposeBackward(const std::shared_ptr &grad_output, int64_t dim0, int64_t dim1) { + return TransposeForward(grad_output, dim1, dim0); +} + +std::shared_ptr MaskForward(const std::shared_ptr &input, const std::shared_ptr &mask, + float value) { + CHECK_EQ(input->NumElements() % mask->NumElements(), 0); + CHECK_EQ(static_cast(input->Dtype()), static_cast(mask->Dtype())); + auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); + + const float *in_ptr = static_cast(input->DataPtr()); + + for (int i = 0; i < input->NumElements(); ++i) { + if ((std::abs(static_cast(mask->DataPtr())[i % mask->NumElements()] - 1.0f) < 1e-5)) { + static_cast(output->DataPtr())[i] = value; + } else { + static_cast(output->DataPtr())[i] = in_ptr[i]; + } + } + return output; +} + +std::shared_ptr MaskBackward(const std::shared_ptr &grad_output, const std::shared_ptr &mask) { + CHECK_EQ(grad_output->NumElements() % mask->NumElements(), 0); + CHECK_EQ(static_cast(grad_output->Dtype()), static_cast(mask->Dtype())); + auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); + + for (int i = 0; i < grad_output->NumElements(); ++i) { + if ((std::abs(static_cast(mask->DataPtr())[i % mask->NumElements()] - 1.0f) < 1e-5)) { + static_cast(grad_input->DataPtr())[i] = 0.0; + } else { + static_cast(grad_input->DataPtr())[i] = static_cast(grad_output->DataPtr())[i]; + } + } + return grad_input; +} + +std::shared_ptr RepeatInterleaveForward(const std::shared_ptr &input, int64_t repeat, int64_t dim) { + CHECK_GT(repeat, 0); + CHECK_GE(dim, 0); + CHECK_LT(dim, input->Dims().size()); + + const auto &input_dims = input->Dims(); + const int64_t outer = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); + const int64_t inner + = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); + const int64_t dim_size = input_dims[dim]; + + std::vector output_dims = input_dims; + output_dims[dim] = dim_size * repeat; + auto output = std::make_shared(output_dims, input->Dtype(), input->GetDevice()); + + const float *input_ptr = static_cast(input->DataPtr()); + float *output_ptr = static_cast(output->DataPtr()); + + for (int64_t o = 0; o < outer; ++o) { + for (int64_t i = 0; i < dim_size; ++i) { + for (int r = 0; r < repeat; ++r) { + std::memcpy(output_ptr + ((o * dim_size * repeat + i * repeat + r) * inner), + input_ptr + ((o * dim_size + i) * inner), sizeof(float) * inner); + } + } + } + + return output; +} + +std::shared_ptr RepeatInterleaveBackward(const std::shared_ptr &grad_output, + const std::vector &input_dims, int64_t dim) { + CHECK_GE(dim, 0); + CHECK_LT(dim, input_dims.size()); + + const int64_t outer = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); + const int64_t inner + = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); + const int64_t dim_size = input_dims[dim]; + + int repeat = grad_output->Dims()[dim] / dim_size; + CHECK_EQ(grad_output->Dims()[dim], dim_size * repeat); + + auto grad_input = std::make_shared(input_dims, grad_output->Dtype(), grad_output->GetDevice()); + grad_input->Fill(0.0f); + + const float *grad_out_ptr = static_cast(grad_output->DataPtr()); + float *grad_in_ptr = static_cast(grad_input->DataPtr()); + + for (int64_t o = 0; o < outer; ++o) { + for (int64_t i = 0; i < dim_size; ++i) { + for (int64_t j = 0; j < inner; ++j) { + float sum = 0.0f; + for (int r = 0; r < repeat; ++r) { + sum += grad_out_ptr[((o * dim_size * repeat + i * repeat + r) * inner) + j]; + } + grad_in_ptr[(o * dim_size + i) * inner + j] = sum; + } + } + } + + return grad_input; +} + +} // namespace infini_train::kernels::cpu + +#define REGISTER_CPU_TRANSFORM_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCPU, kernel_name, infini_train::kernels::cpu::kernel_name) + +REGISTER_CPU_TRANSFORM_KERNEL(TrilForward) +REGISTER_CPU_TRANSFORM_KERNEL(TrilBackward) +REGISTER_CPU_TRANSFORM_KERNEL(TriuForward) +REGISTER_CPU_TRANSFORM_KERNEL(TriuBackward) +REGISTER_CPU_TRANSFORM_KERNEL(TransposeForward) +REGISTER_CPU_TRANSFORM_KERNEL(TransposeBackward) +REGISTER_CPU_TRANSFORM_KERNEL(MaskForward) +REGISTER_CPU_TRANSFORM_KERNEL(MaskBackward) +REGISTER_CPU_TRANSFORM_KERNEL(RepeatInterleaveForward) +REGISTER_CPU_TRANSFORM_KERNEL(RepeatInterleaveBackward) + +#undef REGISTER_CPU_TRANSFORM_KERNEL diff --git a/infini_train/src/kernels/cuda/accumulate_grad.cu b/infini_train/src/kernels/cuda/accumulate_grad.cu index 5f977c3..bb6093b 100644 --- a/infini_train/src/kernels/cuda/accumulate_grad.cu +++ b/infini_train/src/kernels/cuda/accumulate_grad.cu @@ -1,41 +1,70 @@ -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { - -__global__ void AccumulateGradKernel(const float *grad_ptr, float rate, float *tensor_ptr, size_t num_elements) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx < num_elements) { - tensor_ptr[idx] += rate * grad_ptr[idx]; - } -} - -void AccumulateGrad(const std::shared_ptr &gradient, float rate, const std::shared_ptr &tensor) { - size_t num_elements = gradient->NumElements(); - - const float *grad_ptr = static_cast(gradient->DataPtr()); - float *tensor_ptr = static_cast(tensor->DataPtr()); - - int threads_per_block = 256; - int num_blocks = (num_elements + threads_per_block - 1) / threads_per_block; - - AccumulateGradKernel<<>>(grad_ptr, rate, tensor_ptr, num_elements); -} - -void AdamAccumulateGrad(const std::shared_ptr &grad, const std::shared_ptr ¶m, - const std::shared_ptr &m, const std::shared_ptr &v, float learning_rate, - float beta1, float beta2, float eps, int64_t t) { - // =================================== 作业 =================================== - // TODO:实现Adam优化器的梯度累积和参数更新 - // REF: - // =================================== 作业 =================================== -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_ACCUMULATE_GRAD_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_ACCUMULATE_GRAD_KERNEL(AccumulateGrad) -REGISTER_CUDA_ACCUMULATE_GRAD_KERNEL(AdamAccumulateGrad) - -#undef REGISTER_CUDA_ACCUMULATE_GRAD_KERNEL +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { + +__global__ void AccumulateGradKernel(const float *grad_ptr, float rate, float *tensor_ptr, size_t num_elements) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < num_elements) { + tensor_ptr[idx] += rate * grad_ptr[idx]; + } +} + +void AccumulateGrad(const std::shared_ptr &gradient, float rate, const std::shared_ptr &tensor) { + size_t num_elements = gradient->NumElements(); + + const float *grad_ptr = static_cast(gradient->DataPtr()); + float *tensor_ptr = static_cast(tensor->DataPtr()); + + int threads_per_block = 256; + int num_blocks = (num_elements + threads_per_block - 1) / threads_per_block; + + AccumulateGradKernel<<>>(grad_ptr, rate, tensor_ptr, num_elements); +} + +__global__ void AdamAccumulateGradKernel(const float *grad_ptr, float *param_ptr, float *m_ptr, float *v_ptr, + float learning_rate, float beta1, float beta2, float eps, float alpha, + size_t num_elements) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < num_elements) { + float g = grad_ptr[idx]; + m_ptr[idx] = beta1 * m_ptr[idx] + (1.0f - beta1) * g; + v_ptr[idx] = beta2 * v_ptr[idx] + (1.0f - beta2) * g * g; + param_ptr[idx] -= alpha * m_ptr[idx] / (sqrtf(v_ptr[idx]) + eps); + } +} + +void AdamAccumulateGrad(const std::shared_ptr &grad, const std::shared_ptr ¶m, + const std::shared_ptr &m, const std::shared_ptr &v, float learning_rate, + float beta1, float beta2, float eps, int64_t t) { + // =================================== 作业 =================================== + // TODO:实现Adam优化器的梯度累积和参数更新 + // REF: + // =================================== 作业 =================================== + + size_t num_elements = grad->NumElements(); + + const float *grad_ptr = static_cast(grad->DataPtr()); + float *param_ptr = static_cast(param->DataPtr()); + float *m_ptr = static_cast(m->DataPtr()); + float *v_ptr = static_cast(v->DataPtr()); + + float beta1_t = powf(beta1, static_cast(t)); + float beta2_t = powf(beta2, static_cast(t)); + float alpha = learning_rate * sqrtf(1.0f - beta2_t) / (1.0f - beta1_t); + + int threads_per_block = 256; + int num_blocks = (num_elements + threads_per_block - 1) / threads_per_block; + + AdamAccumulateGradKernel<<>>(grad_ptr, param_ptr, m_ptr, v_ptr, learning_rate, beta1, + beta2, eps, alpha, num_elements); +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_ACCUMULATE_GRAD_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_ACCUMULATE_GRAD_KERNEL(AccumulateGrad) +REGISTER_CUDA_ACCUMULATE_GRAD_KERNEL(AdamAccumulateGrad) + +#undef REGISTER_CUDA_ACCUMULATE_GRAD_KERNEL diff --git a/infini_train/src/kernels/cuda/cross_entropy.cu b/infini_train/src/kernels/cuda/cross_entropy.cu index 7938329..8288a8c 100644 --- a/infini_train/src/kernels/cuda/cross_entropy.cu +++ b/infini_train/src/kernels/cuda/cross_entropy.cu @@ -1,209 +1,209 @@ -#include -#include -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { -namespace { -constexpr float kNegativeInfinity = -std::numeric_limits::infinity(); -} - -#if defined(CUB_VERSION) && CUB_VERSION >= 200800 - #include - using CubSumOp = ::cuda::std::plus<>; - using CubMaxOp = ::cuda::maximum<>; - using CubMinOp = ::cuda::minimum<>; -#else - using CubSumOp = cub::Sum; - using CubMaxOp = cub::Max; - using CubMinOp = cub::Min; -#endif - -template -__global__ void CrossEntropyForwardKernel(const float *__restrict__ input_ptr, - const TargetType *__restrict__ target_ptr, float *__restrict__ loss_ptr, - int bs, int num_classes) { - __shared__ struct { - float max_logit; - float sum_exp; - TargetType target_class; - typename cub::BlockReduce::TempStorage reduce; - } shared; - - const int sample_idx = blockIdx.x; - if (sample_idx >= bs) { - return; - } - - const int tid = threadIdx.x; - const size_t base = sample_idx * num_classes; - - if (tid == 0) { - shared.target_class = target_ptr[sample_idx]; - } - __syncthreads(); - - // calculate the max - float thread_max = kNegativeInfinity; - for (int i = tid; i < num_classes; i += BLOCK_SIZE) { thread_max = fmaxf(thread_max, input_ptr[base + i]); } - shared.max_logit = cub::BlockReduce(shared.reduce).Reduce(thread_max, CubMaxOp()); - __syncthreads(); - - // calculate the sum of exponents - float thread_sum = 0.0f; - for (int i = tid; i < num_classes; i += BLOCK_SIZE) { thread_sum += expf(input_ptr[base + i] - shared.max_logit); } - shared.sum_exp = cub::BlockReduce(shared.reduce).Sum(thread_sum); - __syncthreads(); - - // calculate the loss - if (tid == 0) { - const float target_val = input_ptr[base + shared.target_class] - shared.max_logit; - loss_ptr[sample_idx] = logf(shared.sum_exp) - target_val; - } -} - -std::shared_ptr CrossEntropyForward(const std::shared_ptr &input, - const std::shared_ptr &target) { - const auto &input_dims = input->Dims(); - CHECK_GE(input_dims.size(), 2); - const int bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); - const int num_classes = *input_dims.rbegin(); - - auto batched_output = std::make_shared(std::vector{bs}, DataType::kFLOAT32, input->GetDevice()); - const float *input_ptr = static_cast(input->DataPtr()); - float *batched_loss_ptr = static_cast(batched_output->DataPtr()); - - constexpr int threads_per_block = 256; - int num_blocks = bs; - - switch (target->Dtype()) { - case DataType::kUINT8: { - const uint8_t *target_ptr = static_cast(target->DataPtr()); - CrossEntropyForwardKernel - <<>>(input_ptr, target_ptr, batched_loss_ptr, bs, num_classes); - break; - } - case DataType::kINT64: { - const int64_t *target_ptr = static_cast(target->DataPtr()); - CrossEntropyForwardKernel - <<>>(input_ptr, target_ptr, batched_loss_ptr, bs, num_classes); - break; - } - default: - LOG(FATAL) << "Unsupported target data type: " << static_cast(target->Dtype()); - } - cudaDeviceSynchronize(); - - auto loss_cpu = batched_output->To(Device()); - auto loss = std::make_shared(std::vector{}, DataType::kFLOAT32, Device()); - static_cast(loss->DataPtr())[0] - = std::accumulate(static_cast(loss_cpu.DataPtr()), - static_cast(loss_cpu.DataPtr()) + bs, 0.0f) - / bs; - - return {std::make_shared(loss->To(input->GetDevice()))}; -} - -template -__global__ void CrossEntropyBackwardKernel(const float *__restrict__ input_ptr, float *__restrict__ input_grad_ptr, - const TargetType *__restrict__ target_ptr, int bs, int num_classes) { - __shared__ struct { - float max_logit; - float sum_exp; - int target_class; - typename cub::BlockReduce::TempStorage reduce; - } shared; - - const int tid = threadIdx.x; - const int idx = blockIdx.x; - - if (idx >= bs) { - return; - } - - const size_t idx_base = idx * num_classes; - - if (tid == 0) { - shared.target_class = static_cast(target_ptr[idx]); - } - __syncthreads(); - - // calculate the max - float thread_max = kNegativeInfinity; - for (int i = tid; i < num_classes; i += BLOCK_SIZE) { thread_max = fmaxf(thread_max, input_ptr[idx_base + i]); } - shared.max_logit = cub::BlockReduce(shared.reduce).Reduce(thread_max, CubMaxOp()); - __syncthreads(); - - // calculate the sum - float thread_sum = 0.0f; - for (int i = tid; i < num_classes; i += BLOCK_SIZE) { - thread_sum += expf(input_ptr[idx_base + i] - shared.max_logit); - } - shared.sum_exp = cub::BlockReduce(shared.reduce).Sum(thread_sum); - __syncthreads(); - - // calculate the gradient - const float inv_bs = 1.0f / bs; - const float scale = 1.0f / shared.sum_exp; - const int target = shared.target_class; - - for (int i = tid; i < num_classes; i += BLOCK_SIZE) { - const int global_idx = idx_base + i; - const float exp_val = expf(input_ptr[global_idx] - shared.max_logit); - input_grad_ptr[global_idx] = (exp_val * scale - (i == target)) * inv_bs; - } -} - -std::shared_ptr CrossEntropyBackward(const std::shared_ptr &input, - const std::shared_ptr &target, - const std::shared_ptr &grad_output) { - const auto &input_dims = input->Dims(); - CHECK_GE(input_dims.size(), 2); - const int bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); - const int num_classes = *input_dims.rbegin(); - - CHECK_EQ(grad_output->Dims().size(), 0); - auto grad_input = std::make_shared(input->Dims(), DataType::kFLOAT32, grad_output->GetDevice()); - grad_input->Fill(0.0f); - const float *input_ptr = static_cast(input->DataPtr()); - float *input_grad_ptr = static_cast(grad_input->DataPtr()); - - constexpr int threads_per_block = 256; - int num_blocks = bs; - - switch (target->Dtype()) { - case DataType::kUINT8: { - const uint8_t *target_ptr = static_cast(target->DataPtr()); - CrossEntropyBackwardKernel - <<>>(input_ptr, input_grad_ptr, target_ptr, bs, num_classes); - break; - } - case DataType::kINT64: { - const int64_t *target_ptr = static_cast(target->DataPtr()); - CrossEntropyBackwardKernel - <<>>(input_ptr, input_grad_ptr, target_ptr, bs, num_classes); - break; - } - default: - LOG(FATAL) << "Unsupported target data type: " << static_cast(target->Dtype()); - } - - return {grad_input}; -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_CROSS_ENTROPY_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_CROSS_ENTROPY_KERNEL(CrossEntropyForward) -REGISTER_CUDA_CROSS_ENTROPY_KERNEL(CrossEntropyBackward) - -#undef REGISTER_CUDA_CROSS_ENTROPY_KERNEL +#include +#include +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { +namespace { +constexpr float kNegativeInfinity = -std::numeric_limits::infinity(); +} + +#if defined(CUB_VERSION) && CUB_VERSION >= 200800 + #include + using CubSumOp = ::cuda::std::plus<>; + using CubMaxOp = ::cuda::maximum<>; + using CubMinOp = ::cuda::minimum<>; +#else + using CubSumOp = cub::Sum; + using CubMaxOp = cub::Max; + using CubMinOp = cub::Min; +#endif + +template +__global__ void CrossEntropyForwardKernel(const float *__restrict__ input_ptr, + const TargetType *__restrict__ target_ptr, float *__restrict__ loss_ptr, + int bs, int num_classes) { + __shared__ struct { + float max_logit; + float sum_exp; + TargetType target_class; + typename cub::BlockReduce::TempStorage reduce; + } shared; + + const int sample_idx = blockIdx.x; + if (sample_idx >= bs) { + return; + } + + const int tid = threadIdx.x; + const size_t base = sample_idx * num_classes; + + if (tid == 0) { + shared.target_class = target_ptr[sample_idx]; + } + __syncthreads(); + + // calculate the max + float thread_max = kNegativeInfinity; + for (int i = tid; i < num_classes; i += BLOCK_SIZE) { thread_max = fmaxf(thread_max, input_ptr[base + i]); } + shared.max_logit = cub::BlockReduce(shared.reduce).Reduce(thread_max, CubMaxOp()); + __syncthreads(); + + // calculate the sum of exponents + float thread_sum = 0.0f; + for (int i = tid; i < num_classes; i += BLOCK_SIZE) { thread_sum += expf(input_ptr[base + i] - shared.max_logit); } + shared.sum_exp = cub::BlockReduce(shared.reduce).Sum(thread_sum); + __syncthreads(); + + // calculate the loss + if (tid == 0) { + const float target_val = input_ptr[base + shared.target_class] - shared.max_logit; + loss_ptr[sample_idx] = logf(shared.sum_exp) - target_val; + } +} + +std::shared_ptr CrossEntropyForward(const std::shared_ptr &input, + const std::shared_ptr &target) { + const auto &input_dims = input->Dims(); + CHECK_GE(input_dims.size(), 2); + const int bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); + const int num_classes = *input_dims.rbegin(); + + auto batched_output = std::make_shared(std::vector{bs}, DataType::kFLOAT32, input->GetDevice()); + const float *input_ptr = static_cast(input->DataPtr()); + float *batched_loss_ptr = static_cast(batched_output->DataPtr()); + + constexpr int threads_per_block = 256; + int num_blocks = bs; + + switch (target->Dtype()) { + case DataType::kUINT8: { + const uint8_t *target_ptr = static_cast(target->DataPtr()); + CrossEntropyForwardKernel + <<>>(input_ptr, target_ptr, batched_loss_ptr, bs, num_classes); + break; + } + case DataType::kINT64: { + const int64_t *target_ptr = static_cast(target->DataPtr()); + CrossEntropyForwardKernel + <<>>(input_ptr, target_ptr, batched_loss_ptr, bs, num_classes); + break; + } + default: + LOG(FATAL) << "Unsupported target data type: " << static_cast(target->Dtype()); + } + cudaDeviceSynchronize(); + + auto loss_cpu = batched_output->To(Device()); + auto loss = std::make_shared(std::vector{}, DataType::kFLOAT32, Device()); + static_cast(loss->DataPtr())[0] + = std::accumulate(static_cast(loss_cpu.DataPtr()), + static_cast(loss_cpu.DataPtr()) + bs, 0.0f) + / bs; + + return {std::make_shared(loss->To(input->GetDevice()))}; +} + +template +__global__ void CrossEntropyBackwardKernel(const float *__restrict__ input_ptr, float *__restrict__ input_grad_ptr, + const TargetType *__restrict__ target_ptr, int bs, int num_classes) { + __shared__ struct { + float max_logit; + float sum_exp; + int target_class; + typename cub::BlockReduce::TempStorage reduce; + } shared; + + const int tid = threadIdx.x; + const int idx = blockIdx.x; + + if (idx >= bs) { + return; + } + + const size_t idx_base = idx * num_classes; + + if (tid == 0) { + shared.target_class = static_cast(target_ptr[idx]); + } + __syncthreads(); + + // calculate the max + float thread_max = kNegativeInfinity; + for (int i = tid; i < num_classes; i += BLOCK_SIZE) { thread_max = fmaxf(thread_max, input_ptr[idx_base + i]); } + shared.max_logit = cub::BlockReduce(shared.reduce).Reduce(thread_max, CubMaxOp()); + __syncthreads(); + + // calculate the sum + float thread_sum = 0.0f; + for (int i = tid; i < num_classes; i += BLOCK_SIZE) { + thread_sum += expf(input_ptr[idx_base + i] - shared.max_logit); + } + shared.sum_exp = cub::BlockReduce(shared.reduce).Sum(thread_sum); + __syncthreads(); + + // calculate the gradient + const float inv_bs = 1.0f / bs; + const float scale = 1.0f / shared.sum_exp; + const int target = shared.target_class; + + for (int i = tid; i < num_classes; i += BLOCK_SIZE) { + const int global_idx = idx_base + i; + const float exp_val = expf(input_ptr[global_idx] - shared.max_logit); + input_grad_ptr[global_idx] = (exp_val * scale - (i == target)) * inv_bs; + } +} + +std::shared_ptr CrossEntropyBackward(const std::shared_ptr &input, + const std::shared_ptr &target, + const std::shared_ptr &grad_output) { + const auto &input_dims = input->Dims(); + CHECK_GE(input_dims.size(), 2); + const int bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); + const int num_classes = *input_dims.rbegin(); + + CHECK_EQ(grad_output->Dims().size(), 0); + auto grad_input = std::make_shared(input->Dims(), DataType::kFLOAT32, grad_output->GetDevice()); + grad_input->Fill(0.0f); + const float *input_ptr = static_cast(input->DataPtr()); + float *input_grad_ptr = static_cast(grad_input->DataPtr()); + + constexpr int threads_per_block = 256; + int num_blocks = bs; + + switch (target->Dtype()) { + case DataType::kUINT8: { + const uint8_t *target_ptr = static_cast(target->DataPtr()); + CrossEntropyBackwardKernel + <<>>(input_ptr, input_grad_ptr, target_ptr, bs, num_classes); + break; + } + case DataType::kINT64: { + const int64_t *target_ptr = static_cast(target->DataPtr()); + CrossEntropyBackwardKernel + <<>>(input_ptr, input_grad_ptr, target_ptr, bs, num_classes); + break; + } + default: + LOG(FATAL) << "Unsupported target data type: " << static_cast(target->Dtype()); + } + + return {grad_input}; +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_CROSS_ENTROPY_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_CROSS_ENTROPY_KERNEL(CrossEntropyForward) +REGISTER_CUDA_CROSS_ENTROPY_KERNEL(CrossEntropyBackward) + +#undef REGISTER_CUDA_CROSS_ENTROPY_KERNEL diff --git a/infini_train/src/kernels/cuda/elementwise.cu b/infini_train/src/kernels/cuda/elementwise.cu index e1fa344..d52781c 100644 --- a/infini_train/src/kernels/cuda/elementwise.cu +++ b/infini_train/src/kernels/cuda/elementwise.cu @@ -1,500 +1,500 @@ -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { - -#define CEIL_DIV(x, y) (((x) + (y)-1) / (y)) - -namespace { - -template -__global__ void UnaryForwardKernel(T *output, Func fn, size_t num_elements, size_t offset, const T *input) { - size_t idx = blockIdx.x * blockDim.x + threadIdx.x + offset; - - if (idx < num_elements) { - output[idx] = fn(input[idx]); - } -} - -// Helper for broadcast indexing -__device__ inline int64_t CalcOffset(int64_t idx, int ndim, const int64_t *strides, const int64_t *shape, - const int64_t *out_strides) { - int64_t offset = 0; - for (int i = 0; i < ndim; ++i) { - int64_t out_index = (idx / out_strides[i]) % shape[i]; - int64_t index = shape[i] == 1 ? 0 : out_index; - offset += index * strides[i]; - } - return offset; -} - -template -__global__ void BinaryForwardKernel(T *output, Func fn, int ndim, const int64_t *a_strides, const int64_t *a_shape, - const int64_t *b_strides, const int64_t *b_shape, const int64_t *out_strides, - const T *a, const T *b, size_t num_elements) { - size_t idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx >= num_elements) { - return; - } - - int64_t a_offset = CalcOffset(idx, ndim, a_strides, a_shape, out_strides); - int64_t b_offset = CalcOffset(idx, ndim, b_strides, b_shape, out_strides); - - output[idx] = fn(a[a_offset], b[b_offset]); -} - -// launch the given kernel function with the given output and inputs -template -void LaunchKernel(Kernel &&kernel, const std::shared_ptr &output, const Inputs &...inputs) { - auto extract_ptrs - = [](const auto &...ts) { return std::make_tuple(static_cast(ts ? ts->DataPtr() : nullptr)...); }; - auto input_ptrs = extract_ptrs(inputs...); - - const size_t num_elements = output->NumElements(); - dim3 block_dims(std::min(BLOCK_SIZE, static_cast(1024))); - dim3 grid_dims(CEIL_DIV(num_elements, block_dims.x)); - const size_t step = grid_dims.x * block_dims.x; - - for (size_t offset = 0; offset < num_elements; offset += step) { - std::apply([&](auto... ptrs) { kernel(grid_dims, block_dims, offset, ptrs...); }, input_ptrs); - } -} - -// Helper for stride calculation -std::vector ComputeStride(const std::vector &dims) { - std::vector strides(dims.size(), 1); - for (int i = dims.size() - 2; i >= 0; --i) { strides[i] = strides[i + 1] * dims[i + 1]; } - return strides; -} - -// launch a forward elementwise operation given the calculation function, output, and the inputs -// Note: currently only support unary and binary operations -template -void LaunchForward(Func func, const std::shared_ptr &output, const Inputs &...inputs) { - T *output_ptr = static_cast(output->DataPtr()); - - if constexpr (sizeof...(inputs) == 1) { - // Unary case - LaunchKernel( - [&](dim3 grid, dim3 block, size_t offset, auto... ptrs) { - UnaryForwardKernel<<>>(output_ptr, func, output->NumElements(), offset, ptrs...); - }, - output, inputs...); - } else if constexpr (sizeof...(inputs) == 2) { - // Binary case - auto input_tuple = std::make_tuple(inputs...); - const auto &input_a = std::get<0>(input_tuple); - const auto &input_b = std::get<1>(input_tuple); - - const auto &a_dims = input_a->Dims(); - const auto &b_dims = input_b->Dims(); - const auto &out_dims = output->Dims(); - int ndim = out_dims.size(); - - std::vector a_shape(ndim, 1), b_shape(ndim, 1), out_shape(ndim, 1); - std::copy_backward(a_dims.begin(), a_dims.end(), a_shape.end()); - std::copy_backward(b_dims.begin(), b_dims.end(), b_shape.end()); - std::copy_backward(out_dims.begin(), out_dims.end(), out_shape.end()); - - auto a_stride_host = ComputeStride(a_shape); - auto b_stride_host = ComputeStride(b_shape); - auto out_stride_host = ComputeStride(out_shape); - - int64_t *device_buffer; - cudaMallocAsync(&device_buffer, 5 * ndim * sizeof(int64_t), 0); - - int64_t *device_a_strides, *device_b_strides, *device_out_strides, *device_a_shape, *device_b_shape; - device_a_strides = device_buffer + ndim * 0; - device_b_strides = device_buffer + ndim * 1; - device_out_strides = device_buffer + ndim * 2; - device_a_shape = device_buffer + ndim * 3; - device_b_shape = device_buffer + ndim * 4; - - std::vector host_buffer; - host_buffer.insert(host_buffer.end(), a_stride_host.begin(), a_stride_host.end()); - host_buffer.insert(host_buffer.end(), b_stride_host.begin(), b_stride_host.end()); - host_buffer.insert(host_buffer.end(), out_stride_host.begin(), out_stride_host.end()); - host_buffer.insert(host_buffer.end(), a_shape.begin(), a_shape.end()); - host_buffer.insert(host_buffer.end(), b_shape.begin(), b_shape.end()); - - cudaMemcpyAsync(device_buffer, host_buffer.data(), 5 * ndim * sizeof(int64_t), cudaMemcpyHostToDevice, 0); - - LaunchKernel( - [&](dim3 grid, dim3 block, size_t offset, const T *a_ptr, const T *b_ptr) { - BinaryForwardKernel<<>>(output_ptr, func, ndim, device_a_strides, device_a_shape, - device_b_strides, device_b_shape, device_out_strides, a_ptr, b_ptr, - output->NumElements()); - }, - output, inputs...); - - cudaFreeAsync(device_buffer, 0); - } else { - static_assert(sizeof...(inputs) == 1 || sizeof...(inputs) == 2, - "LaunchForward currently only supports unary and binary operations."); - } -} - -// Backward kernel for unary operators -template -__global__ void UnaryBackwardKernel(T *output, Func fn, size_t num_elements, size_t offset, const T *grad_output, - const T *input) { - size_t idx = blockIdx.x * blockDim.x + threadIdx.x + offset; - - if (idx < num_elements) { - output[idx] = grad_output[idx] * fn(input ? input[idx] : T(0)); - } -} - -// Backward kernel for binary operators -template -__global__ void BinaryBackwardKernel(T *output_a, T *output_b, FuncA fn_a, FuncB fn_b, int ndim, size_t num_elements, - const int64_t *a_strides, const int64_t *a_shape, const int64_t *b_strides, - const int64_t *b_shape, const int64_t *out_strides, const T *grad_output, - const T *input_a, const T *input_b) { - size_t idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx >= num_elements) { - return; - } - - int64_t a_offset = CalcOffset(idx, ndim, a_strides, a_shape, out_strides); - int64_t b_offset = CalcOffset(idx, ndim, b_strides, b_shape, out_strides); - - const T a_val = input_a ? input_a[a_offset] : T(0); - const T b_val = input_b ? input_b[b_offset] : T(0); - - output_a[a_offset] = grad_output[idx] * fn_a(a_val, b_val); - atomicAdd(&output_b[b_offset], grad_output[idx] * fn_b(a_val, b_val)); -} - -// launch unary operator's backward kernel -template -void LaunchBackward(Func func, const std::shared_ptr &output, const std::shared_ptr &grad_output, - const Inputs &...inputs) { - T *output_ptr = static_cast(output->DataPtr()); - const T *grad_ptr = static_cast(grad_output->DataPtr()); - - LaunchKernel( - [=](dim3 grid, dim3 block, size_t offset, auto... ptrs) { - UnaryBackwardKernel<<>>(output_ptr, func, output->NumElements(), offset, grad_ptr, ptrs...); - }, - output, inputs...); -} - -// launch binary operator's backward kernel -template -void LaunchBackward(FuncA fun_a, FuncB fun_b, const std::shared_ptr &output_a, - const std::shared_ptr &output_b, const std::vector &a_dims, - const std::vector &b_dims, const std::shared_ptr &grad_output, - const Inputs &...inputs) { - T *output_a_ptr = static_cast(output_a->DataPtr()); - T *output_b_ptr = static_cast(output_b->DataPtr()); - const T *grad_output_ptr = static_cast(grad_output->DataPtr()); - - const auto &out_dims = grad_output->Dims(); - int ndim = out_dims.size(); - - std::vector a_shape(ndim, 1), b_shape(ndim, 1), out_shape(ndim, 1); - std::copy_backward(a_dims.begin(), a_dims.end(), a_shape.end()); - std::copy_backward(b_dims.begin(), b_dims.end(), b_shape.end()); - std::copy_backward(out_dims.begin(), out_dims.end(), out_shape.end()); - - auto a_stride_host = ComputeStride(a_shape); - auto b_stride_host = ComputeStride(b_shape); - auto out_stride_host = ComputeStride(out_shape); - - int64_t *device_buffer; - cudaMallocAsync(&device_buffer, 5 * ndim * sizeof(int64_t), 0); - - int64_t *device_a_strides, *device_b_strides, *device_out_strides, *device_a_shape, *device_b_shape; - device_a_strides = device_buffer + ndim * 0; - device_b_strides = device_buffer + ndim * 1; - device_out_strides = device_buffer + ndim * 2; - device_a_shape = device_buffer + ndim * 3; - device_b_shape = device_buffer + ndim * 4; - - std::vector host_buffer; - host_buffer.insert(host_buffer.end(), a_stride_host.begin(), a_stride_host.end()); - host_buffer.insert(host_buffer.end(), b_stride_host.begin(), b_stride_host.end()); - host_buffer.insert(host_buffer.end(), out_stride_host.begin(), out_stride_host.end()); - host_buffer.insert(host_buffer.end(), a_shape.begin(), a_shape.end()); - host_buffer.insert(host_buffer.end(), b_shape.begin(), b_shape.end()); - - cudaMemcpyAsync(device_buffer, host_buffer.data(), 5 * ndim * sizeof(int64_t), cudaMemcpyHostToDevice, 0); - - const size_t num_elements = grad_output->NumElements(); - LaunchKernel( - [=](dim3 grid, dim3 block, size_t offset, auto... ptrs) { - BinaryBackwardKernel<<>>(output_a_ptr, output_b_ptr, fun_a, fun_b, ndim, num_elements, - device_a_strides, device_a_shape, device_b_strides, device_b_shape, - device_out_strides, grad_output_ptr, ptrs...); - }, - output_a, inputs...); - - cudaFreeAsync(device_buffer, 0); -} - -template std::shared_ptr UnaryForward(const std::shared_ptr &input, Func unary_fn) { - auto dtype = input->Dtype(); - auto output = std::make_shared(input->Dims(), dtype, input->GetDevice()); - - switch (dtype) { - case DataType::kFLOAT32: - LaunchForward<256, float>(unary_fn, output, input); - break; - default: - LOG(FATAL) << "CUDA unary forward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; - } - - return output; -} - -template -std::shared_ptr UnaryBackward(const std::shared_ptr &grad_output, const std::shared_ptr &a, - Func unary_fn) { - auto dtype = grad_output->Dtype(); - auto output = std::make_shared(grad_output->Dims(), dtype, grad_output->GetDevice()); - output->Fill(0.0f); - switch (dtype) { - case DataType::kFLOAT32: - LaunchBackward<256, float>(unary_fn, output, grad_output, a); - break; - default: - LOG(FATAL) << "CUDA unary backward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; - } - - return output; -} - -template -std::shared_ptr BinaryForward(const std::shared_ptr &a, const std::shared_ptr &b, - Func binary_fn) { - auto dtype = a->Dtype(); - // Currently a and b should have the same data type and only one-way broadcasting from b to a is assumed by default - CHECK(dtype == b->Dtype() && a->NumElements() >= b->NumElements() && a->NumElements() % b->NumElements() == 0); - - auto output = std::make_shared(a->Dims(), dtype, a->GetDevice()); - - switch (dtype) { - case DataType::kFLOAT32: - LaunchForward<256, float>(binary_fn, output, a, b); - break; - default: - LOG(FATAL) << "CUDA binary forward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; - } - - return output; -} - -template -std::pair, std::shared_ptr> -BinaryBackward(const std::shared_ptr &grad_output, const std::shared_ptr &a, - const std::shared_ptr &b, const std::vector &a_dims, const std::vector &b_dims, - FuncA fn_a, FuncB fn_b) { - const auto a_num_elements = std::accumulate(a_dims.begin(), a_dims.end(), 1, std::multiplies()); - const auto b_num_elements = std::accumulate(b_dims.begin(), b_dims.end(), 1, std::multiplies()); - - CHECK(a_num_elements >= b_num_elements && a_num_elements % b_num_elements == 0); - if (a) { - CHECK(a_num_elements == a->NumElements()); - } - if (b) { - CHECK(b_num_elements == b->NumElements()); - } - auto dtype = grad_output->Dtype(); - auto device = grad_output->GetDevice(); - - // Currently a and b should have the same data type - if (a && b) { - CHECK(a->Dtype() == b->Dtype()); - } - auto grad_a = std::make_shared(a_dims, dtype, device); - auto grad_b = std::make_shared(b_dims, dtype, device); - grad_a->Fill(0.0f); - grad_b->Fill(0.0f); - switch (dtype) { - case DataType::kFLOAT32: - LaunchBackward<256, float>(fn_a, fn_b, grad_a, grad_b, a_dims, b_dims, grad_output, a, b); - break; - default: - LOG(FATAL) << "CUDA binary backward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; - } - - return {grad_a, grad_b}; -} -} // namespace - -std::shared_ptr NegForward(const std::shared_ptr &input) { - return UnaryForward(input, [] __device__(float x) { return -x; }); -} - -std::shared_ptr NegBackward(const std::shared_ptr &grad_output) { - return UnaryBackward(grad_output, nullptr, [] __device__(float) { return -1.0f; }); -} - -std::shared_ptr ReciprocalForward(const std::shared_ptr &input) { - return UnaryForward(input, [] __device__(float x) { return 1.0f / x; }); -} - -std::shared_ptr ReciprocalBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &input) { - return UnaryBackward(grad_output, input, [] __device__(float x) { return -1.0f / (x * x); }); -} - -std::shared_ptr SinForward(const std::shared_ptr &input) { - return UnaryForward(input, [] __device__(float x) { return sinf(x); }); -} - -std::shared_ptr SinBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input) { - return UnaryBackward(grad_output, input, [] __device__(float x) { return cosf(x); }); -} - -std::shared_ptr CosForward(const std::shared_ptr &input) { - return UnaryForward(input, [] __device__(float x) { return cosf(x); }); -} - -std::shared_ptr CosBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input) { - return UnaryBackward(grad_output, input, [] __device__(float x) { return -sinf(x); }); -} - -std::shared_ptr TanhForward(const std::shared_ptr &input) { - return UnaryForward(input, [] __device__(float x) { return tanhf(x); }); -} - -std::shared_ptr TanhBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &output) { - return UnaryBackward(grad_output, output, [] __device__(float x) { return 1.0 - x * x; }); -} - -std::shared_ptr PowForward(const std::shared_ptr &input, float scalar, bool scalar_is_base) { - if (scalar_is_base) { - return UnaryForward(input, [scalar] __device__(float x) { return powf(scalar, x); }); - } else { - return UnaryForward(input, [scalar] __device__(float x) { return powf(x, scalar); }); - } -} - -std::shared_ptr PowBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, - float scalar, bool scalar_is_base) { - if (scalar_is_base) { - return UnaryBackward(grad_output, input, - [scalar] __device__(float x) { return logf(scalar) * powf(scalar, x); }); - } else { - return UnaryBackward(grad_output, input, - [scalar] __device__(float x) { return scalar * powf(x, scalar - 1.0f); }); - } -} - -std::shared_ptr RsqrtForward(const std::shared_ptr &input) { - return UnaryForward(input, [] __device__(float x) { return 1.0f / sqrtf(x); }); -} - -std::shared_ptr RsqrtBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &input) { - return UnaryBackward(grad_output, input, [] __device__(float x) { return -0.5f / (x * sqrtf(x)); }); -} - -std::shared_ptr EqualsScalarForward(const std::shared_ptr &a, float scalar) { - return UnaryForward(a, [scalar] __device__(float x) { return x == scalar ? 1.0f : 0.0f; }); -} - -std::shared_ptr AddForward(const std::shared_ptr &a, const std::shared_ptr &b) { - return BinaryForward(a, b, [] __device__(float x, float y) { return x + y; }); -} - -std::pair, std::shared_ptr> AddBackward(const std::shared_ptr &grad_output, - const std::vector &a_dims, - const std::vector &b_dims) { - return BinaryBackward( - grad_output, nullptr, nullptr, a_dims, b_dims, [] __device__(float, float) { return 1.f; }, - [] __device__(float, float) { return 1.f; }); -} - -std::shared_ptr AddScalarForward(const std::shared_ptr &a, float scalar) { - return UnaryForward(a, [scalar] __device__(float x) { return x + scalar; }); -} - -std::shared_ptr AddScalarBackward(const std::shared_ptr &grad_output) { - return UnaryBackward(grad_output, nullptr, [] __device__(float) { return 1.0f; }); -} - -std::shared_ptr SubForward(const std::shared_ptr &a, const std::shared_ptr &b) { - return BinaryForward(a, b, [] __device__(float x, float y) { return x - y; }); -} - -std::pair, std::shared_ptr> SubBackward(const std::shared_ptr &grad_output, - const std::vector &a_dims, - const std::vector &b_dims) { - return BinaryBackward( - grad_output, nullptr, nullptr, a_dims, b_dims, [] __device__(float, float) { return 1.f; }, - [] __device__(float, float) { return -1.f; }); -} - -std::shared_ptr MulForward(const std::shared_ptr &a, const std::shared_ptr &b) { - return BinaryForward(a, b, [] __device__(float x, float y) { return x * y; }); -} - -std::pair, std::shared_ptr> MulBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &a, - const std::shared_ptr &b) { - return BinaryBackward( - grad_output, a, b, a->Dims(), b->Dims(), [] __device__(float, float y) { return y; }, - [] __device__(float x, float) { return x; }); -} - -std::shared_ptr MulScalarForward(const std::shared_ptr &a, float scalar) { - return UnaryForward(a, [scalar] __device__(float x) { return x * scalar; }); -} - -std::shared_ptr MulScalarBackward(const std::shared_ptr &grad_output, float scalar) { - return UnaryBackward(grad_output, nullptr, [scalar] __device__(float) { return scalar; }); -} - -std::shared_ptr DivForward(const std::shared_ptr &a, const std::shared_ptr &b) { - return BinaryForward(a, b, [] __device__(float x, float y) { return x / y; }); -} - -std::pair, std::shared_ptr> DivBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &a, - const std::shared_ptr &b) { - return BinaryBackward( - grad_output, a, b, a->Dims(), b->Dims(), [] __device__(float, float y) { return 1 / y; }, - [] __device__(float x, float y) { return -x / (y * y); }); -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_ELEMENTWISE_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_ELEMENTWISE_KERNEL(NegForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(NegBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(ReciprocalForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(ReciprocalBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(SinForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(SinBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(CosForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(CosBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(TanhForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(TanhBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(PowForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(PowBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(RsqrtForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(RsqrtBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(EqualsScalarForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(AddForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(AddBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(AddScalarForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(AddScalarBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(SubForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(SubBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(MulForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(MulBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(MulScalarForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(MulScalarBackward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(DivForward) -REGISTER_CUDA_ELEMENTWISE_KERNEL(DivBackward) - -#undef REGISTER_CUDA_ELEMENTWISE_KERNEL +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { + +#define CEIL_DIV(x, y) (((x) + (y)-1) / (y)) + +namespace { + +template +__global__ void UnaryForwardKernel(T *output, Func fn, size_t num_elements, size_t offset, const T *input) { + size_t idx = blockIdx.x * blockDim.x + threadIdx.x + offset; + + if (idx < num_elements) { + output[idx] = fn(input[idx]); + } +} + +// Helper for broadcast indexing +__device__ inline int64_t CalcOffset(int64_t idx, int ndim, const int64_t *strides, const int64_t *shape, + const int64_t *out_strides) { + int64_t offset = 0; + for (int i = 0; i < ndim; ++i) { + int64_t out_index = (idx / out_strides[i]) % shape[i]; + int64_t index = shape[i] == 1 ? 0 : out_index; + offset += index * strides[i]; + } + return offset; +} + +template +__global__ void BinaryForwardKernel(T *output, Func fn, int ndim, const int64_t *a_strides, const int64_t *a_shape, + const int64_t *b_strides, const int64_t *b_shape, const int64_t *out_strides, + const T *a, const T *b, size_t num_elements) { + size_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= num_elements) { + return; + } + + int64_t a_offset = CalcOffset(idx, ndim, a_strides, a_shape, out_strides); + int64_t b_offset = CalcOffset(idx, ndim, b_strides, b_shape, out_strides); + + output[idx] = fn(a[a_offset], b[b_offset]); +} + +// launch the given kernel function with the given output and inputs +template +void LaunchKernel(Kernel &&kernel, const std::shared_ptr &output, const Inputs &...inputs) { + auto extract_ptrs + = [](const auto &...ts) { return std::make_tuple(static_cast(ts ? ts->DataPtr() : nullptr)...); }; + auto input_ptrs = extract_ptrs(inputs...); + + const size_t num_elements = output->NumElements(); + dim3 block_dims(std::min(BLOCK_SIZE, static_cast(1024))); + dim3 grid_dims(CEIL_DIV(num_elements, block_dims.x)); + const size_t step = grid_dims.x * block_dims.x; + + for (size_t offset = 0; offset < num_elements; offset += step) { + std::apply([&](auto... ptrs) { kernel(grid_dims, block_dims, offset, ptrs...); }, input_ptrs); + } +} + +// Helper for stride calculation +std::vector ComputeStride(const std::vector &dims) { + std::vector strides(dims.size(), 1); + for (int i = dims.size() - 2; i >= 0; --i) { strides[i] = strides[i + 1] * dims[i + 1]; } + return strides; +} + +// launch a forward elementwise operation given the calculation function, output, and the inputs +// Note: currently only support unary and binary operations +template +void LaunchForward(Func func, const std::shared_ptr &output, const Inputs &...inputs) { + T *output_ptr = static_cast(output->DataPtr()); + + if constexpr (sizeof...(inputs) == 1) { + // Unary case + LaunchKernel( + [&](dim3 grid, dim3 block, size_t offset, auto... ptrs) { + UnaryForwardKernel<<>>(output_ptr, func, output->NumElements(), offset, ptrs...); + }, + output, inputs...); + } else if constexpr (sizeof...(inputs) == 2) { + // Binary case + auto input_tuple = std::make_tuple(inputs...); + const auto &input_a = std::get<0>(input_tuple); + const auto &input_b = std::get<1>(input_tuple); + + const auto &a_dims = input_a->Dims(); + const auto &b_dims = input_b->Dims(); + const auto &out_dims = output->Dims(); + int ndim = out_dims.size(); + + std::vector a_shape(ndim, 1), b_shape(ndim, 1), out_shape(ndim, 1); + std::copy_backward(a_dims.begin(), a_dims.end(), a_shape.end()); + std::copy_backward(b_dims.begin(), b_dims.end(), b_shape.end()); + std::copy_backward(out_dims.begin(), out_dims.end(), out_shape.end()); + + auto a_stride_host = ComputeStride(a_shape); + auto b_stride_host = ComputeStride(b_shape); + auto out_stride_host = ComputeStride(out_shape); + + int64_t *device_buffer; + cudaMallocAsync(&device_buffer, 5 * ndim * sizeof(int64_t), 0); + + int64_t *device_a_strides, *device_b_strides, *device_out_strides, *device_a_shape, *device_b_shape; + device_a_strides = device_buffer + ndim * 0; + device_b_strides = device_buffer + ndim * 1; + device_out_strides = device_buffer + ndim * 2; + device_a_shape = device_buffer + ndim * 3; + device_b_shape = device_buffer + ndim * 4; + + std::vector host_buffer; + host_buffer.insert(host_buffer.end(), a_stride_host.begin(), a_stride_host.end()); + host_buffer.insert(host_buffer.end(), b_stride_host.begin(), b_stride_host.end()); + host_buffer.insert(host_buffer.end(), out_stride_host.begin(), out_stride_host.end()); + host_buffer.insert(host_buffer.end(), a_shape.begin(), a_shape.end()); + host_buffer.insert(host_buffer.end(), b_shape.begin(), b_shape.end()); + + cudaMemcpyAsync(device_buffer, host_buffer.data(), 5 * ndim * sizeof(int64_t), cudaMemcpyHostToDevice, 0); + + LaunchKernel( + [&](dim3 grid, dim3 block, size_t offset, const T *a_ptr, const T *b_ptr) { + BinaryForwardKernel<<>>(output_ptr, func, ndim, device_a_strides, device_a_shape, + device_b_strides, device_b_shape, device_out_strides, a_ptr, b_ptr, + output->NumElements()); + }, + output, inputs...); + + cudaFreeAsync(device_buffer, 0); + } else { + static_assert(sizeof...(inputs) == 1 || sizeof...(inputs) == 2, + "LaunchForward currently only supports unary and binary operations."); + } +} + +// Backward kernel for unary operators +template +__global__ void UnaryBackwardKernel(T *output, Func fn, size_t num_elements, size_t offset, const T *grad_output, + const T *input) { + size_t idx = blockIdx.x * blockDim.x + threadIdx.x + offset; + + if (idx < num_elements) { + output[idx] = grad_output[idx] * fn(input ? input[idx] : T(0)); + } +} + +// Backward kernel for binary operators +template +__global__ void BinaryBackwardKernel(T *output_a, T *output_b, FuncA fn_a, FuncB fn_b, int ndim, size_t num_elements, + const int64_t *a_strides, const int64_t *a_shape, const int64_t *b_strides, + const int64_t *b_shape, const int64_t *out_strides, const T *grad_output, + const T *input_a, const T *input_b) { + size_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= num_elements) { + return; + } + + int64_t a_offset = CalcOffset(idx, ndim, a_strides, a_shape, out_strides); + int64_t b_offset = CalcOffset(idx, ndim, b_strides, b_shape, out_strides); + + const T a_val = input_a ? input_a[a_offset] : T(0); + const T b_val = input_b ? input_b[b_offset] : T(0); + + output_a[a_offset] = grad_output[idx] * fn_a(a_val, b_val); + atomicAdd(&output_b[b_offset], grad_output[idx] * fn_b(a_val, b_val)); +} + +// launch unary operator's backward kernel +template +void LaunchBackward(Func func, const std::shared_ptr &output, const std::shared_ptr &grad_output, + const Inputs &...inputs) { + T *output_ptr = static_cast(output->DataPtr()); + const T *grad_ptr = static_cast(grad_output->DataPtr()); + + LaunchKernel( + [=](dim3 grid, dim3 block, size_t offset, auto... ptrs) { + UnaryBackwardKernel<<>>(output_ptr, func, output->NumElements(), offset, grad_ptr, ptrs...); + }, + output, inputs...); +} + +// launch binary operator's backward kernel +template +void LaunchBackward(FuncA fun_a, FuncB fun_b, const std::shared_ptr &output_a, + const std::shared_ptr &output_b, const std::vector &a_dims, + const std::vector &b_dims, const std::shared_ptr &grad_output, + const Inputs &...inputs) { + T *output_a_ptr = static_cast(output_a->DataPtr()); + T *output_b_ptr = static_cast(output_b->DataPtr()); + const T *grad_output_ptr = static_cast(grad_output->DataPtr()); + + const auto &out_dims = grad_output->Dims(); + int ndim = out_dims.size(); + + std::vector a_shape(ndim, 1), b_shape(ndim, 1), out_shape(ndim, 1); + std::copy_backward(a_dims.begin(), a_dims.end(), a_shape.end()); + std::copy_backward(b_dims.begin(), b_dims.end(), b_shape.end()); + std::copy_backward(out_dims.begin(), out_dims.end(), out_shape.end()); + + auto a_stride_host = ComputeStride(a_shape); + auto b_stride_host = ComputeStride(b_shape); + auto out_stride_host = ComputeStride(out_shape); + + int64_t *device_buffer; + cudaMallocAsync(&device_buffer, 5 * ndim * sizeof(int64_t), 0); + + int64_t *device_a_strides, *device_b_strides, *device_out_strides, *device_a_shape, *device_b_shape; + device_a_strides = device_buffer + ndim * 0; + device_b_strides = device_buffer + ndim * 1; + device_out_strides = device_buffer + ndim * 2; + device_a_shape = device_buffer + ndim * 3; + device_b_shape = device_buffer + ndim * 4; + + std::vector host_buffer; + host_buffer.insert(host_buffer.end(), a_stride_host.begin(), a_stride_host.end()); + host_buffer.insert(host_buffer.end(), b_stride_host.begin(), b_stride_host.end()); + host_buffer.insert(host_buffer.end(), out_stride_host.begin(), out_stride_host.end()); + host_buffer.insert(host_buffer.end(), a_shape.begin(), a_shape.end()); + host_buffer.insert(host_buffer.end(), b_shape.begin(), b_shape.end()); + + cudaMemcpyAsync(device_buffer, host_buffer.data(), 5 * ndim * sizeof(int64_t), cudaMemcpyHostToDevice, 0); + + const size_t num_elements = grad_output->NumElements(); + LaunchKernel( + [=](dim3 grid, dim3 block, size_t offset, auto... ptrs) { + BinaryBackwardKernel<<>>(output_a_ptr, output_b_ptr, fun_a, fun_b, ndim, num_elements, + device_a_strides, device_a_shape, device_b_strides, device_b_shape, + device_out_strides, grad_output_ptr, ptrs...); + }, + output_a, inputs...); + + cudaFreeAsync(device_buffer, 0); +} + +template std::shared_ptr UnaryForward(const std::shared_ptr &input, Func unary_fn) { + auto dtype = input->Dtype(); + auto output = std::make_shared(input->Dims(), dtype, input->GetDevice()); + + switch (dtype) { + case DataType::kFLOAT32: + LaunchForward<256, float>(unary_fn, output, input); + break; + default: + LOG(FATAL) << "CUDA unary forward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; + } + + return output; +} + +template +std::shared_ptr UnaryBackward(const std::shared_ptr &grad_output, const std::shared_ptr &a, + Func unary_fn) { + auto dtype = grad_output->Dtype(); + auto output = std::make_shared(grad_output->Dims(), dtype, grad_output->GetDevice()); + output->Fill(0.0f); + switch (dtype) { + case DataType::kFLOAT32: + LaunchBackward<256, float>(unary_fn, output, grad_output, a); + break; + default: + LOG(FATAL) << "CUDA unary backward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; + } + + return output; +} + +template +std::shared_ptr BinaryForward(const std::shared_ptr &a, const std::shared_ptr &b, + Func binary_fn) { + auto dtype = a->Dtype(); + // Currently a and b should have the same data type and only one-way broadcasting from b to a is assumed by default + CHECK(dtype == b->Dtype() && a->NumElements() >= b->NumElements() && a->NumElements() % b->NumElements() == 0); + + auto output = std::make_shared(a->Dims(), dtype, a->GetDevice()); + + switch (dtype) { + case DataType::kFLOAT32: + LaunchForward<256, float>(binary_fn, output, a, b); + break; + default: + LOG(FATAL) << "CUDA binary forward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; + } + + return output; +} + +template +std::pair, std::shared_ptr> +BinaryBackward(const std::shared_ptr &grad_output, const std::shared_ptr &a, + const std::shared_ptr &b, const std::vector &a_dims, const std::vector &b_dims, + FuncA fn_a, FuncB fn_b) { + const auto a_num_elements = std::accumulate(a_dims.begin(), a_dims.end(), 1, std::multiplies()); + const auto b_num_elements = std::accumulate(b_dims.begin(), b_dims.end(), 1, std::multiplies()); + + CHECK(a_num_elements >= b_num_elements && a_num_elements % b_num_elements == 0); + if (a) { + CHECK(a_num_elements == a->NumElements()); + } + if (b) { + CHECK(b_num_elements == b->NumElements()); + } + auto dtype = grad_output->Dtype(); + auto device = grad_output->GetDevice(); + + // Currently a and b should have the same data type + if (a && b) { + CHECK(a->Dtype() == b->Dtype()); + } + auto grad_a = std::make_shared(a_dims, dtype, device); + auto grad_b = std::make_shared(b_dims, dtype, device); + grad_a->Fill(0.0f); + grad_b->Fill(0.0f); + switch (dtype) { + case DataType::kFLOAT32: + LaunchBackward<256, float>(fn_a, fn_b, grad_a, grad_b, a_dims, b_dims, grad_output, a, b); + break; + default: + LOG(FATAL) << "CUDA binary backward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; + } + + return {grad_a, grad_b}; +} +} // namespace + +std::shared_ptr NegForward(const std::shared_ptr &input) { + return UnaryForward(input, [] __device__(float x) { return -x; }); +} + +std::shared_ptr NegBackward(const std::shared_ptr &grad_output) { + return UnaryBackward(grad_output, nullptr, [] __device__(float) { return -1.0f; }); +} + +std::shared_ptr ReciprocalForward(const std::shared_ptr &input) { + return UnaryForward(input, [] __device__(float x) { return 1.0f / x; }); +} + +std::shared_ptr ReciprocalBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &input) { + return UnaryBackward(grad_output, input, [] __device__(float x) { return -1.0f / (x * x); }); +} + +std::shared_ptr SinForward(const std::shared_ptr &input) { + return UnaryForward(input, [] __device__(float x) { return sinf(x); }); +} + +std::shared_ptr SinBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input) { + return UnaryBackward(grad_output, input, [] __device__(float x) { return cosf(x); }); +} + +std::shared_ptr CosForward(const std::shared_ptr &input) { + return UnaryForward(input, [] __device__(float x) { return cosf(x); }); +} + +std::shared_ptr CosBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input) { + return UnaryBackward(grad_output, input, [] __device__(float x) { return -sinf(x); }); +} + +std::shared_ptr TanhForward(const std::shared_ptr &input) { + return UnaryForward(input, [] __device__(float x) { return tanhf(x); }); +} + +std::shared_ptr TanhBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &output) { + return UnaryBackward(grad_output, output, [] __device__(float x) { return 1.0 - x * x; }); +} + +std::shared_ptr PowForward(const std::shared_ptr &input, float scalar, bool scalar_is_base) { + if (scalar_is_base) { + return UnaryForward(input, [scalar] __device__(float x) { return powf(scalar, x); }); + } else { + return UnaryForward(input, [scalar] __device__(float x) { return powf(x, scalar); }); + } +} + +std::shared_ptr PowBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, + float scalar, bool scalar_is_base) { + if (scalar_is_base) { + return UnaryBackward(grad_output, input, + [scalar] __device__(float x) { return logf(scalar) * powf(scalar, x); }); + } else { + return UnaryBackward(grad_output, input, + [scalar] __device__(float x) { return scalar * powf(x, scalar - 1.0f); }); + } +} + +std::shared_ptr RsqrtForward(const std::shared_ptr &input) { + return UnaryForward(input, [] __device__(float x) { return 1.0f / sqrtf(x); }); +} + +std::shared_ptr RsqrtBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &input) { + return UnaryBackward(grad_output, input, [] __device__(float x) { return -0.5f / (x * sqrtf(x)); }); +} + +std::shared_ptr EqualsScalarForward(const std::shared_ptr &a, float scalar) { + return UnaryForward(a, [scalar] __device__(float x) { return x == scalar ? 1.0f : 0.0f; }); +} + +std::shared_ptr AddForward(const std::shared_ptr &a, const std::shared_ptr &b) { + return BinaryForward(a, b, [] __device__(float x, float y) { return x + y; }); +} + +std::pair, std::shared_ptr> AddBackward(const std::shared_ptr &grad_output, + const std::vector &a_dims, + const std::vector &b_dims) { + return BinaryBackward( + grad_output, nullptr, nullptr, a_dims, b_dims, [] __device__(float, float) { return 1.f; }, + [] __device__(float, float) { return 1.f; }); +} + +std::shared_ptr AddScalarForward(const std::shared_ptr &a, float scalar) { + return UnaryForward(a, [scalar] __device__(float x) { return x + scalar; }); +} + +std::shared_ptr AddScalarBackward(const std::shared_ptr &grad_output) { + return UnaryBackward(grad_output, nullptr, [] __device__(float) { return 1.0f; }); +} + +std::shared_ptr SubForward(const std::shared_ptr &a, const std::shared_ptr &b) { + return BinaryForward(a, b, [] __device__(float x, float y) { return x - y; }); +} + +std::pair, std::shared_ptr> SubBackward(const std::shared_ptr &grad_output, + const std::vector &a_dims, + const std::vector &b_dims) { + return BinaryBackward( + grad_output, nullptr, nullptr, a_dims, b_dims, [] __device__(float, float) { return 1.f; }, + [] __device__(float, float) { return -1.f; }); +} + +std::shared_ptr MulForward(const std::shared_ptr &a, const std::shared_ptr &b) { + return BinaryForward(a, b, [] __device__(float x, float y) { return x * y; }); +} + +std::pair, std::shared_ptr> MulBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &a, + const std::shared_ptr &b) { + return BinaryBackward( + grad_output, a, b, a->Dims(), b->Dims(), [] __device__(float, float y) { return y; }, + [] __device__(float x, float) { return x; }); +} + +std::shared_ptr MulScalarForward(const std::shared_ptr &a, float scalar) { + return UnaryForward(a, [scalar] __device__(float x) { return x * scalar; }); +} + +std::shared_ptr MulScalarBackward(const std::shared_ptr &grad_output, float scalar) { + return UnaryBackward(grad_output, nullptr, [scalar] __device__(float) { return scalar; }); +} + +std::shared_ptr DivForward(const std::shared_ptr &a, const std::shared_ptr &b) { + return BinaryForward(a, b, [] __device__(float x, float y) { return x / y; }); +} + +std::pair, std::shared_ptr> DivBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &a, + const std::shared_ptr &b) { + return BinaryBackward( + grad_output, a, b, a->Dims(), b->Dims(), [] __device__(float, float y) { return 1 / y; }, + [] __device__(float x, float y) { return -x / (y * y); }); +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_ELEMENTWISE_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_ELEMENTWISE_KERNEL(NegForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(NegBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(ReciprocalForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(ReciprocalBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(SinForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(SinBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(CosForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(CosBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(TanhForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(TanhBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(PowForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(PowBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(RsqrtForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(RsqrtBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(EqualsScalarForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(AddForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(AddBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(AddScalarForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(AddScalarBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(SubForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(SubBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(MulForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(MulBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(MulScalarForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(MulScalarBackward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(DivForward) +REGISTER_CUDA_ELEMENTWISE_KERNEL(DivBackward) + +#undef REGISTER_CUDA_ELEMENTWISE_KERNEL diff --git a/infini_train/src/kernels/cuda/embedding.cu b/infini_train/src/kernels/cuda/embedding.cu index 3ac0a8d..437b960 100644 --- a/infini_train/src/kernels/cuda/embedding.cu +++ b/infini_train/src/kernels/cuda/embedding.cu @@ -1,97 +1,97 @@ -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { - -#define CUDA_CHECK(call) \ - do { \ - cudaError_t status = call; \ - if (status != cudaSuccess) { \ - LOG(FATAL) << "CUDA Error: " << cudaGetErrorString(status) << " at " << __FILE__ << ":" << __LINE__; \ - } \ - } while (0) - -__global__ void EmbeddingForwardKernel(const int64_t *input, float *output, const float *weight, int batch_size, - int max_seqlen, int embed_dim) { - int idx = (blockIdx.x * blockDim.x + threadIdx.x); - if (idx >= batch_size * max_seqlen * embed_dim) { - return; - } - - int bt = idx / embed_dim; - int b = bt / max_seqlen; - int t = bt % max_seqlen; - int c = idx % embed_dim; - - int ix = static_cast(input[b * max_seqlen + t]); - - output[b * max_seqlen * embed_dim + t * embed_dim + c] = weight[ix * embed_dim + c]; -} - -std::shared_ptr EmbeddingForward(const std::shared_ptr &input, const std::shared_ptr &weight) { - CHECK(input->Dtype() == DataType::kINT64); - CHECK_EQ(weight->Dims().size(), 2); - - const int batch_size = input->Dims().size() == 2 ? input->Dims()[0] : 1; - const int max_seqlen = input->Dims().size() == 2 ? input->Dims()[1] : input->Dims()[0]; - const int embed_dim = weight->Dims()[1]; - auto output_dims = input->Dims(); - output_dims.push_back(embed_dim); - - auto output = std::make_shared(output_dims, DataType::kFLOAT32, input->GetDevice()); - int threads_per_block = 256; - int num_blocks = (batch_size * max_seqlen * embed_dim + threads_per_block - 1) / threads_per_block; - EmbeddingForwardKernel<<>>( - static_cast(input->DataPtr()), static_cast(output->DataPtr()), - static_cast(weight->DataPtr()), batch_size, max_seqlen, embed_dim); - return output; -} - -__global__ void EmbeddingBackwardKernel(const int64_t *input_ptr, const float *grad_output_ptr, float *grad_weight_ptr, - int num_tokens, int embedding_dim) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx >= num_tokens) { - return; - } - - int token_id = static_cast(input_ptr[idx]); - if (token_id < 0) { - return; - } - - for (int j = 0; j < embedding_dim; ++j) { - atomicAdd(&grad_weight_ptr[token_id * embedding_dim + j], grad_output_ptr[idx * embedding_dim + j]); - } -} - -std::shared_ptr EmbeddingBackward(const std::shared_ptr &input, const std::vector &weight_dims, - const std::shared_ptr &grad_output) { - CHECK(input->Dtype() == DataType::kINT64); - CHECK_EQ(weight_dims.size(), 2); - const int embedding_dim = weight_dims[1]; - CHECK_EQ(input->Dims().size() + 1, grad_output->Dims().size()); - for (int idx = 0; idx < input->Dims().size(); ++idx) { CHECK_EQ(input->Dims()[idx], grad_output->Dims()[idx]); } - CHECK_EQ(*grad_output->Dims().rbegin(), embedding_dim); - - auto grad_weight = std::make_shared(weight_dims, DataType::kFLOAT32, grad_output->GetDevice()); - grad_weight->Fill(0.0f); - const int num_tokens = input->NumElements(); - const int threads_per_block = 256; - const int num_blocks = (num_tokens + threads_per_block - 1) / threads_per_block; - - EmbeddingBackwardKernel<<>>( - static_cast(input->DataPtr()), static_cast(grad_output->DataPtr()), - static_cast(grad_weight->DataPtr()), num_tokens, embedding_dim); - return grad_weight; -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_EMBEDDING_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_EMBEDDING_KERNEL(EmbeddingForward) -REGISTER_CUDA_EMBEDDING_KERNEL(EmbeddingBackward) - -#undef REGISTER_CUDA_EMBEDDING_KERNEL +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { + +#define CUDA_CHECK(call) \ + do { \ + cudaError_t status = call; \ + if (status != cudaSuccess) { \ + LOG(FATAL) << "CUDA Error: " << cudaGetErrorString(status) << " at " << __FILE__ << ":" << __LINE__; \ + } \ + } while (0) + +__global__ void EmbeddingForwardKernel(const int64_t *input, float *output, const float *weight, int batch_size, + int max_seqlen, int embed_dim) { + int idx = (blockIdx.x * blockDim.x + threadIdx.x); + if (idx >= batch_size * max_seqlen * embed_dim) { + return; + } + + int bt = idx / embed_dim; + int b = bt / max_seqlen; + int t = bt % max_seqlen; + int c = idx % embed_dim; + + int ix = static_cast(input[b * max_seqlen + t]); + + output[b * max_seqlen * embed_dim + t * embed_dim + c] = weight[ix * embed_dim + c]; +} + +std::shared_ptr EmbeddingForward(const std::shared_ptr &input, const std::shared_ptr &weight) { + CHECK(input->Dtype() == DataType::kINT64); + CHECK_EQ(weight->Dims().size(), 2); + + const int batch_size = input->Dims().size() == 2 ? input->Dims()[0] : 1; + const int max_seqlen = input->Dims().size() == 2 ? input->Dims()[1] : input->Dims()[0]; + const int embed_dim = weight->Dims()[1]; + auto output_dims = input->Dims(); + output_dims.push_back(embed_dim); + + auto output = std::make_shared(output_dims, DataType::kFLOAT32, input->GetDevice()); + int threads_per_block = 256; + int num_blocks = (batch_size * max_seqlen * embed_dim + threads_per_block - 1) / threads_per_block; + EmbeddingForwardKernel<<>>( + static_cast(input->DataPtr()), static_cast(output->DataPtr()), + static_cast(weight->DataPtr()), batch_size, max_seqlen, embed_dim); + return output; +} + +__global__ void EmbeddingBackwardKernel(const int64_t *input_ptr, const float *grad_output_ptr, float *grad_weight_ptr, + int num_tokens, int embedding_dim) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= num_tokens) { + return; + } + + int token_id = static_cast(input_ptr[idx]); + if (token_id < 0) { + return; + } + + for (int j = 0; j < embedding_dim; ++j) { + atomicAdd(&grad_weight_ptr[token_id * embedding_dim + j], grad_output_ptr[idx * embedding_dim + j]); + } +} + +std::shared_ptr EmbeddingBackward(const std::shared_ptr &input, const std::vector &weight_dims, + const std::shared_ptr &grad_output) { + CHECK(input->Dtype() == DataType::kINT64); + CHECK_EQ(weight_dims.size(), 2); + const int embedding_dim = weight_dims[1]; + CHECK_EQ(input->Dims().size() + 1, grad_output->Dims().size()); + for (int idx = 0; idx < input->Dims().size(); ++idx) { CHECK_EQ(input->Dims()[idx], grad_output->Dims()[idx]); } + CHECK_EQ(*grad_output->Dims().rbegin(), embedding_dim); + + auto grad_weight = std::make_shared(weight_dims, DataType::kFLOAT32, grad_output->GetDevice()); + grad_weight->Fill(0.0f); + const int num_tokens = input->NumElements(); + const int threads_per_block = 256; + const int num_blocks = (num_tokens + threads_per_block - 1) / threads_per_block; + + EmbeddingBackwardKernel<<>>( + static_cast(input->DataPtr()), static_cast(grad_output->DataPtr()), + static_cast(grad_weight->DataPtr()), num_tokens, embedding_dim); + return grad_weight; +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_EMBEDDING_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_EMBEDDING_KERNEL(EmbeddingForward) +REGISTER_CUDA_EMBEDDING_KERNEL(EmbeddingBackward) + +#undef REGISTER_CUDA_EMBEDDING_KERNEL diff --git a/infini_train/src/kernels/cuda/fill.cu b/infini_train/src/kernels/cuda/fill.cu index 470e10c..a302d54 100644 --- a/infini_train/src/kernels/cuda/fill.cu +++ b/infini_train/src/kernels/cuda/fill.cu @@ -1,23 +1,23 @@ -#include "glog/logging.h" -#include -#include -#include -#include - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { -void Fill(std::shared_ptr tensor, void *value_ptr) { - // FIXME(zbl): support other data types - thrust::device_ptr dev_ptr(reinterpret_cast(tensor->DataPtr())); - thrust::fill(thrust::cuda::par.on(0), dev_ptr, dev_ptr + tensor->NumElements(), *(static_cast(value_ptr))); -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_FILL_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_FILL_KERNEL(Fill) - -#undef REGISTER_CUDA_FILL_KERNEL +#include "glog/logging.h" +#include +#include +#include +#include + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { +void Fill(std::shared_ptr tensor, void *value_ptr) { + // FIXME(zbl): support other data types + thrust::device_ptr dev_ptr(reinterpret_cast(tensor->DataPtr())); + thrust::fill(thrust::cuda::par.on(0), dev_ptr, dev_ptr + tensor->NumElements(), *(static_cast(value_ptr))); +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_FILL_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_FILL_KERNEL(Fill) + +#undef REGISTER_CUDA_FILL_KERNEL diff --git a/infini_train/src/kernels/cuda/layernorm.cu b/infini_train/src/kernels/cuda/layernorm.cu index 4b6dd0a..7b94f63 100644 --- a/infini_train/src/kernels/cuda/layernorm.cu +++ b/infini_train/src/kernels/cuda/layernorm.cu @@ -1,174 +1,174 @@ -#include "glog/logging.h" -#include - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { - -template -__global__ void LayerNormForwardKernel(const float *input, const float *weight, const float *bias, float *mean_out, - float *rstd_out, float *output, float eps, int embed_dim) { - using BlockReduce = cub::BlockReduce; - __shared__ typename BlockReduce::TempStorage temp_storage_mean; - __shared__ typename BlockReduce::TempStorage temp_storage_rstd; - __shared__ float shared_mean; - __shared__ float shared_rstd; - - const int token_idx = blockIdx.x; - const float *x = input + token_idx * embed_dim; - float *y = output + token_idx * embed_dim; - - float sum = 0.0f; - float sqsum = 0.0f; - for (int i = threadIdx.x; i < embed_dim; i += BLOCK_SIZE) { - float val = x[i]; - sum += val; - sqsum += val * val; - } - - float total_sum = BlockReduce(temp_storage_mean).Sum(sum); - __syncthreads(); - - float total_sqsum = BlockReduce(temp_storage_rstd).Sum(sqsum); - __syncthreads(); - - if (threadIdx.x == 0) { - float mean = total_sum / embed_dim; - float var = total_sqsum / embed_dim - mean * mean; - float rstd = rsqrtf(var + eps); - shared_mean = mean; - shared_rstd = rstd; - if (mean_out) { - mean_out[token_idx] = mean; - } - if (rstd_out) { - rstd_out[token_idx] = rstd; - } - } - __syncthreads(); - - for (int i = threadIdx.x; i < embed_dim; i += BLOCK_SIZE) { - float norm = (x[i] - shared_mean) * shared_rstd; - y[i] = norm * weight[i] + bias[i]; - } -} - -std::tuple, std::shared_ptr, std::shared_ptr> -LayerNormForward(const std::shared_ptr &input, const std::shared_ptr &weight, - const std::shared_ptr &bias, const float eps) { - CHECK_EQ(input->Dims().size(), 3); - CHECK_LE(input->Dims()[2], weight->Dims()[0]); - CHECK_LE(input->Dims()[2], bias->Dims()[0]); - - const int batch_size = input->Dims()[0]; - const int max_seqlen = input->Dims()[1]; - const int embed_dim = input->Dims()[2]; - - auto output = std::make_shared(input->Dims(), DataType::kFLOAT32, input->GetDevice()); - auto mean = std::make_shared(std::vector{batch_size, max_seqlen}, DataType::kFLOAT32, - input->GetDevice()); - auto rstd = std::make_shared(std::vector{batch_size, max_seqlen}, DataType::kFLOAT32, - input->GetDevice()); - mean->Fill(0.0f); - rstd->Fill(0.0f); - - constexpr int BLOCK_SIZE = 256; - int threads_per_block = BLOCK_SIZE; - int num_blocks = batch_size * max_seqlen; - - LayerNormForwardKernel<<>>( - static_cast(input->DataPtr()), static_cast(weight->DataPtr()), - static_cast(bias->DataPtr()), static_cast(mean->DataPtr()), - static_cast(rstd->DataPtr()), static_cast(output->DataPtr()), eps, embed_dim); - return {output, mean, rstd}; -} - -template -__global__ void LayerNormBackwardKernel(const float *__restrict__ input, const float *__restrict__ grad_output, - const float *__restrict__ mean, const float *__restrict__ rstd, - const float *__restrict__ weight, float *__restrict__ grad_input, - float *__restrict__ grad_weight, float *__restrict__ grad_bias, int embed_dim) { - using BlockReduce = cub::BlockReduce; - __shared__ typename BlockReduce::TempStorage temp_storage_mean; - __shared__ typename BlockReduce::TempStorage temp_storage_norm; - __shared__ float shared_mean; - __shared__ float shared_norm; - - int tid = threadIdx.x; - int token_idx = blockIdx.x; - - const float *input_ptr = input + token_idx * embed_dim; - const float *grad_output_ptr = grad_output + token_idx * embed_dim; - float *grad_input_ptr = grad_input + token_idx * embed_dim; - - float mean_val = mean[token_idx]; - float rstd_val = rstd[token_idx]; - - float dnorm_mean = 0.f; - float dnorm_norm_mean = 0.f; - - for (int i = tid; i < embed_dim; i += BLOCK_SIZE) { - float dnorm = weight[i] * grad_output_ptr[i]; - dnorm_mean += dnorm; - dnorm_norm_mean += dnorm * (input_ptr[i] - mean_val); - } - - dnorm_mean = BlockReduce(temp_storage_mean).Sum(dnorm_mean); - __syncthreads(); - dnorm_norm_mean = BlockReduce(temp_storage_norm).Sum(dnorm_norm_mean); - __syncthreads(); - - if (tid == 0) { - float mean_d = dnorm_mean / embed_dim; - float norm_d = (dnorm_norm_mean / embed_dim) * rstd_val - mean_d * mean_val * rstd_val; - shared_mean = mean_d; - shared_norm = norm_d; - } - __syncthreads(); - - for (int i = tid; i < embed_dim; i += BLOCK_SIZE) { - float norm = (input_ptr[i] - mean_val) * rstd_val; - - grad_input_ptr[i] = (weight[i] * grad_output_ptr[i] - shared_mean - norm * shared_norm) * rstd_val; - - atomicAdd(&grad_weight[i], grad_output_ptr[i] * norm); - atomicAdd(&grad_bias[i], grad_output_ptr[i]); - } -} - -std::tuple, std::shared_ptr, std::shared_ptr> -LayerNormBackward(const std::shared_ptr &input, const std::shared_ptr &weight, - const std::shared_ptr &bias, const std::shared_ptr &mean, - const std::shared_ptr &rstd, const std::shared_ptr &grad_output) { - const int batch_size = input->Dims()[0]; - const int max_seqlen = input->Dims()[1]; - const int embed_dim = input->Dims()[2]; - - auto grad_input = std::make_shared(input->Dims(), DataType::kFLOAT32, grad_output->GetDevice()); - auto grad_weight = std::make_shared(weight->Dims(), DataType::kFLOAT32, grad_output->GetDevice()); - auto grad_bias = std::make_shared(bias->Dims(), DataType::kFLOAT32, grad_output->GetDevice()); - grad_input->Fill(0.0f); - grad_weight->Fill(0.0f); - grad_bias->Fill(0.0f); - - constexpr int BLOCK_SIZE = 256; - int threads_per_block = BLOCK_SIZE; - int num_blocks = batch_size * max_seqlen; - - LayerNormBackwardKernel<<>>( - static_cast(input->DataPtr()), static_cast(grad_output->DataPtr()), - static_cast(mean->DataPtr()), static_cast(rstd->DataPtr()), - static_cast(weight->DataPtr()), static_cast(grad_input->DataPtr()), - static_cast(grad_weight->DataPtr()), static_cast(grad_bias->DataPtr()), embed_dim); - return {grad_input, grad_weight, grad_bias}; -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_LAYERNORM_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_LAYERNORM_KERNEL(LayerNormForward) -REGISTER_CUDA_LAYERNORM_KERNEL(LayerNormBackward) - -#undef REGISTER_CUDA_LAYERNORM_KERNEL +#include "glog/logging.h" +#include + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { + +template +__global__ void LayerNormForwardKernel(const float *input, const float *weight, const float *bias, float *mean_out, + float *rstd_out, float *output, float eps, int embed_dim) { + using BlockReduce = cub::BlockReduce; + __shared__ typename BlockReduce::TempStorage temp_storage_mean; + __shared__ typename BlockReduce::TempStorage temp_storage_rstd; + __shared__ float shared_mean; + __shared__ float shared_rstd; + + const int token_idx = blockIdx.x; + const float *x = input + token_idx * embed_dim; + float *y = output + token_idx * embed_dim; + + float sum = 0.0f; + float sqsum = 0.0f; + for (int i = threadIdx.x; i < embed_dim; i += BLOCK_SIZE) { + float val = x[i]; + sum += val; + sqsum += val * val; + } + + float total_sum = BlockReduce(temp_storage_mean).Sum(sum); + __syncthreads(); + + float total_sqsum = BlockReduce(temp_storage_rstd).Sum(sqsum); + __syncthreads(); + + if (threadIdx.x == 0) { + float mean = total_sum / embed_dim; + float var = total_sqsum / embed_dim - mean * mean; + float rstd = rsqrtf(var + eps); + shared_mean = mean; + shared_rstd = rstd; + if (mean_out) { + mean_out[token_idx] = mean; + } + if (rstd_out) { + rstd_out[token_idx] = rstd; + } + } + __syncthreads(); + + for (int i = threadIdx.x; i < embed_dim; i += BLOCK_SIZE) { + float norm = (x[i] - shared_mean) * shared_rstd; + y[i] = norm * weight[i] + bias[i]; + } +} + +std::tuple, std::shared_ptr, std::shared_ptr> +LayerNormForward(const std::shared_ptr &input, const std::shared_ptr &weight, + const std::shared_ptr &bias, const float eps) { + CHECK_EQ(input->Dims().size(), 3); + CHECK_LE(input->Dims()[2], weight->Dims()[0]); + CHECK_LE(input->Dims()[2], bias->Dims()[0]); + + const int batch_size = input->Dims()[0]; + const int max_seqlen = input->Dims()[1]; + const int embed_dim = input->Dims()[2]; + + auto output = std::make_shared(input->Dims(), DataType::kFLOAT32, input->GetDevice()); + auto mean = std::make_shared(std::vector{batch_size, max_seqlen}, DataType::kFLOAT32, + input->GetDevice()); + auto rstd = std::make_shared(std::vector{batch_size, max_seqlen}, DataType::kFLOAT32, + input->GetDevice()); + mean->Fill(0.0f); + rstd->Fill(0.0f); + + constexpr int BLOCK_SIZE = 256; + int threads_per_block = BLOCK_SIZE; + int num_blocks = batch_size * max_seqlen; + + LayerNormForwardKernel<<>>( + static_cast(input->DataPtr()), static_cast(weight->DataPtr()), + static_cast(bias->DataPtr()), static_cast(mean->DataPtr()), + static_cast(rstd->DataPtr()), static_cast(output->DataPtr()), eps, embed_dim); + return {output, mean, rstd}; +} + +template +__global__ void LayerNormBackwardKernel(const float *__restrict__ input, const float *__restrict__ grad_output, + const float *__restrict__ mean, const float *__restrict__ rstd, + const float *__restrict__ weight, float *__restrict__ grad_input, + float *__restrict__ grad_weight, float *__restrict__ grad_bias, int embed_dim) { + using BlockReduce = cub::BlockReduce; + __shared__ typename BlockReduce::TempStorage temp_storage_mean; + __shared__ typename BlockReduce::TempStorage temp_storage_norm; + __shared__ float shared_mean; + __shared__ float shared_norm; + + int tid = threadIdx.x; + int token_idx = blockIdx.x; + + const float *input_ptr = input + token_idx * embed_dim; + const float *grad_output_ptr = grad_output + token_idx * embed_dim; + float *grad_input_ptr = grad_input + token_idx * embed_dim; + + float mean_val = mean[token_idx]; + float rstd_val = rstd[token_idx]; + + float dnorm_mean = 0.f; + float dnorm_norm_mean = 0.f; + + for (int i = tid; i < embed_dim; i += BLOCK_SIZE) { + float dnorm = weight[i] * grad_output_ptr[i]; + dnorm_mean += dnorm; + dnorm_norm_mean += dnorm * (input_ptr[i] - mean_val); + } + + dnorm_mean = BlockReduce(temp_storage_mean).Sum(dnorm_mean); + __syncthreads(); + dnorm_norm_mean = BlockReduce(temp_storage_norm).Sum(dnorm_norm_mean); + __syncthreads(); + + if (tid == 0) { + float mean_d = dnorm_mean / embed_dim; + float norm_d = (dnorm_norm_mean / embed_dim) * rstd_val - mean_d * mean_val * rstd_val; + shared_mean = mean_d; + shared_norm = norm_d; + } + __syncthreads(); + + for (int i = tid; i < embed_dim; i += BLOCK_SIZE) { + float norm = (input_ptr[i] - mean_val) * rstd_val; + + grad_input_ptr[i] = (weight[i] * grad_output_ptr[i] - shared_mean - norm * shared_norm) * rstd_val; + + atomicAdd(&grad_weight[i], grad_output_ptr[i] * norm); + atomicAdd(&grad_bias[i], grad_output_ptr[i]); + } +} + +std::tuple, std::shared_ptr, std::shared_ptr> +LayerNormBackward(const std::shared_ptr &input, const std::shared_ptr &weight, + const std::shared_ptr &bias, const std::shared_ptr &mean, + const std::shared_ptr &rstd, const std::shared_ptr &grad_output) { + const int batch_size = input->Dims()[0]; + const int max_seqlen = input->Dims()[1]; + const int embed_dim = input->Dims()[2]; + + auto grad_input = std::make_shared(input->Dims(), DataType::kFLOAT32, grad_output->GetDevice()); + auto grad_weight = std::make_shared(weight->Dims(), DataType::kFLOAT32, grad_output->GetDevice()); + auto grad_bias = std::make_shared(bias->Dims(), DataType::kFLOAT32, grad_output->GetDevice()); + grad_input->Fill(0.0f); + grad_weight->Fill(0.0f); + grad_bias->Fill(0.0f); + + constexpr int BLOCK_SIZE = 256; + int threads_per_block = BLOCK_SIZE; + int num_blocks = batch_size * max_seqlen; + + LayerNormBackwardKernel<<>>( + static_cast(input->DataPtr()), static_cast(grad_output->DataPtr()), + static_cast(mean->DataPtr()), static_cast(rstd->DataPtr()), + static_cast(weight->DataPtr()), static_cast(grad_input->DataPtr()), + static_cast(grad_weight->DataPtr()), static_cast(grad_bias->DataPtr()), embed_dim); + return {grad_input, grad_weight, grad_bias}; +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_LAYERNORM_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_LAYERNORM_KERNEL(LayerNormForward) +REGISTER_CUDA_LAYERNORM_KERNEL(LayerNormBackward) + +#undef REGISTER_CUDA_LAYERNORM_KERNEL diff --git a/infini_train/src/kernels/cuda/linear.cu b/infini_train/src/kernels/cuda/linear.cu index efaaaa6..bed5108 100644 --- a/infini_train/src/kernels/cuda/linear.cu +++ b/infini_train/src/kernels/cuda/linear.cu @@ -1,243 +1,357 @@ -#include "cublas_v2.h" -#include "glog/logging.h" -#include - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { - -#define CUDA_CHECK(call) \ - do { \ - cudaError_t status = call; \ - if (status != cudaSuccess) { \ - LOG(FATAL) << "CUDA Error: " << cudaGetErrorString(status) << " at " << __FILE__ << ":" << __LINE__; \ - } \ - } while (0) - -#define CUBLAS_CHECK(call) \ - do { \ - cublasStatus_t status = call; \ - if (status != CUBLAS_STATUS_SUCCESS) { \ - LOG(FATAL) << "CUBLAS Error: " << cublasGetStatusString(status) << " at " << __FILE__ << ":" << __LINE__; \ - } \ - } while (0) - -std::shared_ptr MatmulForward(const std::shared_ptr &input, const std::shared_ptr &other) { - // =================================== 作业 =================================== - // TODO:实现CUDA上的矩阵乘法前向计算 - // REF: - // =================================== 作业 =================================== - - auto output = std::make_shared(); - return output; -} - -std::tuple, std::shared_ptr> -MatmulBackward(const std::shared_ptr &input, const std::shared_ptr &other, - const std::shared_ptr &grad_output) { - // =================================== 作业 =================================== - // TODO:实现CUDA上的矩阵乘法反向传播 - // REF: - // =================================== 作业 =================================== - - auto grad_input = std::make_shared(); - auto grad_other = std::make_shared(); - return {grad_input, grad_other}; -} - -__global__ void BiasCopyKernel(float *output, const float *bias, int bs, int out_features) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx >= bs * out_features) { - return; - } - int j = idx % out_features; - output[idx] = bias[j]; -} - -std::shared_ptr LinearForward(const std::shared_ptr &input, const std::shared_ptr &weight, - bool transpose, const std::shared_ptr &bias) { - - /* - !transpose: output = input * weight + bias - output[*, out_features] = input[*, in_features] * weight[in_features, out_features] + bias[out_features] - - transpose: output = input * weight^T + bias - output[*, out_features] = input[*, in_features] * weight[out_features, in_features]^T + bias[out_features] - */ - - const auto &input_dims = input->Dims(); - CHECK_GE(input_dims.size(), 2); - const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); - const int64_t in_features = *input_dims.rbegin(); - - const auto &weight_dims = weight->Dims(); - CHECK_EQ(weight_dims.size(), 2); - CHECK_EQ(in_features, weight_dims[transpose ? 1 : 0]); - - // As for cublas: - // C = alpha * op(B) * op(A) + beta * C - // Dimensions: - // input: (bs, in_features) - // weight: (in_features, out_features) or (out_features, in_features) if transposed - // output: (bs, out_features) - const int64_t out_features = weight_dims[transpose ? 0 : 1]; - - auto output_dims = input_dims; - *output_dims.rbegin() = out_features; - auto output = std::make_shared(output_dims, DataType::kFLOAT32, input->GetDevice()); - - if (bias) { - CHECK_EQ(bias->Dims().size(), 1); - CHECK_EQ(bias->Dims()[0], out_features); - int threads_per_block = 256; - int num_blocks = (bs * out_features + threads_per_block - 1) / threads_per_block; - BiasCopyKernel<<>>( - static_cast(output->DataPtr()), static_cast(bias->DataPtr()), bs, out_features); - } else { - output->Fill(0.0f); - } - - const float alpha = 1.0f; - const float beta = 1.0f; - cublasHandle_t handle; - CUBLAS_CHECK(cublasCreate(&handle)); - if (transpose) { - // weight is [out_features, in_features] here - - // output = input * weight.T --> output.T = weight * input.T - // C = output.T[out_features, bs] - // A = weight.T[in_features, out_features] - // B = input.T[in_features, bs] - CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_T, CUBLAS_OP_N, out_features, bs, in_features, &alpha, - static_cast(weight->DataPtr()), in_features, - static_cast(input->DataPtr()), in_features, &beta, - static_cast(output->DataPtr()), out_features)); - } else { - // output = input * weight --> output.T = weight.T * input.T - // C = output.T[out_features, bs] - // A = weight.T[out_features, in_features] - // B = input.T[in_features, bs] - CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, out_features, bs, in_features, &alpha, - static_cast(weight->DataPtr()), out_features, - static_cast(input->DataPtr()), in_features, &beta, - static_cast(output->DataPtr()), out_features)); - } - CUBLAS_CHECK(cublasDestroy(handle)); - return output; -} - -template -__global__ void ReduceColumnsKernel(const float *__restrict__ input, float *__restrict__ output, int num_rows, - int num_cols) { - using BlockReduce = cub::BlockReduce; - __shared__ typename BlockReduce::TempStorage temp_storage; - - int row = blockIdx.x; - float sum = 0.0f; - - for (int col = threadIdx.x; col < num_cols; col += blockDim.x) { sum += input[row * num_cols + col]; } - - float reduced = BlockReduce(temp_storage).Sum(sum); - - if (threadIdx.x == 0) { - output[row] = reduced; - } -} - -std::tuple, std::shared_ptr, std::shared_ptr> -LinearBackward(const std::shared_ptr &input, const std::shared_ptr &weight, bool transpose, - int64_t out_features, const std::shared_ptr &grad_output, const bool bias) { - const auto &input_dims = input->Dims(); - CHECK_GE(input_dims.size(), 2); - const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); - const int64_t in_features = *input_dims.rbegin(); - - const auto &weight_dims = weight->Dims(); - CHECK_EQ(weight_dims.size(), 2); - CHECK_EQ(in_features, weight_dims[transpose ? 1 : 0]); - CHECK_EQ(out_features, weight_dims[transpose ? 0 : 1]); - - auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32, grad_output->GetDevice()); - auto grad_weight = std::make_shared(weight_dims, DataType::kFLOAT32, grad_output->GetDevice()); - grad_input->Fill(0.0f); - grad_weight->Fill(0.0f); - std::shared_ptr grad_bias = nullptr; - if (bias) { - grad_bias = std::make_shared(std::vector{out_features}, DataType::kFLOAT32, - grad_output->GetDevice()); - grad_bias->Fill(0.0f); - } - - float alpha = 1.0f; - float beta = 0.0f; - cublasHandle_t handle; - CUBLAS_CHECK(cublasCreate(&handle)); - - if (transpose) { - // weight is [out_features, in_features] here - - // d_input = d_output * weight --> d_input.T = weight.T * d_output.T - // C = d_input.T[in_features, bs] - // A = weight.T[in_features, out_features] - // B = d_output.T[out_features, bs] - CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, in_features, bs, out_features, &alpha, - static_cast(weight->DataPtr()), in_features, - static_cast(grad_output->DataPtr()), out_features, &beta, - static_cast(grad_input->DataPtr()), in_features)); - - // d_weight = d_output.T * input --> d_weight.T = input.T * d_output - // C = d_weight.T[in_features, out_features] - // A = input.T[in_features, bs] - // B = d_output.T[out_features, bs] - CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_T, in_features, out_features, bs, &alpha, - static_cast(input->DataPtr()), in_features, - static_cast(grad_output->DataPtr()), out_features, &beta, - static_cast(grad_weight->DataPtr()), in_features)); - } else { - // weight is [in_features, out_features] here - - // d_input = d_output * weight.T --> d_input.T = weight * d_output.T - // C = d_input.T[in_features, bs] - // A = weight.T[out_features, in_features] - // B = d_output.T[out_features, bs] - CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_T, CUBLAS_OP_N, in_features, bs, out_features, &alpha, - static_cast(weight->DataPtr()), out_features, - static_cast(grad_output->DataPtr()), out_features, &beta, - static_cast(grad_input->DataPtr()), in_features)); - - // d_weight = input.T * d_output --> d_weight.T = d_output.T * input - // C = d_weight.T[out_features, in_features] - // A = d_output.T[out_features, bs] - // B = input.T[in_features, bs] - CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_T, out_features, in_features, bs, &alpha, - static_cast(grad_output->DataPtr()), out_features, - static_cast(input->DataPtr()), in_features, &beta, - static_cast(grad_weight->DataPtr()), out_features)); - } - - // d_bias = \sum_i(i=0, bs-1) d_output[i] - if (bias) { - constexpr int BLOCK_SIZE = 256; - int threads_per_block = BLOCK_SIZE; - int num_blocks = out_features; - ReduceColumnsKernel - <<>>(static_cast(grad_output->DataPtr()), - static_cast(grad_bias->DataPtr()), out_features, bs); - } - - CUBLAS_CHECK(cublasDestroy(handle)); - - return {grad_input, grad_weight, grad_bias}; -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_LINEAR_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_LINEAR_KERNEL(MatmulForward) -REGISTER_CUDA_LINEAR_KERNEL(MatmulBackward) -REGISTER_CUDA_LINEAR_KERNEL(LinearForward) -REGISTER_CUDA_LINEAR_KERNEL(LinearBackward) - -#undef REGISTER_CUDA_LINEAR_KERNEL +#include "cublas_v2.h" +#include "glog/logging.h" +#include + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { + +#define CUDA_CHECK(call) \ + do { \ + cudaError_t status = call; \ + if (status != cudaSuccess) { \ + LOG(FATAL) << "CUDA Error: " << cudaGetErrorString(status) << " at " << __FILE__ << ":" << __LINE__; \ + } \ + } while (0) + +#define CUBLAS_CHECK(call) \ + do { \ + cublasStatus_t status = call; \ + if (status != CUBLAS_STATUS_SUCCESS) { \ + LOG(FATAL) << "CUBLAS Error: " << cublasGetStatusString(status) << " at " << __FILE__ << ":" << __LINE__; \ + } \ + } while (0) + +std::shared_ptr MatmulForward(const std::shared_ptr &input, const std::shared_ptr &other) { + // =================================== 作业 =================================== + // TODO:实现CUDA上的矩阵乘法前向计算 + // REF: + // =================================== 作业 =================================== + + const auto &input_dims = input->Dims(); + const auto &other_dims = other->Dims(); + CHECK_GE(input_dims.size(), 2); + CHECK_GE(other_dims.size(), 2); + const int64_t M = input_dims[input_dims.size() - 2]; + const int64_t K = input_dims[input_dims.size() - 1]; + const int64_t N = other_dims[other_dims.size() - 1]; + CHECK_EQ(K, other_dims[other_dims.size() - 2]); + + int64_t batch_input = 1; + for (size_t i = 0; i < input_dims.size() - 2; ++i) { + batch_input *= input_dims[i]; + } + int64_t batch_other = 1; + for (size_t i = 0; i < other_dims.size() - 2; ++i) { + batch_other *= other_dims[i]; + } + int64_t batch = std::max(batch_input, batch_other); + + std::vector output_dims; + if (batch > 1) { + output_dims = (input_dims.size() >= other_dims.size()) ? input_dims : other_dims; + output_dims[output_dims.size() - 2] = M; + output_dims[output_dims.size() - 1] = N; + } else { + output_dims = {M, N}; + } + + auto output = std::make_shared(output_dims, DataType::kFLOAT32, input->GetDevice()); + output->Fill(0.0f); + + const float alpha = 1.0f; + const float beta = 0.0f; + cublasHandle_t handle; + CUBLAS_CHECK(cublasCreate(&handle)); + + if (batch <= 1) { + // C = output^T[N, M] = other^T[N, K] * input^T[K, M] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, N, M, K, &alpha, + static_cast(other->DataPtr()), N, + static_cast(input->DataPtr()), K, &beta, + static_cast(output->DataPtr()), N)); + } else { + // Use strided batched GEMM + int64_t strideA = (batch_other == 1) ? 0 : K * N; + int64_t strideB = (batch_input == 1) ? 0 : M * K; + int64_t strideC = M * N; + CUBLAS_CHECK(cublasSgemmStridedBatched(handle, CUBLAS_OP_N, CUBLAS_OP_N, N, M, K, &alpha, + static_cast(other->DataPtr()), N, strideA, + static_cast(input->DataPtr()), K, strideB, &beta, + static_cast(output->DataPtr()), N, strideC, batch)); + } + + CUBLAS_CHECK(cublasDestroy(handle)); + return output; +} + +std::tuple, std::shared_ptr> +MatmulBackward(const std::shared_ptr &input, const std::shared_ptr &other, + const std::shared_ptr &grad_output) { + // =================================== 作业 =================================== + // TODO:实现CUDA上的矩阵乘法反向传播 + // REF: + // =================================== 作业 =================================== + + const auto &input_dims = input->Dims(); + const auto &other_dims = other->Dims(); + const int64_t M = input_dims[input_dims.size() - 2]; + const int64_t K = input_dims[input_dims.size() - 1]; + const int64_t N = other_dims[other_dims.size() - 1]; + + int64_t batch_input = 1; + for (size_t i = 0; i < input_dims.size() - 2; ++i) { + batch_input *= input_dims[i]; + } + int64_t batch_other = 1; + for (size_t i = 0; i < other_dims.size() - 2; ++i) { + batch_other *= other_dims[i]; + } + int64_t batch = std::max(batch_input, batch_other); + + auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32, grad_output->GetDevice()); + auto grad_other = std::make_shared(other_dims, DataType::kFLOAT32, grad_output->GetDevice()); + grad_input->Fill(0.0f); + grad_other->Fill(0.0f); + + const float alpha = 1.0f; + const float beta = 0.0f; + cublasHandle_t handle; + CUBLAS_CHECK(cublasCreate(&handle)); + + if (batch <= 1) { + // grad_input = grad_output * other^T + // grad_input^T[K, M] = other[K, N] * grad_output^T[N, M] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_T, CUBLAS_OP_N, K, M, N, &alpha, + static_cast(other->DataPtr()), N, + static_cast(grad_output->DataPtr()), N, &beta, + static_cast(grad_input->DataPtr()), K)); + + // grad_other = input^T * grad_output + // grad_other^T[N, K] = grad_output^T[N, M] * input[M, K] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_T, N, K, M, &alpha, + static_cast(grad_output->DataPtr()), N, + static_cast(input->DataPtr()), K, &beta, + static_cast(grad_other->DataPtr()), N)); + } else { + // Strided batched for grad_input + int64_t strideA_gi = (batch_other == 1) ? 0 : K * N; + int64_t strideB_gi = M * N; + int64_t strideC_gi = (batch_input == 1) ? 0 : M * K; + CUBLAS_CHECK(cublasSgemmStridedBatched(handle, CUBLAS_OP_T, CUBLAS_OP_N, K, M, N, &alpha, + static_cast(other->DataPtr()), N, strideA_gi, + static_cast(grad_output->DataPtr()), N, strideB_gi, &beta, + static_cast(grad_input->DataPtr()), K, strideC_gi, batch)); + + // Strided batched for grad_other + int64_t strideA_go = M * N; + int64_t strideB_go = (batch_input == 1) ? 0 : M * K; + int64_t strideC_go = (batch_other == 1) ? 0 : K * N; + CUBLAS_CHECK(cublasSgemmStridedBatched(handle, CUBLAS_OP_N, CUBLAS_OP_T, N, K, M, &alpha, + static_cast(grad_output->DataPtr()), N, strideA_go, + static_cast(input->DataPtr()), K, strideB_go, &beta, + static_cast(grad_other->DataPtr()), N, strideC_go, batch)); + } + + CUBLAS_CHECK(cublasDestroy(handle)); + return {grad_input, grad_other}; +} + +__global__ void BiasCopyKernel(float *output, const float *bias, int bs, int out_features) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= bs * out_features) { + return; + } + int j = idx % out_features; + output[idx] = bias[j]; +} + +std::shared_ptr LinearForward(const std::shared_ptr &input, const std::shared_ptr &weight, + bool transpose, const std::shared_ptr &bias) { + + /* + !transpose: output = input * weight + bias + output[*, out_features] = input[*, in_features] * weight[in_features, out_features] + bias[out_features] + + transpose: output = input * weight^T + bias + output[*, out_features] = input[*, in_features] * weight[out_features, in_features]^T + bias[out_features] + */ + + const auto &input_dims = input->Dims(); + CHECK_GE(input_dims.size(), 2); + const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); + const int64_t in_features = *input_dims.rbegin(); + + const auto &weight_dims = weight->Dims(); + CHECK_EQ(weight_dims.size(), 2); + CHECK_EQ(in_features, weight_dims[transpose ? 1 : 0]); + + // As for cublas: + // C = alpha * op(B) * op(A) + beta * C + // Dimensions: + // input: (bs, in_features) + // weight: (in_features, out_features) or (out_features, in_features) if transposed + // output: (bs, out_features) + const int64_t out_features = weight_dims[transpose ? 0 : 1]; + + auto output_dims = input_dims; + *output_dims.rbegin() = out_features; + auto output = std::make_shared(output_dims, DataType::kFLOAT32, input->GetDevice()); + + if (bias) { + CHECK_EQ(bias->Dims().size(), 1); + CHECK_EQ(bias->Dims()[0], out_features); + int threads_per_block = 256; + int num_blocks = (bs * out_features + threads_per_block - 1) / threads_per_block; + BiasCopyKernel<<>>( + static_cast(output->DataPtr()), static_cast(bias->DataPtr()), bs, out_features); + CUDA_CHECK(cudaGetLastError()); + CUDA_CHECK(cudaDeviceSynchronize()); + } else { + output->Fill(0.0f); + } + + const float alpha = 1.0f; + const float beta = 1.0f; + cublasHandle_t handle; + CUBLAS_CHECK(cublasCreate(&handle)); + if (transpose) { + // weight is [out_features, in_features] here + + // output = input * weight.T --> output.T = weight * input.T + // C = output.T[out_features, bs] + // A = weight.T[in_features, out_features] + // B = input.T[in_features, bs] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_T, CUBLAS_OP_N, out_features, bs, in_features, &alpha, + static_cast(weight->DataPtr()), in_features, + static_cast(input->DataPtr()), in_features, &beta, + static_cast(output->DataPtr()), out_features)); + } else { + // output = input * weight --> output.T = weight.T * input.T + // C = output.T[out_features, bs] + // A = weight.T[out_features, in_features] + // B = input.T[in_features, bs] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, out_features, bs, in_features, &alpha, + static_cast(weight->DataPtr()), out_features, + static_cast(input->DataPtr()), in_features, &beta, + static_cast(output->DataPtr()), out_features)); + } + CUBLAS_CHECK(cublasDestroy(handle)); + return output; +} + +template +__global__ void ReduceColumnsKernel(const float *__restrict__ input, float *__restrict__ output, int num_rows, + int num_cols) { + using BlockReduce = cub::BlockReduce; + __shared__ typename BlockReduce::TempStorage temp_storage; + + int row = blockIdx.x; + float sum = 0.0f; + + for (int col = threadIdx.x; col < num_cols; col += blockDim.x) { sum += input[row * num_cols + col]; } + + float reduced = BlockReduce(temp_storage).Sum(sum); + + if (threadIdx.x == 0) { + output[row] = reduced; + } +} + +std::tuple, std::shared_ptr, std::shared_ptr> +LinearBackward(const std::shared_ptr &input, const std::shared_ptr &weight, bool transpose, + int64_t out_features, const std::shared_ptr &grad_output, const bool bias) { + const auto &input_dims = input->Dims(); + CHECK_GE(input_dims.size(), 2); + const int64_t bs = std::accumulate(input_dims.rbegin() + 1, input_dims.rend(), 1, std::multiplies{}); + const int64_t in_features = *input_dims.rbegin(); + + const auto &weight_dims = weight->Dims(); + CHECK_EQ(weight_dims.size(), 2); + CHECK_EQ(in_features, weight_dims[transpose ? 1 : 0]); + CHECK_EQ(out_features, weight_dims[transpose ? 0 : 1]); + + auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32, grad_output->GetDevice()); + auto grad_weight = std::make_shared(weight_dims, DataType::kFLOAT32, grad_output->GetDevice()); + grad_input->Fill(0.0f); + grad_weight->Fill(0.0f); + std::shared_ptr grad_bias = nullptr; + if (bias) { + grad_bias = std::make_shared(std::vector{out_features}, DataType::kFLOAT32, + grad_output->GetDevice()); + grad_bias->Fill(0.0f); + } + + float alpha = 1.0f; + float beta = 0.0f; + cublasHandle_t handle; + CUBLAS_CHECK(cublasCreate(&handle)); + + if (transpose) { + // weight is [out_features, in_features] here + + // d_input = d_output * weight --> d_input.T = weight.T * d_output.T + // C = d_input.T[in_features, bs] + // A = weight.T[in_features, out_features] + // B = d_output.T[out_features, bs] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N, in_features, bs, out_features, &alpha, + static_cast(weight->DataPtr()), in_features, + static_cast(grad_output->DataPtr()), out_features, &beta, + static_cast(grad_input->DataPtr()), in_features)); + + // d_weight = d_output.T * input --> d_weight.T = input.T * d_output + // C = d_weight.T[in_features, out_features] + // A = input.T[in_features, bs] + // B = d_output.T[out_features, bs] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_T, in_features, out_features, bs, &alpha, + static_cast(input->DataPtr()), in_features, + static_cast(grad_output->DataPtr()), out_features, &beta, + static_cast(grad_weight->DataPtr()), in_features)); + } else { + // weight is [in_features, out_features] here + + // d_input = d_output * weight.T --> d_input.T = weight * d_output.T + // C = d_input.T[in_features, bs] + // A = weight.T[out_features, in_features] + // B = d_output.T[out_features, bs] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_T, CUBLAS_OP_N, in_features, bs, out_features, &alpha, + static_cast(weight->DataPtr()), out_features, + static_cast(grad_output->DataPtr()), out_features, &beta, + static_cast(grad_input->DataPtr()), in_features)); + + // d_weight = input.T * d_output --> d_weight.T = d_output.T * input + // C = d_weight.T[out_features, in_features] + // A = d_output.T[out_features, bs] + // B = input.T[in_features, bs] + CUBLAS_CHECK(cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_T, out_features, in_features, bs, &alpha, + static_cast(grad_output->DataPtr()), out_features, + static_cast(input->DataPtr()), in_features, &beta, + static_cast(grad_weight->DataPtr()), out_features)); + } + + // d_bias = \sum_i(i=0, bs-1) d_output[i] + if (bias) { + constexpr int BLOCK_SIZE = 256; + int threads_per_block = BLOCK_SIZE; + int num_blocks = out_features; + ReduceColumnsKernel + <<>>(static_cast(grad_output->DataPtr()), + static_cast(grad_bias->DataPtr()), out_features, bs); + } + + CUBLAS_CHECK(cublasDestroy(handle)); + + return {grad_input, grad_weight, grad_bias}; +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_LINEAR_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_LINEAR_KERNEL(MatmulForward) +REGISTER_CUDA_LINEAR_KERNEL(MatmulBackward) +REGISTER_CUDA_LINEAR_KERNEL(LinearForward) +REGISTER_CUDA_LINEAR_KERNEL(LinearBackward) + +#undef REGISTER_CUDA_LINEAR_KERNEL diff --git a/infini_train/src/kernels/cuda/no_op.cu b/infini_train/src/kernels/cuda/no_op.cu index eba88c5..428acb6 100644 --- a/infini_train/src/kernels/cuda/no_op.cu +++ b/infini_train/src/kernels/cuda/no_op.cu @@ -1,30 +1,30 @@ -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { -std::shared_ptr NoOpForward(const std::shared_ptr &input, const std::vector &dims) { - const int64_t num_elements = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); - CHECK_EQ(input->NumElements(), num_elements); - - auto output = std::make_shared(*input, 0, dims); - return output; -} - -std::shared_ptr NoOpBackward(const std::vector &dims, const std::shared_ptr &grad_output) { - auto num_elements = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); - CHECK_EQ(num_elements, grad_output->NumElements()); - - auto grad_input = std::make_shared(*grad_output, 0, dims); - return grad_input; -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_NO_OP_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_NO_OP_KERNEL(NoOpForward) -REGISTER_CUDA_NO_OP_KERNEL(NoOpBackward) - -#undef REGISTER_CUDA_NO_OP_KERNEL +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { +std::shared_ptr NoOpForward(const std::shared_ptr &input, const std::vector &dims) { + const int64_t num_elements = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); + CHECK_EQ(input->NumElements(), num_elements); + + auto output = std::make_shared(*input, 0, dims); + return output; +} + +std::shared_ptr NoOpBackward(const std::vector &dims, const std::shared_ptr &grad_output) { + auto num_elements = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); + CHECK_EQ(num_elements, grad_output->NumElements()); + + auto grad_input = std::make_shared(*grad_output, 0, dims); + return grad_input; +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_NO_OP_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_NO_OP_KERNEL(NoOpForward) +REGISTER_CUDA_NO_OP_KERNEL(NoOpBackward) + +#undef REGISTER_CUDA_NO_OP_KERNEL diff --git a/infini_train/src/kernels/cuda/outer.cu b/infini_train/src/kernels/cuda/outer.cu index 2d330bd..5a22c17 100644 --- a/infini_train/src/kernels/cuda/outer.cu +++ b/infini_train/src/kernels/cuda/outer.cu @@ -1,112 +1,112 @@ -#include -#include - -#include "cublas_v2.h" -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { - -#define CUDA_CHECK(call) \ - do { \ - cudaError_t status = call; \ - if (status != cudaSuccess) { \ - LOG(FATAL) << "CUDA Error: " << cudaGetErrorString(status) << " at " << __FILE__ << ":" << __LINE__; \ - } \ - } while (0) - -#define CUBLAS_CHECK(call) \ - do { \ - cublasStatus_t status = call; \ - if (status != CUBLAS_STATUS_SUCCESS) { \ - LOG(FATAL) << "CUBLAS Error: " << cublasGetStatusString(status) << " at " << __FILE__ << ":" << __LINE__; \ - } \ - } while (0) - -std::shared_ptr OuterForward(const std::shared_ptr &input, const std::shared_ptr &other) { - /* - Computes outer product: output[i, j] = input[i] * other[j] - Equivalent to: input: [M, 1], other: [1, N] → output: [M, N] - */ - - const auto &in_dims = input->Dims(); - const auto &ot_dims = other->Dims(); - CHECK_EQ(in_dims.size(), 1); - CHECK_EQ(ot_dims.size(), 1); - - const int64_t M = in_dims[0]; - const int64_t N = ot_dims[0]; - - auto output = std::make_shared(std::vector{M, N}, DataType::kFLOAT32, input->GetDevice()); - - // reinterpret input: [M] as column vector [M, 1] - // reinterpret other: [N] as row vector [1, N] - // output[M, N] = input[M, 1] * other.T[1, N] - // output.T[N, M] = other[N, 1] * input.T[1, M] - float alpha = 1.0f; - float beta = 0.0f; - cublasHandle_t handle; - CUBLAS_CHECK(cublasCreate(&handle)); - - CUBLAS_CHECK(cublasSgemm( - handle, CUBLAS_OP_N, CUBLAS_OP_N, N, M, 1, &alpha, static_cast(other->DataPtr()), N, - static_cast(input->DataPtr()), 1, &beta, static_cast(output->DataPtr()), N)); - - CUBLAS_CHECK(cublasDestroy(handle)); - return output; -} - -std::tuple, std::shared_ptr> OuterBackward(const std::shared_ptr &input, - const std::shared_ptr &other, - const std::shared_ptr &grad_output) { - /* - grad_input: [M] = grad_output: [M, N] × other: [N] - grad_other: [N] = grad_output.T: [N, M] × input: [M] - */ - const int64_t M = input->Dims()[0]; - const int64_t N = other->Dims()[0]; - CHECK_EQ(grad_output->Dims().size(), 2); - CHECK_EQ(grad_output->Dims()[0], M); - CHECK_EQ(grad_output->Dims()[1], N); - - auto grad_input = std::make_shared(std::vector{M}, DataType::kFLOAT32, grad_output->GetDevice()); - auto grad_other = std::make_shared(std::vector{N}, DataType::kFLOAT32, grad_output->GetDevice()); - grad_input->Fill(0.0f); - grad_other->Fill(0.0f); - - float alpha = 1.0f; - float beta = 0.0f; - cublasHandle_t handle; - CUBLAS_CHECK(cublasCreate(&handle)); - - // grad_input[M, 1] = grad_output[M, N] × other[N, 1] - // y = grad_input[M] - // A = grad_output.T[N, M] - // x = other[N] - CUBLAS_CHECK(cublasSgemv(handle, CUBLAS_OP_T, N, M, &alpha, static_cast(grad_output->DataPtr()), N, - static_cast(other->DataPtr()), 1, &beta, - static_cast(grad_input->DataPtr()), 1)); - - // grad_other[N, 1] = grad_output.T[N, M] × input[M, 1] - // y = grad_other[N] - // A = grad_output.T[N, M] - // x = input[M] - CUBLAS_CHECK(cublasSgemv(handle, CUBLAS_OP_N, N, M, &alpha, static_cast(grad_output->DataPtr()), N, - static_cast(input->DataPtr()), 1, &beta, - static_cast(grad_other->DataPtr()), 1)); - - CUBLAS_CHECK(cublasDestroy(handle)); - return {grad_input, grad_other}; -} - -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_OUTER_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_OUTER_KERNEL(OuterForward) -REGISTER_CUDA_OUTER_KERNEL(OuterBackward) - -#undef REGISTER_CUDA_OUTER_KERNEL +#include +#include + +#include "cublas_v2.h" +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { + +#define CUDA_CHECK(call) \ + do { \ + cudaError_t status = call; \ + if (status != cudaSuccess) { \ + LOG(FATAL) << "CUDA Error: " << cudaGetErrorString(status) << " at " << __FILE__ << ":" << __LINE__; \ + } \ + } while (0) + +#define CUBLAS_CHECK(call) \ + do { \ + cublasStatus_t status = call; \ + if (status != CUBLAS_STATUS_SUCCESS) { \ + LOG(FATAL) << "CUBLAS Error: " << cublasGetStatusString(status) << " at " << __FILE__ << ":" << __LINE__; \ + } \ + } while (0) + +std::shared_ptr OuterForward(const std::shared_ptr &input, const std::shared_ptr &other) { + /* + Computes outer product: output[i, j] = input[i] * other[j] + Equivalent to: input: [M, 1], other: [1, N] → output: [M, N] + */ + + const auto &in_dims = input->Dims(); + const auto &ot_dims = other->Dims(); + CHECK_EQ(in_dims.size(), 1); + CHECK_EQ(ot_dims.size(), 1); + + const int64_t M = in_dims[0]; + const int64_t N = ot_dims[0]; + + auto output = std::make_shared(std::vector{M, N}, DataType::kFLOAT32, input->GetDevice()); + + // reinterpret input: [M] as column vector [M, 1] + // reinterpret other: [N] as row vector [1, N] + // output[M, N] = input[M, 1] * other.T[1, N] + // output.T[N, M] = other[N, 1] * input.T[1, M] + float alpha = 1.0f; + float beta = 0.0f; + cublasHandle_t handle; + CUBLAS_CHECK(cublasCreate(&handle)); + + CUBLAS_CHECK(cublasSgemm( + handle, CUBLAS_OP_N, CUBLAS_OP_N, N, M, 1, &alpha, static_cast(other->DataPtr()), N, + static_cast(input->DataPtr()), 1, &beta, static_cast(output->DataPtr()), N)); + + CUBLAS_CHECK(cublasDestroy(handle)); + return output; +} + +std::tuple, std::shared_ptr> OuterBackward(const std::shared_ptr &input, + const std::shared_ptr &other, + const std::shared_ptr &grad_output) { + /* + grad_input: [M] = grad_output: [M, N] × other: [N] + grad_other: [N] = grad_output.T: [N, M] × input: [M] + */ + const int64_t M = input->Dims()[0]; + const int64_t N = other->Dims()[0]; + CHECK_EQ(grad_output->Dims().size(), 2); + CHECK_EQ(grad_output->Dims()[0], M); + CHECK_EQ(grad_output->Dims()[1], N); + + auto grad_input = std::make_shared(std::vector{M}, DataType::kFLOAT32, grad_output->GetDevice()); + auto grad_other = std::make_shared(std::vector{N}, DataType::kFLOAT32, grad_output->GetDevice()); + grad_input->Fill(0.0f); + grad_other->Fill(0.0f); + + float alpha = 1.0f; + float beta = 0.0f; + cublasHandle_t handle; + CUBLAS_CHECK(cublasCreate(&handle)); + + // grad_input[M, 1] = grad_output[M, N] × other[N, 1] + // y = grad_input[M] + // A = grad_output.T[N, M] + // x = other[N] + CUBLAS_CHECK(cublasSgemv(handle, CUBLAS_OP_T, N, M, &alpha, static_cast(grad_output->DataPtr()), N, + static_cast(other->DataPtr()), 1, &beta, + static_cast(grad_input->DataPtr()), 1)); + + // grad_other[N, 1] = grad_output.T[N, M] × input[M, 1] + // y = grad_other[N] + // A = grad_output.T[N, M] + // x = input[M] + CUBLAS_CHECK(cublasSgemv(handle, CUBLAS_OP_N, N, M, &alpha, static_cast(grad_output->DataPtr()), N, + static_cast(input->DataPtr()), 1, &beta, + static_cast(grad_other->DataPtr()), 1)); + + CUBLAS_CHECK(cublasDestroy(handle)); + return {grad_input, grad_other}; +} + +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_OUTER_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_OUTER_KERNEL(OuterForward) +REGISTER_CUDA_OUTER_KERNEL(OuterBackward) + +#undef REGISTER_CUDA_OUTER_KERNEL diff --git a/infini_train/src/kernels/cuda/reduction.cu b/infini_train/src/kernels/cuda/reduction.cu index e85e113..4e61dd4 100644 --- a/infini_train/src/kernels/cuda/reduction.cu +++ b/infini_train/src/kernels/cuda/reduction.cu @@ -1,230 +1,230 @@ -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { -namespace { -constexpr float kInfinity = std::numeric_limits::infinity(); -} // namespace - -namespace { - -#if defined(CUB_VERSION) && CUB_VERSION >= 200800 - #include - using CubSumOp = ::cuda::std::plus<>; - using CubMaxOp = ::cuda::maximum<>; - using CubMinOp = ::cuda::minimum<>; -#else - using CubSumOp = cub::Sum; - using CubMaxOp = cub::Max; - using CubMinOp = cub::Min; -#endif - -// Reduction operators -template struct CubOp; - -template <> struct CubOp { - __device__ static float Init() { return 0.0f; } - __device__ static float Reduce(float a, float b) { return a + b; } - __device__ static CubSumOp Op() { return CubSumOp(); } -}; - -template <> struct CubOp { - __device__ static float Init() { return -kInfinity; } - __device__ static float Reduce(float a, float b) { return fmaxf(a, b); } - __device__ static CubMaxOp Op() { return CubMaxOp(); } -}; - -template <> struct CubOp { - __device__ static float Init() { return kInfinity; } - __device__ static float Reduce(float a, float b) { return fminf(a, b); } - __device__ static CubMinOp Op() { return CubMinOp(); } -}; - -// Finalization strategies -struct MeanFinalize { - __device__ __forceinline__ float operator()(float sum, int64_t count) const { - return sum / static_cast(count); - } -}; - -struct IdentityFinalize { - __device__ __forceinline__ float operator()(float val, int64_t) const { return val; } -}; - -// Generic reduction kernel -template -__global__ void GenericReduceKernel(const float *input, float *output, int64_t N, int64_t H, int64_t W, - FinalizeOp finalize_op) { - using BlockReduce = cub::BlockReduce; - __shared__ typename BlockReduce::TempStorage temp_storage; - - int idx = blockIdx.x; - if (idx >= N * W) { - return; - } - - int n = idx / W; - int w = idx % W; - - float acc = CubOp::Init(); - for (int64_t h = threadIdx.x; h < H; h += blockDim.x) { - int input_idx = (n * H + h) * W + w; - acc = CubOp::Reduce(acc, input[input_idx]); - } - - float reduced = BlockReduce(temp_storage).Reduce(acc, CubOp::Op()); - - if (threadIdx.x == 0) { - output[idx] = finalize_op(reduced, H); - } -} - -// Unified backward kernel for Mean, Sum, Max, and Min -__global__ void GenericReduceBackwardKernel(float *grad_input, const float *grad_output, const float *input, - const float *reduced, int64_t N, int64_t H, int64_t W, bool is_mean, - bool is_masked) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx >= N * H * W) { - return; - } - - int n = idx / (H * W); - int hw = idx % (H * W); - int w = hw % W; - - int reduced_idx = n * W + w; - - if (is_masked) { - float selected = reduced[reduced_idx]; - float value = input[idx]; - grad_input[idx] = (value == selected) ? grad_output[reduced_idx] : 0.0f; - } else { - grad_input[idx] = grad_output[reduced_idx]; - if (is_mean) { - grad_input[idx] /= static_cast(H); - } - } -} -} // namespace - -// Common forward implementation for reduce ops -template -std::shared_ptr ReduceOpForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim, - FinalizeOp finalize_op) { - const auto &input_dims = input->Dims(); - int64_t actual_dim = dim < 0 ? dim + input_dims.size() : dim; - CHECK_GE(actual_dim, 0); - CHECK_LT(actual_dim, input_dims.size()); - - std::vector output_dims = input_dims; - if (keep_dim) { - output_dims[actual_dim] = 1; - } else { - output_dims.erase(output_dims.begin() + actual_dim); - } - - auto output = std::make_shared(output_dims, DataType::kFLOAT32, input->GetDevice()); - - int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + actual_dim, 1, std::multiplies()); - int64_t H = input_dims[actual_dim]; - int64_t W = std::accumulate(input_dims.begin() + actual_dim + 1, input_dims.end(), 1, std::multiplies()); - - const float *input_ptr = static_cast(input->DataPtr()); - float *output_ptr = static_cast(output->DataPtr()); - - constexpr int BLOCK_SIZE = 256; - int threads_per_block = BLOCK_SIZE; - int num_blocks = N * W; - - GenericReduceKernel - <<>>(input_ptr, output_ptr, N, H, W, finalize_op); - - return output; -} - -// Common backward implementation for reduce ops -std::shared_ptr ReduceOpBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &input, const std::shared_ptr &reduced, - const std::vector &input_dims, const int64_t dim, bool keep_dim, - bool is_mean, bool is_masked) { - int64_t actual_dim = dim < 0 ? dim + input_dims.size() : dim; - CHECK_GE(actual_dim, 0); - CHECK_LT(actual_dim, input_dims.size()); - - auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32, grad_output->GetDevice()); - grad_input->Fill(0.0f); - - int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + actual_dim, 1, std::multiplies()); - int64_t H = input_dims[actual_dim]; - int64_t W = std::accumulate(input_dims.begin() + actual_dim + 1, input_dims.end(), 1, std::multiplies()); - - float *grad_input_ptr = static_cast(grad_input->DataPtr()); - const float *grad_output_ptr = static_cast(grad_output->DataPtr()); - const float *input_ptr = input ? static_cast(input->DataPtr()) : nullptr; - const float *reduced_ptr = reduced ? static_cast(reduced->DataPtr()) : nullptr; - - int threads_per_block = 256; - int num_blocks = (N * H * W + threads_per_block - 1) / threads_per_block; - - GenericReduceBackwardKernel<<>>(grad_input_ptr, grad_output_ptr, input_ptr, - reduced_ptr, N, H, W, is_mean, is_masked); - - return grad_input; -} - -std::shared_ptr MeanForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { - return ReduceOpForward(input, dim, keep_dim, MeanFinalize{}); -} - -std::shared_ptr SumForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { - return ReduceOpForward(input, dim, keep_dim, IdentityFinalize{}); -} - -std::shared_ptr MaxForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { - return ReduceOpForward(input, dim, keep_dim, IdentityFinalize{}); -} - -std::shared_ptr MinForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { - return ReduceOpForward(input, dim, keep_dim, IdentityFinalize{}); -} - -std::shared_ptr MeanBackward(const std::shared_ptr &grad_output, const std::vector &input_dims, - const int64_t dim, bool keep_dim) { - return ReduceOpBackward(grad_output, nullptr, nullptr, input_dims, dim, keep_dim, true, false); -} - -std::shared_ptr SumBackward(const std::shared_ptr &grad_output, const std::vector &input_dims, - const int64_t dim, bool keep_dim) { - return ReduceOpBackward(grad_output, nullptr, nullptr, input_dims, dim, keep_dim, false, false); -} - -std::shared_ptr MaxBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, - const std::shared_ptr &reduced, const int64_t dim, bool keep_dim) { - return ReduceOpBackward(grad_output, input, reduced, input->Dims(), dim, keep_dim, false, true); -} - -std::shared_ptr MinBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, - const std::shared_ptr &reduced, const int64_t dim, bool keep_dim) { - return ReduceOpBackward(grad_output, input, reduced, input->Dims(), dim, keep_dim, false, true); -} - -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_REDUCTION_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_REDUCTION_KERNEL(MeanForward) -REGISTER_CUDA_REDUCTION_KERNEL(SumForward) -REGISTER_CUDA_REDUCTION_KERNEL(MaxForward) -REGISTER_CUDA_REDUCTION_KERNEL(MinForward) -REGISTER_CUDA_REDUCTION_KERNEL(MeanBackward) -REGISTER_CUDA_REDUCTION_KERNEL(SumBackward) -REGISTER_CUDA_REDUCTION_KERNEL(MaxBackward) -REGISTER_CUDA_REDUCTION_KERNEL(MinBackward) - -#undef REGISTER_CUDA_REDUCTION_KERNEL +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { +namespace { +constexpr float kInfinity = std::numeric_limits::infinity(); +} // namespace + +namespace { + +#if defined(CUB_VERSION) && CUB_VERSION >= 200800 + #include + using CubSumOp = ::cuda::std::plus<>; + using CubMaxOp = ::cuda::maximum<>; + using CubMinOp = ::cuda::minimum<>; +#else + using CubSumOp = cub::Sum; + using CubMaxOp = cub::Max; + using CubMinOp = cub::Min; +#endif + +// Reduction operators +template struct CubOp; + +template <> struct CubOp { + __device__ static float Init() { return 0.0f; } + __device__ static float Reduce(float a, float b) { return a + b; } + __device__ static CubSumOp Op() { return CubSumOp(); } +}; + +template <> struct CubOp { + __device__ static float Init() { return -kInfinity; } + __device__ static float Reduce(float a, float b) { return fmaxf(a, b); } + __device__ static CubMaxOp Op() { return CubMaxOp(); } +}; + +template <> struct CubOp { + __device__ static float Init() { return kInfinity; } + __device__ static float Reduce(float a, float b) { return fminf(a, b); } + __device__ static CubMinOp Op() { return CubMinOp(); } +}; + +// Finalization strategies +struct MeanFinalize { + __device__ __forceinline__ float operator()(float sum, int64_t count) const { + return sum / static_cast(count); + } +}; + +struct IdentityFinalize { + __device__ __forceinline__ float operator()(float val, int64_t) const { return val; } +}; + +// Generic reduction kernel +template +__global__ void GenericReduceKernel(const float *input, float *output, int64_t N, int64_t H, int64_t W, + FinalizeOp finalize_op) { + using BlockReduce = cub::BlockReduce; + __shared__ typename BlockReduce::TempStorage temp_storage; + + int idx = blockIdx.x; + if (idx >= N * W) { + return; + } + + int n = idx / W; + int w = idx % W; + + float acc = CubOp::Init(); + for (int64_t h = threadIdx.x; h < H; h += blockDim.x) { + int input_idx = (n * H + h) * W + w; + acc = CubOp::Reduce(acc, input[input_idx]); + } + + float reduced = BlockReduce(temp_storage).Reduce(acc, CubOp::Op()); + + if (threadIdx.x == 0) { + output[idx] = finalize_op(reduced, H); + } +} + +// Unified backward kernel for Mean, Sum, Max, and Min +__global__ void GenericReduceBackwardKernel(float *grad_input, const float *grad_output, const float *input, + const float *reduced, int64_t N, int64_t H, int64_t W, bool is_mean, + bool is_masked) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= N * H * W) { + return; + } + + int n = idx / (H * W); + int hw = idx % (H * W); + int w = hw % W; + + int reduced_idx = n * W + w; + + if (is_masked) { + float selected = reduced[reduced_idx]; + float value = input[idx]; + grad_input[idx] = (value == selected) ? grad_output[reduced_idx] : 0.0f; + } else { + grad_input[idx] = grad_output[reduced_idx]; + if (is_mean) { + grad_input[idx] /= static_cast(H); + } + } +} +} // namespace + +// Common forward implementation for reduce ops +template +std::shared_ptr ReduceOpForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim, + FinalizeOp finalize_op) { + const auto &input_dims = input->Dims(); + int64_t actual_dim = dim < 0 ? dim + input_dims.size() : dim; + CHECK_GE(actual_dim, 0); + CHECK_LT(actual_dim, input_dims.size()); + + std::vector output_dims = input_dims; + if (keep_dim) { + output_dims[actual_dim] = 1; + } else { + output_dims.erase(output_dims.begin() + actual_dim); + } + + auto output = std::make_shared(output_dims, DataType::kFLOAT32, input->GetDevice()); + + int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + actual_dim, 1, std::multiplies()); + int64_t H = input_dims[actual_dim]; + int64_t W = std::accumulate(input_dims.begin() + actual_dim + 1, input_dims.end(), 1, std::multiplies()); + + const float *input_ptr = static_cast(input->DataPtr()); + float *output_ptr = static_cast(output->DataPtr()); + + constexpr int BLOCK_SIZE = 256; + int threads_per_block = BLOCK_SIZE; + int num_blocks = N * W; + + GenericReduceKernel + <<>>(input_ptr, output_ptr, N, H, W, finalize_op); + + return output; +} + +// Common backward implementation for reduce ops +std::shared_ptr ReduceOpBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &input, const std::shared_ptr &reduced, + const std::vector &input_dims, const int64_t dim, bool keep_dim, + bool is_mean, bool is_masked) { + int64_t actual_dim = dim < 0 ? dim + input_dims.size() : dim; + CHECK_GE(actual_dim, 0); + CHECK_LT(actual_dim, input_dims.size()); + + auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32, grad_output->GetDevice()); + grad_input->Fill(0.0f); + + int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + actual_dim, 1, std::multiplies()); + int64_t H = input_dims[actual_dim]; + int64_t W = std::accumulate(input_dims.begin() + actual_dim + 1, input_dims.end(), 1, std::multiplies()); + + float *grad_input_ptr = static_cast(grad_input->DataPtr()); + const float *grad_output_ptr = static_cast(grad_output->DataPtr()); + const float *input_ptr = input ? static_cast(input->DataPtr()) : nullptr; + const float *reduced_ptr = reduced ? static_cast(reduced->DataPtr()) : nullptr; + + int threads_per_block = 256; + int num_blocks = (N * H * W + threads_per_block - 1) / threads_per_block; + + GenericReduceBackwardKernel<<>>(grad_input_ptr, grad_output_ptr, input_ptr, + reduced_ptr, N, H, W, is_mean, is_masked); + + return grad_input; +} + +std::shared_ptr MeanForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { + return ReduceOpForward(input, dim, keep_dim, MeanFinalize{}); +} + +std::shared_ptr SumForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { + return ReduceOpForward(input, dim, keep_dim, IdentityFinalize{}); +} + +std::shared_ptr MaxForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { + return ReduceOpForward(input, dim, keep_dim, IdentityFinalize{}); +} + +std::shared_ptr MinForward(const std::shared_ptr &input, const int64_t dim, const bool keep_dim) { + return ReduceOpForward(input, dim, keep_dim, IdentityFinalize{}); +} + +std::shared_ptr MeanBackward(const std::shared_ptr &grad_output, const std::vector &input_dims, + const int64_t dim, bool keep_dim) { + return ReduceOpBackward(grad_output, nullptr, nullptr, input_dims, dim, keep_dim, true, false); +} + +std::shared_ptr SumBackward(const std::shared_ptr &grad_output, const std::vector &input_dims, + const int64_t dim, bool keep_dim) { + return ReduceOpBackward(grad_output, nullptr, nullptr, input_dims, dim, keep_dim, false, false); +} + +std::shared_ptr MaxBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, + const std::shared_ptr &reduced, const int64_t dim, bool keep_dim) { + return ReduceOpBackward(grad_output, input, reduced, input->Dims(), dim, keep_dim, false, true); +} + +std::shared_ptr MinBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, + const std::shared_ptr &reduced, const int64_t dim, bool keep_dim) { + return ReduceOpBackward(grad_output, input, reduced, input->Dims(), dim, keep_dim, false, true); +} + +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_REDUCTION_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_REDUCTION_KERNEL(MeanForward) +REGISTER_CUDA_REDUCTION_KERNEL(SumForward) +REGISTER_CUDA_REDUCTION_KERNEL(MaxForward) +REGISTER_CUDA_REDUCTION_KERNEL(MinForward) +REGISTER_CUDA_REDUCTION_KERNEL(MeanBackward) +REGISTER_CUDA_REDUCTION_KERNEL(SumBackward) +REGISTER_CUDA_REDUCTION_KERNEL(MaxBackward) +REGISTER_CUDA_REDUCTION_KERNEL(MinBackward) + +#undef REGISTER_CUDA_REDUCTION_KERNEL diff --git a/infini_train/src/kernels/cuda/sigmoid.cu b/infini_train/src/kernels/cuda/sigmoid.cu index 3f32447..a77aa7d 100644 --- a/infini_train/src/kernels/cuda/sigmoid.cu +++ b/infini_train/src/kernels/cuda/sigmoid.cu @@ -1,66 +1,66 @@ -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { -__global__ void SigmoidForwardKernel(const float *input_ptr, float *output_ptr, size_t num_elements) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx < num_elements) { - output_ptr[idx] = 1.0f / (1.0f + exp(-input_ptr[idx])); - } -} - -std::shared_ptr SigmoidForward(const std::shared_ptr &input) { - size_t num_elements = input->NumElements(); - - auto output = std::make_shared(input->Dims(), DataType::kFLOAT32, input->GetDevice()); - - const float *input_ptr = static_cast(input->DataPtr()); - float *output_ptr = static_cast(output->DataPtr()); - - int threads_per_block = 256; - int num_blocks = (num_elements + threads_per_block - 1) / threads_per_block; - - SigmoidForwardKernel<<>>(input_ptr, output_ptr, num_elements); - - return output; -} - -__global__ void SigmoidBackwardKernel(const float *output_ptr, const float *grad_output_ptr, float *grad_input_ptr, - size_t num_elements) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx < num_elements) { - grad_input_ptr[idx] = grad_output_ptr[idx] * output_ptr[idx] * (1.0f - output_ptr[idx]); - } -} - -std::shared_ptr SigmoidBackward(const std::shared_ptr &output, - const std::shared_ptr &grad_output) { - size_t num_elements = output->NumElements(); - - auto grad_input = std::make_shared(output->Dims(), DataType::kFLOAT32, grad_output->GetDevice()); - grad_input->Fill(0.0f); - const float *output_ptr = static_cast(output->DataPtr()); - const float *grad_output_ptr = static_cast(grad_output->DataPtr()); - float *grad_input_ptr = static_cast(grad_input->DataPtr()); - - int threads_per_block = 256; - int num_blocks = (num_elements + threads_per_block - 1) / threads_per_block; - - SigmoidBackwardKernel<<>>(output_ptr, grad_output_ptr, grad_input_ptr, num_elements); - - return grad_input; -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_SIGMOID_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_SIGMOID_KERNEL(SigmoidForward) -REGISTER_CUDA_SIGMOID_KERNEL(SigmoidBackward) - -#undef REGISTER_CUDA_SIGMOID_KERNEL +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { +__global__ void SigmoidForwardKernel(const float *input_ptr, float *output_ptr, size_t num_elements) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < num_elements) { + output_ptr[idx] = 1.0f / (1.0f + exp(-input_ptr[idx])); + } +} + +std::shared_ptr SigmoidForward(const std::shared_ptr &input) { + size_t num_elements = input->NumElements(); + + auto output = std::make_shared(input->Dims(), DataType::kFLOAT32, input->GetDevice()); + + const float *input_ptr = static_cast(input->DataPtr()); + float *output_ptr = static_cast(output->DataPtr()); + + int threads_per_block = 256; + int num_blocks = (num_elements + threads_per_block - 1) / threads_per_block; + + SigmoidForwardKernel<<>>(input_ptr, output_ptr, num_elements); + + return output; +} + +__global__ void SigmoidBackwardKernel(const float *output_ptr, const float *grad_output_ptr, float *grad_input_ptr, + size_t num_elements) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < num_elements) { + grad_input_ptr[idx] = grad_output_ptr[idx] * output_ptr[idx] * (1.0f - output_ptr[idx]); + } +} + +std::shared_ptr SigmoidBackward(const std::shared_ptr &output, + const std::shared_ptr &grad_output) { + size_t num_elements = output->NumElements(); + + auto grad_input = std::make_shared(output->Dims(), DataType::kFLOAT32, grad_output->GetDevice()); + grad_input->Fill(0.0f); + const float *output_ptr = static_cast(output->DataPtr()); + const float *grad_output_ptr = static_cast(grad_output->DataPtr()); + float *grad_input_ptr = static_cast(grad_input->DataPtr()); + + int threads_per_block = 256; + int num_blocks = (num_elements + threads_per_block - 1) / threads_per_block; + + SigmoidBackwardKernel<<>>(output_ptr, grad_output_ptr, grad_input_ptr, num_elements); + + return grad_input; +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_SIGMOID_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_SIGMOID_KERNEL(SigmoidForward) +REGISTER_CUDA_SIGMOID_KERNEL(SigmoidBackward) + +#undef REGISTER_CUDA_SIGMOID_KERNEL diff --git a/infini_train/src/kernels/cuda/slice.cu b/infini_train/src/kernels/cuda/slice.cu index 61ea478..52d38bd 100644 --- a/infini_train/src/kernels/cuda/slice.cu +++ b/infini_train/src/kernels/cuda/slice.cu @@ -1,173 +1,173 @@ -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { -__global__ void SliceForwardKernel(const float *input, float *output, const int64_t *new_dims, const int64_t *starts, - const int64_t *steps, const int64_t *in_strides, const int64_t *out_strides, - int num_dims, int64_t total_elements) { - int64_t out_idx = blockIdx.x * blockDim.x + threadIdx.x; - if (out_idx >= total_elements) { - return; - } - - int64_t in_index = 0; - for (int i = 0; i < num_dims; ++i) { - int64_t idx = (out_idx / out_strides[i]) % new_dims[i]; - in_index += (starts[i] + idx * steps[i]) * in_strides[i]; - } - - output[out_idx] = input[in_index]; -} - -std::shared_ptr SliceForward(const std::shared_ptr &input, const std::vector &starts, - const std::vector &ends, const std::vector &steps) { - CHECK_EQ(starts.size(), ends.size()); - CHECK_EQ(starts.size(), steps.size()); - auto &dims = input->Dims(); - CHECK_EQ(starts.size(), dims.size()); - const int64_t num_dims = dims.size(); - - std::vector new_dims; - for (int i = 0; i < starts.size(); i++) { - CHECK_LE(starts[i], ends[i]); - CHECK_LE(0, steps[i]); - new_dims.push_back((ends[i] - starts[i] + steps[i] - 1) / steps[i]); - } - - auto new_tensor = std::make_shared(new_dims, input->Dtype(), input->GetDevice()); - // NOTE(zbl): must initialize with 0 - new_tensor->Fill(0.0f); - - std::vector src_strides(dims.size(), 0), dst_strides(new_dims.size(), 0); - int64_t stride = 1; - for (int i = dims.size() - 1; i >= 0; --i) { - src_strides[i] = stride; - stride *= dims[i]; - } - - stride = 1; - for (int i = new_dims.size() - 1; i >= 0; --i) { - dst_strides[i] = stride; - stride *= new_dims[i]; - } - - int64_t total_elements = stride; - - int64_t *new_dims_dev, *starts_dev, *steps_dev, *input_strides_dev, *output_strides_dev; - - cudaMallocAsync(&new_dims_dev, - (ends.size() + starts.size() + steps.size() + dims.size() + new_dims.size()) * sizeof(int64_t), 0); - starts_dev = new_dims_dev + ends.size(); - steps_dev = starts_dev + starts.size(); - input_strides_dev = steps_dev + steps.size(); - output_strides_dev = input_strides_dev + dims.size(); - - cudaMemcpyAsync(new_dims_dev, new_dims.data(), ends.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); - cudaMemcpyAsync(starts_dev, starts.data(), starts.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); - cudaMemcpyAsync(steps_dev, steps.data(), steps.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); - cudaMemcpyAsync(input_strides_dev, src_strides.data(), dims.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); - cudaMemcpyAsync(output_strides_dev, dst_strides.data(), new_dims.size() * sizeof(int64_t), cudaMemcpyHostToDevice, - 0); - - int threads_per_block = 256; - int num_blocks = (total_elements + threads_per_block - 1) / threads_per_block; - - SliceForwardKernel<<>>( - static_cast(input->DataPtr()), static_cast(new_tensor->DataPtr()), new_dims_dev, - starts_dev, steps_dev, input_strides_dev, output_strides_dev, num_dims, total_elements); - - cudaFreeAsync(new_dims_dev, 0); - - return new_tensor; -} - -__global__ void SliceBackwardKernel(const float *grad_output, float *grad_input, const int64_t *new_dims, - const int64_t *starts, const int64_t *steps, const int64_t *in_strides, - const int64_t *out_strides, int num_dims, int64_t total_elements) { - int64_t out_idx = blockIdx.x * blockDim.x + threadIdx.x; - if (out_idx >= total_elements) { - return; - } - - int64_t in_index = 0; - for (int i = 0; i < num_dims; ++i) { - int64_t idx = (out_idx / out_strides[i]) % new_dims[i]; - in_index += (starts[i] + idx * steps[i]) * in_strides[i]; - } - atomicAdd(&grad_input[in_index], grad_output[out_idx]); -} - -std::shared_ptr SliceBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, - const std::vector &starts, const std::vector &ends, - const std::vector &steps) { - CHECK_EQ(starts.size(), ends.size()); - CHECK_EQ(starts.size(), steps.size()); - auto &dims = input->Dims(); - CHECK_EQ(starts.size(), dims.size()); - const int64_t num_dims = dims.size(); - - std::vector new_dims; - for (int i = 0; i < starts.size(); i++) { - CHECK_LE(starts[i], ends[i]); - CHECK_LE(0, steps[i]); - new_dims.push_back((ends[i] - starts[i] + steps[i] - 1) / steps[i]); - } - - auto grad_input = std::make_shared(input->Dims(), input->Dtype(), grad_output->GetDevice()); - grad_input->Fill(0.0); - - std::vector src_strides(dims.size()); - int64_t stride = 1; - for (int i = src_strides.size() - 1; i >= 0; --i) { - src_strides[i] = stride; - stride *= dims[i]; - } - - std::vector dst_strides(new_dims.size()); - stride = 1; - for (int i = dst_strides.size() - 1; i >= 0; --i) { - dst_strides[i] = stride; - stride *= new_dims[i]; - } - - int64_t total_elements = stride; - - int dims_size = dims.size(); - int64_t *new_dims_dev, *starts_dev, *steps_dev, *input_strides_dev, *output_strides_dev; - - cudaMallocAsync(&new_dims_dev, - (ends.size() + starts.size() + steps.size() + dims.size() + new_dims.size()) * sizeof(int64_t), 0); - starts_dev = new_dims_dev + ends.size(); - steps_dev = starts_dev + starts.size(); - input_strides_dev = steps_dev + steps.size(); - output_strides_dev = input_strides_dev + dims.size(); - - cudaMemcpyAsync(new_dims_dev, new_dims.data(), ends.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); - cudaMemcpyAsync(starts_dev, starts.data(), starts.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); - cudaMemcpyAsync(steps_dev, steps.data(), steps.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); - cudaMemcpyAsync(input_strides_dev, src_strides.data(), dims.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); - cudaMemcpyAsync(output_strides_dev, dst_strides.data(), new_dims.size() * sizeof(int64_t), cudaMemcpyHostToDevice, - 0); - - int threads_per_block = 256; - int num_blocks = (total_elements + threads_per_block - 1) / threads_per_block; - - SliceBackwardKernel<<>>( - static_cast(grad_output->DataPtr()), static_cast(grad_input->DataPtr()), new_dims_dev, - starts_dev, steps_dev, input_strides_dev, output_strides_dev, num_dims, total_elements); - - cudaFreeAsync(new_dims_dev, 0); - - return grad_input; -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_SLICE_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_SLICE_KERNEL(SliceForward) -REGISTER_CUDA_SLICE_KERNEL(SliceBackward) - -#undef REGISTER_CUDA_SLICE_KERNEL +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { +__global__ void SliceForwardKernel(const float *input, float *output, const int64_t *new_dims, const int64_t *starts, + const int64_t *steps, const int64_t *in_strides, const int64_t *out_strides, + int num_dims, int64_t total_elements) { + int64_t out_idx = blockIdx.x * blockDim.x + threadIdx.x; + if (out_idx >= total_elements) { + return; + } + + int64_t in_index = 0; + for (int i = 0; i < num_dims; ++i) { + int64_t idx = (out_idx / out_strides[i]) % new_dims[i]; + in_index += (starts[i] + idx * steps[i]) * in_strides[i]; + } + + output[out_idx] = input[in_index]; +} + +std::shared_ptr SliceForward(const std::shared_ptr &input, const std::vector &starts, + const std::vector &ends, const std::vector &steps) { + CHECK_EQ(starts.size(), ends.size()); + CHECK_EQ(starts.size(), steps.size()); + auto &dims = input->Dims(); + CHECK_EQ(starts.size(), dims.size()); + const int64_t num_dims = dims.size(); + + std::vector new_dims; + for (int i = 0; i < starts.size(); i++) { + CHECK_LE(starts[i], ends[i]); + CHECK_LE(0, steps[i]); + new_dims.push_back((ends[i] - starts[i] + steps[i] - 1) / steps[i]); + } + + auto new_tensor = std::make_shared(new_dims, input->Dtype(), input->GetDevice()); + // NOTE(zbl): must initialize with 0 + new_tensor->Fill(0.0f); + + std::vector src_strides(dims.size(), 0), dst_strides(new_dims.size(), 0); + int64_t stride = 1; + for (int i = dims.size() - 1; i >= 0; --i) { + src_strides[i] = stride; + stride *= dims[i]; + } + + stride = 1; + for (int i = new_dims.size() - 1; i >= 0; --i) { + dst_strides[i] = stride; + stride *= new_dims[i]; + } + + int64_t total_elements = stride; + + int64_t *new_dims_dev, *starts_dev, *steps_dev, *input_strides_dev, *output_strides_dev; + + cudaMallocAsync(&new_dims_dev, + (ends.size() + starts.size() + steps.size() + dims.size() + new_dims.size()) * sizeof(int64_t), 0); + starts_dev = new_dims_dev + ends.size(); + steps_dev = starts_dev + starts.size(); + input_strides_dev = steps_dev + steps.size(); + output_strides_dev = input_strides_dev + dims.size(); + + cudaMemcpyAsync(new_dims_dev, new_dims.data(), ends.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); + cudaMemcpyAsync(starts_dev, starts.data(), starts.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); + cudaMemcpyAsync(steps_dev, steps.data(), steps.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); + cudaMemcpyAsync(input_strides_dev, src_strides.data(), dims.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); + cudaMemcpyAsync(output_strides_dev, dst_strides.data(), new_dims.size() * sizeof(int64_t), cudaMemcpyHostToDevice, + 0); + + int threads_per_block = 256; + int num_blocks = (total_elements + threads_per_block - 1) / threads_per_block; + + SliceForwardKernel<<>>( + static_cast(input->DataPtr()), static_cast(new_tensor->DataPtr()), new_dims_dev, + starts_dev, steps_dev, input_strides_dev, output_strides_dev, num_dims, total_elements); + + cudaFreeAsync(new_dims_dev, 0); + + return new_tensor; +} + +__global__ void SliceBackwardKernel(const float *grad_output, float *grad_input, const int64_t *new_dims, + const int64_t *starts, const int64_t *steps, const int64_t *in_strides, + const int64_t *out_strides, int num_dims, int64_t total_elements) { + int64_t out_idx = blockIdx.x * blockDim.x + threadIdx.x; + if (out_idx >= total_elements) { + return; + } + + int64_t in_index = 0; + for (int i = 0; i < num_dims; ++i) { + int64_t idx = (out_idx / out_strides[i]) % new_dims[i]; + in_index += (starts[i] + idx * steps[i]) * in_strides[i]; + } + atomicAdd(&grad_input[in_index], grad_output[out_idx]); +} + +std::shared_ptr SliceBackward(const std::shared_ptr &grad_output, const std::shared_ptr &input, + const std::vector &starts, const std::vector &ends, + const std::vector &steps) { + CHECK_EQ(starts.size(), ends.size()); + CHECK_EQ(starts.size(), steps.size()); + auto &dims = input->Dims(); + CHECK_EQ(starts.size(), dims.size()); + const int64_t num_dims = dims.size(); + + std::vector new_dims; + for (int i = 0; i < starts.size(); i++) { + CHECK_LE(starts[i], ends[i]); + CHECK_LE(0, steps[i]); + new_dims.push_back((ends[i] - starts[i] + steps[i] - 1) / steps[i]); + } + + auto grad_input = std::make_shared(input->Dims(), input->Dtype(), grad_output->GetDevice()); + grad_input->Fill(0.0); + + std::vector src_strides(dims.size()); + int64_t stride = 1; + for (int i = src_strides.size() - 1; i >= 0; --i) { + src_strides[i] = stride; + stride *= dims[i]; + } + + std::vector dst_strides(new_dims.size()); + stride = 1; + for (int i = dst_strides.size() - 1; i >= 0; --i) { + dst_strides[i] = stride; + stride *= new_dims[i]; + } + + int64_t total_elements = stride; + + int dims_size = dims.size(); + int64_t *new_dims_dev, *starts_dev, *steps_dev, *input_strides_dev, *output_strides_dev; + + cudaMallocAsync(&new_dims_dev, + (ends.size() + starts.size() + steps.size() + dims.size() + new_dims.size()) * sizeof(int64_t), 0); + starts_dev = new_dims_dev + ends.size(); + steps_dev = starts_dev + starts.size(); + input_strides_dev = steps_dev + steps.size(); + output_strides_dev = input_strides_dev + dims.size(); + + cudaMemcpyAsync(new_dims_dev, new_dims.data(), ends.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); + cudaMemcpyAsync(starts_dev, starts.data(), starts.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); + cudaMemcpyAsync(steps_dev, steps.data(), steps.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); + cudaMemcpyAsync(input_strides_dev, src_strides.data(), dims.size() * sizeof(int64_t), cudaMemcpyHostToDevice, 0); + cudaMemcpyAsync(output_strides_dev, dst_strides.data(), new_dims.size() * sizeof(int64_t), cudaMemcpyHostToDevice, + 0); + + int threads_per_block = 256; + int num_blocks = (total_elements + threads_per_block - 1) / threads_per_block; + + SliceBackwardKernel<<>>( + static_cast(grad_output->DataPtr()), static_cast(grad_input->DataPtr()), new_dims_dev, + starts_dev, steps_dev, input_strides_dev, output_strides_dev, num_dims, total_elements); + + cudaFreeAsync(new_dims_dev, 0); + + return grad_input; +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_SLICE_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_SLICE_KERNEL(SliceForward) +REGISTER_CUDA_SLICE_KERNEL(SliceBackward) + +#undef REGISTER_CUDA_SLICE_KERNEL diff --git a/infini_train/src/kernels/cuda/softmax.cu b/infini_train/src/kernels/cuda/softmax.cu index 0f266d0..1151fd9 100644 --- a/infini_train/src/kernels/cuda/softmax.cu +++ b/infini_train/src/kernels/cuda/softmax.cu @@ -1,215 +1,215 @@ -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { - -#if defined(CUB_VERSION) && CUB_VERSION >= 200800 - #include - using CubSumOp = ::cuda::std::plus<>; - using CubMaxOp = ::cuda::maximum<>; - using CubMinOp = ::cuda::minimum<>; -#else - using CubSumOp = cub::Sum; - using CubMaxOp = cub::Max; - using CubMinOp = cub::Min; -#endif - -template -__global__ void SoftmaxForwardKernel(T *output, const T *input, int64_t outer_size, int64_t axis_size, - int64_t inner_size) { - using BlockReduce = cub::BlockReduce; - - __shared__ typename BlockReduce::TempStorage temp_storage_max; - __shared__ typename BlockReduce::TempStorage temp_storage_sum; - __shared__ T row_max; - __shared__ T row_sum; - - const int64_t group = blockIdx.x; // row of the grid - const int64_t inner_idx = blockIdx.y; // column of the grid - const int tid = threadIdx.x; - - // calculate the maximum for each group - T thread_max = -INFINITY; - for (int64_t axis = tid; axis < axis_size; axis += BLOCK_SIZE) { - int64_t idx = (group * axis_size + axis) * inner_size + inner_idx; - thread_max = max(thread_max, input[idx]); - } - T block_max = BlockReduce(temp_storage_max).Reduce(thread_max, CubMaxOp()); - - if (tid == 0) { - row_max = block_max; - } - __syncthreads(); - - // calculate the sum of exponents - T thread_sum = 0; - for (int64_t axis = tid; axis < axis_size; axis += BLOCK_SIZE) { - int64_t idx = (group * axis_size + axis) * inner_size + inner_idx; - T exp_val = exp(input[idx] - row_max); - output[idx] = exp_val; - thread_sum += exp_val; - } - T block_sum = BlockReduce(temp_storage_sum).Sum(thread_sum); - - if (tid == 0) { - row_sum = block_sum; - } - __syncthreads(); - - // normalize - for (int64_t axis = tid; axis < axis_size; axis += BLOCK_SIZE) { - int64_t idx = (group * axis_size + axis) * inner_size + inner_idx; - output[idx] /= row_sum; - } -} - -template -void LaunchForward(const std::shared_ptr &output, const std::shared_ptr &input, int64_t dim) { - const auto &input_dims = input->Dims(); - int64_t outer_size = 1; - int64_t axis_size = input_dims[dim]; - int64_t inner_size = 1; - - for (int i = 0; i < dim; ++i) { outer_size *= input_dims[i]; }; - for (int i = dim + 1; i < input_dims.size(); ++i) { inner_size *= input_dims[i]; }; - if (axis_size == 0) { - LOG(INFO) << "CUDA softmax forward: 'input_dims[dim] == 0' at " << __FILE__ << ":" << __LINE__; - return; - } - if (outer_size == 0) { - return; - } - - T *output_ptr = static_cast(output->DataPtr()); - const T *input_ptr = static_cast(input->DataPtr()); - - if (BLOCK_SIZE > 1024) { - LOG(FATAL) << "CUDA softmax forward: 'BLOCK_SIZE used is larger than the max number of thread per block' at " - << __FILE__ << ":" << __LINE__; - } - dim3 block_dims(BLOCK_SIZE); - dim3 grid_dims(outer_size, inner_size); - - SoftmaxForwardKernel - <<>>(output_ptr, input_ptr, outer_size, axis_size, inner_size); -} - -std::shared_ptr SoftmaxForward(const std::shared_ptr &input, int64_t dim) { - auto dtype = input->Dtype(); - const auto &input_dims = input->Dims(); - dim = dim < 0 ? dim + input_dims.size() : dim; - CHECK(dim >= 0 && dim < input_dims.size()); - auto output = std::make_shared(input_dims, dtype, input->GetDevice()); - - switch (dtype) { - case DataType::kFLOAT32: - LaunchForward<256, float>(output, input, dim); - break; - default: - LOG(FATAL) << "CUDA softmax forward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; - } - return output; -} - -template -__global__ void SoftmaxBackwardKernel(T *grad_input, const T *grad_output, const T *output, int64_t outer_size, - int64_t axis_size, int64_t inner_size) { - using BlockReduce = cub::BlockReduce; - - __shared__ typename BlockReduce::TempStorage temp_storage_sum; - __shared__ T row_sum; - - const int64_t group = blockIdx.x; - const int64_t inner_idx = blockIdx.y; - const int tid = threadIdx.x; - - // calculate the sum of the dot product of gradients - T thread_sum = 0; - for (int64_t axis = tid; axis < axis_size; axis += BLOCK_SIZE) { - const int64_t idx = (group * axis_size + axis) * inner_size + inner_idx; - thread_sum += grad_output[idx] * output[idx]; - } - T block_sum = BlockReduce(temp_storage_sum).Sum(thread_sum); - - if (tid == 0) { - row_sum = block_sum; - } - __syncthreads(); - - // update the input gradient - for (int64_t axis = tid; axis < axis_size; axis += BLOCK_SIZE) { - const int64_t idx = (group * axis_size + axis) * inner_size + inner_idx; - grad_input[idx] = output[idx] * (grad_output[idx] - row_sum); - } -} - -template -void LaunchBackward(const std::shared_ptr &grad_input, const std::shared_ptr &grad_output, - const std::shared_ptr &output, int64_t dim) { - const auto &output_dims = output->Dims(); - int64_t outer_size = 1; - int64_t axis_size = output_dims[dim]; - int64_t inner_size = 1; - - for (int i = 0; i < dim; ++i) { outer_size *= output_dims[i]; }; - for (int i = dim + 1; i < output_dims.size(); ++i) { inner_size *= output_dims[i]; }; - if (axis_size == 0) { - LOG(INFO) << "CUDA softmax backward: 'output_dims[dim] == 0' at " << __FILE__ << ":" << __LINE__; - return; - } - if (outer_size == 0) { - return; - } - - T *grad_input_ptr = static_cast(grad_input->DataPtr()); - const T *grad_output_ptr = static_cast(grad_output->DataPtr()); - const T *output_ptr = static_cast(output->DataPtr()); - - if (BLOCK_SIZE > 1024) { - LOG(FATAL) << "CUDA softmax backward: 'BLOCK_SIZE used is larger than the max number of thread per block' at " - << __FILE__ << ":" << __LINE__; - } - dim3 block(BLOCK_SIZE); - dim3 grid(outer_size, inner_size); - - SoftmaxBackwardKernel - <<>>(grad_input_ptr, grad_output_ptr, output_ptr, outer_size, axis_size, inner_size); -} - -std::shared_ptr SoftmaxBackward(const std::shared_ptr &grad_output, - const std::shared_ptr &output, int64_t dim) { - auto dtype = output->Dtype(); - const auto &output_dims = output->Dims(); - dim = dim < 0 ? dim + output->Dims().size() : dim; - CHECK(dim >= 0 && dim < output->Dims().size()); - - auto grad_input = std::make_shared(output_dims, dtype, output->GetDevice()); - grad_input->Fill(0.0f); - - switch (dtype) { - case DataType::kFLOAT32: - LaunchBackward<256, float>(grad_input, grad_output, output, dim); - break; - default: - LOG(FATAL) << "CUDA softmax backward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; - } - - return grad_input; -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_SOFTMAX_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_SOFTMAX_KERNEL(SoftmaxForward) -REGISTER_CUDA_SOFTMAX_KERNEL(SoftmaxBackward) - -#undef REGISTER_CUDA_SOFTMAX_KERNEL +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { + +#if defined(CUB_VERSION) && CUB_VERSION >= 200800 + #include + using CubSumOp = ::cuda::std::plus<>; + using CubMaxOp = ::cuda::maximum<>; + using CubMinOp = ::cuda::minimum<>; +#else + using CubSumOp = cub::Sum; + using CubMaxOp = cub::Max; + using CubMinOp = cub::Min; +#endif + +template +__global__ void SoftmaxForwardKernel(T *output, const T *input, int64_t outer_size, int64_t axis_size, + int64_t inner_size) { + using BlockReduce = cub::BlockReduce; + + __shared__ typename BlockReduce::TempStorage temp_storage_max; + __shared__ typename BlockReduce::TempStorage temp_storage_sum; + __shared__ T row_max; + __shared__ T row_sum; + + const int64_t group = blockIdx.x; // row of the grid + const int64_t inner_idx = blockIdx.y; // column of the grid + const int tid = threadIdx.x; + + // calculate the maximum for each group + T thread_max = -INFINITY; + for (int64_t axis = tid; axis < axis_size; axis += BLOCK_SIZE) { + int64_t idx = (group * axis_size + axis) * inner_size + inner_idx; + thread_max = max(thread_max, input[idx]); + } + T block_max = BlockReduce(temp_storage_max).Reduce(thread_max, CubMaxOp()); + + if (tid == 0) { + row_max = block_max; + } + __syncthreads(); + + // calculate the sum of exponents + T thread_sum = 0; + for (int64_t axis = tid; axis < axis_size; axis += BLOCK_SIZE) { + int64_t idx = (group * axis_size + axis) * inner_size + inner_idx; + T exp_val = exp(input[idx] - row_max); + output[idx] = exp_val; + thread_sum += exp_val; + } + T block_sum = BlockReduce(temp_storage_sum).Sum(thread_sum); + + if (tid == 0) { + row_sum = block_sum; + } + __syncthreads(); + + // normalize + for (int64_t axis = tid; axis < axis_size; axis += BLOCK_SIZE) { + int64_t idx = (group * axis_size + axis) * inner_size + inner_idx; + output[idx] /= row_sum; + } +} + +template +void LaunchForward(const std::shared_ptr &output, const std::shared_ptr &input, int64_t dim) { + const auto &input_dims = input->Dims(); + int64_t outer_size = 1; + int64_t axis_size = input_dims[dim]; + int64_t inner_size = 1; + + for (int i = 0; i < dim; ++i) { outer_size *= input_dims[i]; }; + for (int i = dim + 1; i < input_dims.size(); ++i) { inner_size *= input_dims[i]; }; + if (axis_size == 0) { + LOG(INFO) << "CUDA softmax forward: 'input_dims[dim] == 0' at " << __FILE__ << ":" << __LINE__; + return; + } + if (outer_size == 0) { + return; + } + + T *output_ptr = static_cast(output->DataPtr()); + const T *input_ptr = static_cast(input->DataPtr()); + + if (BLOCK_SIZE > 1024) { + LOG(FATAL) << "CUDA softmax forward: 'BLOCK_SIZE used is larger than the max number of thread per block' at " + << __FILE__ << ":" << __LINE__; + } + dim3 block_dims(BLOCK_SIZE); + dim3 grid_dims(outer_size, inner_size); + + SoftmaxForwardKernel + <<>>(output_ptr, input_ptr, outer_size, axis_size, inner_size); +} + +std::shared_ptr SoftmaxForward(const std::shared_ptr &input, int64_t dim) { + auto dtype = input->Dtype(); + const auto &input_dims = input->Dims(); + dim = dim < 0 ? dim + input_dims.size() : dim; + CHECK(dim >= 0 && dim < input_dims.size()); + auto output = std::make_shared(input_dims, dtype, input->GetDevice()); + + switch (dtype) { + case DataType::kFLOAT32: + LaunchForward<256, float>(output, input, dim); + break; + default: + LOG(FATAL) << "CUDA softmax forward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; + } + return output; +} + +template +__global__ void SoftmaxBackwardKernel(T *grad_input, const T *grad_output, const T *output, int64_t outer_size, + int64_t axis_size, int64_t inner_size) { + using BlockReduce = cub::BlockReduce; + + __shared__ typename BlockReduce::TempStorage temp_storage_sum; + __shared__ T row_sum; + + const int64_t group = blockIdx.x; + const int64_t inner_idx = blockIdx.y; + const int tid = threadIdx.x; + + // calculate the sum of the dot product of gradients + T thread_sum = 0; + for (int64_t axis = tid; axis < axis_size; axis += BLOCK_SIZE) { + const int64_t idx = (group * axis_size + axis) * inner_size + inner_idx; + thread_sum += grad_output[idx] * output[idx]; + } + T block_sum = BlockReduce(temp_storage_sum).Sum(thread_sum); + + if (tid == 0) { + row_sum = block_sum; + } + __syncthreads(); + + // update the input gradient + for (int64_t axis = tid; axis < axis_size; axis += BLOCK_SIZE) { + const int64_t idx = (group * axis_size + axis) * inner_size + inner_idx; + grad_input[idx] = output[idx] * (grad_output[idx] - row_sum); + } +} + +template +void LaunchBackward(const std::shared_ptr &grad_input, const std::shared_ptr &grad_output, + const std::shared_ptr &output, int64_t dim) { + const auto &output_dims = output->Dims(); + int64_t outer_size = 1; + int64_t axis_size = output_dims[dim]; + int64_t inner_size = 1; + + for (int i = 0; i < dim; ++i) { outer_size *= output_dims[i]; }; + for (int i = dim + 1; i < output_dims.size(); ++i) { inner_size *= output_dims[i]; }; + if (axis_size == 0) { + LOG(INFO) << "CUDA softmax backward: 'output_dims[dim] == 0' at " << __FILE__ << ":" << __LINE__; + return; + } + if (outer_size == 0) { + return; + } + + T *grad_input_ptr = static_cast(grad_input->DataPtr()); + const T *grad_output_ptr = static_cast(grad_output->DataPtr()); + const T *output_ptr = static_cast(output->DataPtr()); + + if (BLOCK_SIZE > 1024) { + LOG(FATAL) << "CUDA softmax backward: 'BLOCK_SIZE used is larger than the max number of thread per block' at " + << __FILE__ << ":" << __LINE__; + } + dim3 block(BLOCK_SIZE); + dim3 grid(outer_size, inner_size); + + SoftmaxBackwardKernel + <<>>(grad_input_ptr, grad_output_ptr, output_ptr, outer_size, axis_size, inner_size); +} + +std::shared_ptr SoftmaxBackward(const std::shared_ptr &grad_output, + const std::shared_ptr &output, int64_t dim) { + auto dtype = output->Dtype(); + const auto &output_dims = output->Dims(); + dim = dim < 0 ? dim + output->Dims().size() : dim; + CHECK(dim >= 0 && dim < output->Dims().size()); + + auto grad_input = std::make_shared(output_dims, dtype, output->GetDevice()); + grad_input->Fill(0.0f); + + switch (dtype) { + case DataType::kFLOAT32: + LaunchBackward<256, float>(grad_input, grad_output, output, dim); + break; + default: + LOG(FATAL) << "CUDA softmax backward: 'Unsupported data type' at " << __FILE__ << ":" << __LINE__; + } + + return grad_input; +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_SOFTMAX_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_SOFTMAX_KERNEL(SoftmaxForward) +REGISTER_CUDA_SOFTMAX_KERNEL(SoftmaxBackward) + +#undef REGISTER_CUDA_SOFTMAX_KERNEL diff --git a/infini_train/src/kernels/cuda/split.cu b/infini_train/src/kernels/cuda/split.cu index 9474229..760c5bf 100644 --- a/infini_train/src/kernels/cuda/split.cu +++ b/infini_train/src/kernels/cuda/split.cu @@ -1,144 +1,144 @@ -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { -__global__ void SplitForwardKernel(const float *input, float *output, int64_t N, int64_t H_in, int64_t H_out, int64_t W, - int64_t start_idx) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - int total = N * H_out * W; - - if (idx < total) { - int w = idx % W; - int h = (idx / W) % H_out; - int n = idx / (H_out * W); - - int input_h = h + start_idx; - int input_idx = n * H_in * W + input_h * W + w; - int output_idx = n * H_out * W + h * W + w; - - output[output_idx] = input[input_idx]; - } -} - -std::vector> SplitForward(const std::shared_ptr &input, int64_t split_size, int dim) { - CHECK_GT(split_size, 0); - CHECK_GE(dim, 0) << "Currently we do not support negative dimension"; - const auto &input_dims = input->Dims(); - CHECK_LT(dim, input_dims.size()); - - std::vector> outputs; - - const int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); - const int64_t W = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); - const int64_t H_in = input_dims[dim]; - - for (int64_t start = 0; start < H_in; start += split_size) { - auto output_dims = input_dims; - const int64_t H_out = std::min(split_size, H_in - start); - output_dims[dim] = H_out; - - auto output = std::make_shared(output_dims, DataType::kFLOAT32, input->GetDevice()); - - int64_t total = N * H_out * W; - int threads_per_block = 256; - int num_blocks = (total + threads_per_block - 1) / threads_per_block; - - SplitForwardKernel<<>>(static_cast(input->DataPtr()), - static_cast(output->DataPtr()), N, H_in, H_out, - W, start); - outputs.push_back(std::move(output)); - } - - return outputs; -} - -__global__ void SplitBackwardKernel(const float *const *grad_outputs, float *grad_input, int64_t N, int64_t H_in, - int64_t W, int64_t split_size, int64_t num_splits, const int64_t *H_outs) { - int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; - int64_t total = N * H_in * W; - if (idx >= total) { - return; - } - - int64_t w = idx % W; - int64_t h = (idx / W) % H_in; - int64_t n = idx / (H_in * W); - - int64_t split_idx = h / split_size; - if (split_idx >= num_splits) { - return; - } - - int64_t H_out = H_outs[split_idx]; - int64_t local_h = h - split_idx * split_size; - - if (local_h >= H_out) { - return; - } - - const float *grad_output = grad_outputs[split_idx]; - float value = grad_output[(n * H_out + local_h) * W + w]; - grad_input[(n * H_in + h) * W + w] = value; -} - -std::shared_ptr SplitBackward(const std::vector &input_dims, int64_t split_size, int dim, - const std::vector> &grad_outputs) { - CHECK_GT(split_size, 0); - CHECK_GE(dim, 0) << "Currently we do not support negative dimension"; - CHECK_LT(dim, input_dims.size()); - - auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32, grad_outputs[0]->GetDevice()); - grad_input->Fill(0.0f); - int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); - int64_t W = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); - int64_t H_in = input_dims[dim]; - int64_t num_splits = grad_outputs.size(); - - // init the array of grad_output ptrs - std::vector host_grad_output_ptrs; - for (const auto &grad_output : grad_outputs) { - host_grad_output_ptrs.push_back(static_cast(grad_output->DataPtr())); - } - - void *device_ptr; - const float **device_grad_output_ptrs; - int64_t *device_H_outs; - cudaMallocAsync(&device_ptr, (sizeof(float *) + sizeof(int64_t)) * num_splits, 0); - device_grad_output_ptrs = (const float **)(device_ptr); - device_H_outs = reinterpret_cast(device_grad_output_ptrs + num_splits); - - cudaMemcpyAsync(device_grad_output_ptrs, host_grad_output_ptrs.data(), sizeof(float *) * num_splits, - cudaMemcpyHostToDevice, 0); - - // init H_out for each split - std::vector H_outs(num_splits); - for (int i = 0; i < num_splits; ++i) { H_outs[i] = std::min(split_size, H_in - i * split_size); } - - cudaMemcpyAsync(device_H_outs, H_outs.data(), sizeof(int64_t) * num_splits, cudaMemcpyHostToDevice, 0); - - int64_t total_elements = N * H_in * W; - int threads_per_block = 256; - int num_blocks = (total_elements + threads_per_block - 1) / threads_per_block; - - SplitBackwardKernel<<>>(device_grad_output_ptrs, - static_cast(grad_input->DataPtr()), N, H_in, W, - split_size, num_splits, device_H_outs); - - cudaFreeAsync(device_ptr, 0); - return grad_input; -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_SPLIT_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_SPLIT_KERNEL(SplitForward) -REGISTER_CUDA_SPLIT_KERNEL(SplitBackward) - -#undef REGISTER_CUDA_SPLIT_KERNEL +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { +__global__ void SplitForwardKernel(const float *input, float *output, int64_t N, int64_t H_in, int64_t H_out, int64_t W, + int64_t start_idx) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + int total = N * H_out * W; + + if (idx < total) { + int w = idx % W; + int h = (idx / W) % H_out; + int n = idx / (H_out * W); + + int input_h = h + start_idx; + int input_idx = n * H_in * W + input_h * W + w; + int output_idx = n * H_out * W + h * W + w; + + output[output_idx] = input[input_idx]; + } +} + +std::vector> SplitForward(const std::shared_ptr &input, int64_t split_size, int dim) { + CHECK_GT(split_size, 0); + CHECK_GE(dim, 0) << "Currently we do not support negative dimension"; + const auto &input_dims = input->Dims(); + CHECK_LT(dim, input_dims.size()); + + std::vector> outputs; + + const int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); + const int64_t W = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); + const int64_t H_in = input_dims[dim]; + + for (int64_t start = 0; start < H_in; start += split_size) { + auto output_dims = input_dims; + const int64_t H_out = std::min(split_size, H_in - start); + output_dims[dim] = H_out; + + auto output = std::make_shared(output_dims, DataType::kFLOAT32, input->GetDevice()); + + int64_t total = N * H_out * W; + int threads_per_block = 256; + int num_blocks = (total + threads_per_block - 1) / threads_per_block; + + SplitForwardKernel<<>>(static_cast(input->DataPtr()), + static_cast(output->DataPtr()), N, H_in, H_out, + W, start); + outputs.push_back(std::move(output)); + } + + return outputs; +} + +__global__ void SplitBackwardKernel(const float *const *grad_outputs, float *grad_input, int64_t N, int64_t H_in, + int64_t W, int64_t split_size, int64_t num_splits, const int64_t *H_outs) { + int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; + int64_t total = N * H_in * W; + if (idx >= total) { + return; + } + + int64_t w = idx % W; + int64_t h = (idx / W) % H_in; + int64_t n = idx / (H_in * W); + + int64_t split_idx = h / split_size; + if (split_idx >= num_splits) { + return; + } + + int64_t H_out = H_outs[split_idx]; + int64_t local_h = h - split_idx * split_size; + + if (local_h >= H_out) { + return; + } + + const float *grad_output = grad_outputs[split_idx]; + float value = grad_output[(n * H_out + local_h) * W + w]; + grad_input[(n * H_in + h) * W + w] = value; +} + +std::shared_ptr SplitBackward(const std::vector &input_dims, int64_t split_size, int dim, + const std::vector> &grad_outputs) { + CHECK_GT(split_size, 0); + CHECK_GE(dim, 0) << "Currently we do not support negative dimension"; + CHECK_LT(dim, input_dims.size()); + + auto grad_input = std::make_shared(input_dims, DataType::kFLOAT32, grad_outputs[0]->GetDevice()); + grad_input->Fill(0.0f); + int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); + int64_t W = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); + int64_t H_in = input_dims[dim]; + int64_t num_splits = grad_outputs.size(); + + // init the array of grad_output ptrs + std::vector host_grad_output_ptrs; + for (const auto &grad_output : grad_outputs) { + host_grad_output_ptrs.push_back(static_cast(grad_output->DataPtr())); + } + + void *device_ptr; + const float **device_grad_output_ptrs; + int64_t *device_H_outs; + cudaMallocAsync(&device_ptr, (sizeof(float *) + sizeof(int64_t)) * num_splits, 0); + device_grad_output_ptrs = (const float **)(device_ptr); + device_H_outs = reinterpret_cast(device_grad_output_ptrs + num_splits); + + cudaMemcpyAsync(device_grad_output_ptrs, host_grad_output_ptrs.data(), sizeof(float *) * num_splits, + cudaMemcpyHostToDevice, 0); + + // init H_out for each split + std::vector H_outs(num_splits); + for (int i = 0; i < num_splits; ++i) { H_outs[i] = std::min(split_size, H_in - i * split_size); } + + cudaMemcpyAsync(device_H_outs, H_outs.data(), sizeof(int64_t) * num_splits, cudaMemcpyHostToDevice, 0); + + int64_t total_elements = N * H_in * W; + int threads_per_block = 256; + int num_blocks = (total_elements + threads_per_block - 1) / threads_per_block; + + SplitBackwardKernel<<>>(device_grad_output_ptrs, + static_cast(grad_input->DataPtr()), N, H_in, W, + split_size, num_splits, device_H_outs); + + cudaFreeAsync(device_ptr, 0); + return grad_input; +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_SPLIT_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_SPLIT_KERNEL(SplitForward) +REGISTER_CUDA_SPLIT_KERNEL(SplitBackward) + +#undef REGISTER_CUDA_SPLIT_KERNEL diff --git a/infini_train/src/kernels/cuda/stack.cu b/infini_train/src/kernels/cuda/stack.cu index e2c8d25..90e61c0 100644 --- a/infini_train/src/kernels/cuda/stack.cu +++ b/infini_train/src/kernels/cuda/stack.cu @@ -1,138 +1,138 @@ -#include -#include -#include -#include -#include - -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { - -#define CUDA_CHECK(call) \ - do { \ - cudaError_t status = call; \ - if (status != cudaSuccess) { \ - LOG(FATAL) << "CUDA Error: " << cudaGetErrorString(status) << " at " << __FILE__ << ":" << __LINE__; \ - } \ - } while (0) - -__global__ void StackForwardKernel(const float **inputs, float *output, int64_t N, int64_t D, int64_t num_inputs) { - int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; - int64_t total = N * num_inputs * D; - - if (idx >= total) { - return; - } - - int64_t d = idx % D; - int64_t s = (idx / D) % num_inputs; - int64_t n = idx / (D * num_inputs); - - const float *input = inputs[s]; - output[idx] = input[n * D + d]; -} - -std::shared_ptr StackForward(const std::vector> &inputs, int64_t dim) { - CHECK(!inputs.empty()); - - const auto &base_dims = inputs[0]->Dims(); - if (dim < 0) { - dim += base_dims.size() + 1; - } - CHECK_GE(dim, 0); - CHECK_LE(dim, base_dims.size()); - for (const auto &input : inputs) { CHECK(input->Dims() == base_dims); } - - std::vector out_dims = base_dims; - out_dims.insert(out_dims.begin() + dim, inputs.size()); - auto output = std::make_shared(out_dims, DataType::kFLOAT32, inputs[0]->GetDevice()); - - const int64_t N = std::accumulate(base_dims.begin(), base_dims.begin() + dim, 1, std::multiplies()); - const int64_t D = std::accumulate(base_dims.begin() + dim, base_dims.end(), 1, std::multiplies()); - const int64_t num_inputs = inputs.size(); - - std::vector host_input_ptrs; - for (const auto &t : inputs) { host_input_ptrs.push_back(static_cast(t->DataPtr())); } - - const float **device_input_ptrs; - cudaMallocAsync(&device_input_ptrs, sizeof(float *) * num_inputs, 0); - cudaMemcpyAsync(device_input_ptrs, host_input_ptrs.data(), sizeof(float *) * num_inputs, cudaMemcpyHostToDevice, 0); - - int64_t total = N * num_inputs * D; - int threads_per_block = 256; - int num_blocks = (total + threads_per_block - 1) / threads_per_block; - StackForwardKernel<<>>(device_input_ptrs, static_cast(output->DataPtr()), N, - D, num_inputs); - - cudaFreeAsync(device_input_ptrs, 0); - return output; -} - -__global__ void StackBackwardKernel(const float *grad_output, float **grad_inputs, int64_t N, int64_t D, - int64_t num_inputs) { - int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; - int64_t total = N * num_inputs * D; - - if (idx >= total) { - return; - } - - int64_t d = idx % D; - int64_t s = (idx / D) % num_inputs; - int64_t n = idx / (D * num_inputs); - - if (s < num_inputs) { - grad_inputs[s][n * D + d] = grad_output[idx]; - } -} - -std::vector> StackBackward(const std::vector &input_dims, int64_t dim, - const std::shared_ptr &grad_output) { - if (dim < 0) { - dim += input_dims.size() + 1; - } - const int64_t num_inputs = grad_output->Dims()[dim]; - std::vector base_dims = grad_output->Dims(); - base_dims.erase(base_dims.begin() + dim); - - std::vector> grads; - for (int i = 0; i < num_inputs; ++i) { - auto t = std::make_shared(base_dims, DataType::kFLOAT32, grad_output->GetDevice()); - t->Fill(0.0f); - grads.push_back(t); - } - - int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); - int64_t D = std::accumulate(input_dims.begin() + dim, input_dims.end(), 1, std::multiplies()); - - std::vector host_ptrs; - for (auto &t : grads) { host_ptrs.push_back(static_cast(t->DataPtr())); } - - float **device_ptrs; - cudaMallocAsync(&device_ptrs, sizeof(float *) * num_inputs, 0); - cudaMemcpyAsync(device_ptrs, host_ptrs.data(), sizeof(float *) * num_inputs, cudaMemcpyHostToDevice, 0); - - int64_t total = N * num_inputs * D; - int threads_per_block = 256; - int num_blocks = (total + threads_per_block - 1) / threads_per_block; - - StackBackwardKernel<<>>(static_cast(grad_output->DataPtr()), - device_ptrs, N, D, num_inputs); - - CUDA_CHECK(cudaGetLastError()); - cudaFreeAsync(device_ptrs, 0); - return grads; -} - -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_STACK_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_STACK_KERNEL(StackForward) -REGISTER_CUDA_STACK_KERNEL(StackBackward) - -#undef REGISTER_CUDA_STACK_KERNEL +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { + +#define CUDA_CHECK(call) \ + do { \ + cudaError_t status = call; \ + if (status != cudaSuccess) { \ + LOG(FATAL) << "CUDA Error: " << cudaGetErrorString(status) << " at " << __FILE__ << ":" << __LINE__; \ + } \ + } while (0) + +__global__ void StackForwardKernel(const float **inputs, float *output, int64_t N, int64_t D, int64_t num_inputs) { + int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; + int64_t total = N * num_inputs * D; + + if (idx >= total) { + return; + } + + int64_t d = idx % D; + int64_t s = (idx / D) % num_inputs; + int64_t n = idx / (D * num_inputs); + + const float *input = inputs[s]; + output[idx] = input[n * D + d]; +} + +std::shared_ptr StackForward(const std::vector> &inputs, int64_t dim) { + CHECK(!inputs.empty()); + + const auto &base_dims = inputs[0]->Dims(); + if (dim < 0) { + dim += base_dims.size() + 1; + } + CHECK_GE(dim, 0); + CHECK_LE(dim, base_dims.size()); + for (const auto &input : inputs) { CHECK(input->Dims() == base_dims); } + + std::vector out_dims = base_dims; + out_dims.insert(out_dims.begin() + dim, inputs.size()); + auto output = std::make_shared(out_dims, DataType::kFLOAT32, inputs[0]->GetDevice()); + + const int64_t N = std::accumulate(base_dims.begin(), base_dims.begin() + dim, 1, std::multiplies()); + const int64_t D = std::accumulate(base_dims.begin() + dim, base_dims.end(), 1, std::multiplies()); + const int64_t num_inputs = inputs.size(); + + std::vector host_input_ptrs; + for (const auto &t : inputs) { host_input_ptrs.push_back(static_cast(t->DataPtr())); } + + const float **device_input_ptrs; + cudaMallocAsync(&device_input_ptrs, sizeof(float *) * num_inputs, 0); + cudaMemcpyAsync(device_input_ptrs, host_input_ptrs.data(), sizeof(float *) * num_inputs, cudaMemcpyHostToDevice, 0); + + int64_t total = N * num_inputs * D; + int threads_per_block = 256; + int num_blocks = (total + threads_per_block - 1) / threads_per_block; + StackForwardKernel<<>>(device_input_ptrs, static_cast(output->DataPtr()), N, + D, num_inputs); + + cudaFreeAsync(device_input_ptrs, 0); + return output; +} + +__global__ void StackBackwardKernel(const float *grad_output, float **grad_inputs, int64_t N, int64_t D, + int64_t num_inputs) { + int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; + int64_t total = N * num_inputs * D; + + if (idx >= total) { + return; + } + + int64_t d = idx % D; + int64_t s = (idx / D) % num_inputs; + int64_t n = idx / (D * num_inputs); + + if (s < num_inputs) { + grad_inputs[s][n * D + d] = grad_output[idx]; + } +} + +std::vector> StackBackward(const std::vector &input_dims, int64_t dim, + const std::shared_ptr &grad_output) { + if (dim < 0) { + dim += input_dims.size() + 1; + } + const int64_t num_inputs = grad_output->Dims()[dim]; + std::vector base_dims = grad_output->Dims(); + base_dims.erase(base_dims.begin() + dim); + + std::vector> grads; + for (int i = 0; i < num_inputs; ++i) { + auto t = std::make_shared(base_dims, DataType::kFLOAT32, grad_output->GetDevice()); + t->Fill(0.0f); + grads.push_back(t); + } + + int64_t N = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); + int64_t D = std::accumulate(input_dims.begin() + dim, input_dims.end(), 1, std::multiplies()); + + std::vector host_ptrs; + for (auto &t : grads) { host_ptrs.push_back(static_cast(t->DataPtr())); } + + float **device_ptrs; + cudaMallocAsync(&device_ptrs, sizeof(float *) * num_inputs, 0); + cudaMemcpyAsync(device_ptrs, host_ptrs.data(), sizeof(float *) * num_inputs, cudaMemcpyHostToDevice, 0); + + int64_t total = N * num_inputs * D; + int threads_per_block = 256; + int num_blocks = (total + threads_per_block - 1) / threads_per_block; + + StackBackwardKernel<<>>(static_cast(grad_output->DataPtr()), + device_ptrs, N, D, num_inputs); + + CUDA_CHECK(cudaGetLastError()); + cudaFreeAsync(device_ptrs, 0); + return grads; +} + +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_STACK_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_STACK_KERNEL(StackForward) +REGISTER_CUDA_STACK_KERNEL(StackBackward) + +#undef REGISTER_CUDA_STACK_KERNEL diff --git a/infini_train/src/kernels/cuda/transform.cu b/infini_train/src/kernels/cuda/transform.cu index 88467ee..45e9c67 100644 --- a/infini_train/src/kernels/cuda/transform.cu +++ b/infini_train/src/kernels/cuda/transform.cu @@ -1,402 +1,402 @@ -#include "cuda_runtime.h" -#include "glog/logging.h" - -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::kernels::cuda { - -__global__ void TrilForwardKernel(const float *input, float *output, int rows, int cols, int64_t diagonal) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx >= rows * cols) { - return; - } - - int row = idx / cols; - int col = idx % cols; - - if (row - col + diagonal >= 0) { - output[idx] = input[idx]; - } else { - output[idx] = 0.0f; - } -} - -std::shared_ptr TrilForward(const std::shared_ptr &input, int64_t diagonal) { - CHECK_EQ(input->Dims().size(), 2); - int64_t rows = input->Dims()[0]; - int64_t cols = input->Dims()[1]; - - auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); - - int threads_per_block = 256; - int num_blocks = (rows * cols + threads_per_block - 1) / threads_per_block; - - TrilForwardKernel<<>>(static_cast(input->DataPtr()), - static_cast(output->DataPtr()), rows, cols, diagonal); - return output; -} - -__global__ void TrilBackwardKernel(const float *grad_output, float *grad_input, int rows, int cols, int64_t diagonal) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx >= rows * cols) { - return; - } - - int row = idx / cols; - int col = idx % cols; - - if (row - col + diagonal >= 0) { - grad_input[idx] = grad_output[idx]; - } else { - grad_input[idx] = 0.0f; - } -} - -std::shared_ptr TrilBackward(const std::shared_ptr &grad_output, int64_t diagonal) { - int rows = grad_output->Dims()[0]; - int cols = grad_output->Dims()[1]; - - auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); - grad_input->Fill(0.0f); - - int threads_per_block = 256; - int num_blocks = (rows * cols + threads_per_block - 1) / threads_per_block; - - TrilBackwardKernel<<>>(static_cast(grad_output->DataPtr()), - static_cast(grad_input->DataPtr()), rows, cols, - diagonal); - - return grad_input; -} - -__global__ void TriuForwardKernel(const float *input, float *output, int rows, int cols, int64_t diagonal) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx >= rows * cols) { - return; - } - - int row = idx / cols; - int col = idx % cols; - - if (row - col + diagonal <= 0) { - output[idx] = input[idx]; - } else { - output[idx] = 0.0f; - } -} - -std::shared_ptr TriuForward(const std::shared_ptr &input, int64_t diagonal) { - CHECK_EQ(input->Dims().size(), 2); - int64_t rows = input->Dims()[0]; - int64_t cols = input->Dims()[1]; - - auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); - - int threads_per_block = 256; - int num_blocks = (rows * cols + threads_per_block - 1) / threads_per_block; - - TriuForwardKernel<<>>(static_cast(input->DataPtr()), - static_cast(output->DataPtr()), rows, cols, diagonal); - - return output; -} - -__global__ void TriuBackwardKernel(const float *grad_output, float *grad_input, int rows, int cols, int64_t diagonal) { - int idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx >= rows * cols) { - return; - } - - int row = idx / cols; - int col = idx % cols; - - if (row - col + diagonal <= 0) { - grad_input[idx] = grad_output[idx]; - } else { - grad_input[idx] = 0.0f; - } -} - -std::shared_ptr TriuBackward(const std::shared_ptr &grad_output, int64_t diagonal) { - int rows = grad_output->Dims()[0]; - int cols = grad_output->Dims()[1]; - - auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); - grad_input->Fill(0.0f); - - int threads_per_block = 256; - int num_blocks = (rows * cols + threads_per_block - 1) / threads_per_block; - - TriuBackwardKernel<<>>(static_cast(grad_output->DataPtr()), - static_cast(grad_input->DataPtr()), rows, cols, - diagonal); - - return grad_input; -} - -__global__ void TransposeForwardKernel(const float *input, float *output, const int64_t *in_dims, - const int64_t *in_strides, const int64_t *out_strides, int64_t ndim, - int64_t dim0, int64_t dim1, int64_t num_elements) { - int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; - if (idx >= num_elements) { - return; - } - - int64_t remaining = idx; - int64_t coords[8]; - - // 1. decode coord from output index - for (int i = 0; i < ndim; ++i) { - coords[i] = remaining / out_strides[i]; - remaining %= out_strides[i]; - } - - // 2. swap the coordinates - int64_t tmp = coords[dim0]; - coords[dim0] = coords[dim1]; - coords[dim1] = tmp; - - // 3. compute input flat index - int64_t in_flat_idx = 0; - for (int i = 0; i < ndim; ++i) { in_flat_idx += coords[i] * in_strides[i]; } - - output[idx] = input[in_flat_idx]; -} - -std::shared_ptr TransposeForward(const std::shared_ptr &input, int64_t dim0, int64_t dim1) { - CHECK_LE(input->Dims().size(), 8); - dim0 = dim0 < 0 ? dim0 + input->Dims().size() : dim0; - dim1 = dim1 < 0 ? dim1 + input->Dims().size() : dim1; - CHECK(dim0 >= 0 && dim0 < input->Dims().size() && dim1 >= 0 && dim1 < input->Dims().size()); - - auto in_dims = input->Dims(); - std::vector out_dims = in_dims; - std::swap(out_dims[dim0], out_dims[dim1]); - - auto output = std::make_shared(out_dims, input->Dtype(), input->GetDevice()); - output->Fill(0.0f); - int64_t ndim = in_dims.size(); - int64_t num_elements = output->NumElements(); - - // compute strides of in_dims and out_dims - std::vector in_strides(ndim, 1); - std::vector out_strides(ndim, 1); - for (int i = ndim - 2; i >= 0; --i) { - in_strides[i] = in_strides[i + 1] * in_dims[i + 1]; - out_strides[i] = out_strides[i + 1] * out_dims[i + 1]; - } - - // Allocate device memory for dims and strides - int64_t *device_buffer; - cudaMallocAsync(&device_buffer, 3 * ndim * sizeof(int64_t), 0); - - int64_t *in_dims_dev = device_buffer; - int64_t *in_strides_dev = device_buffer + ndim; - int64_t *out_strides_dev = device_buffer + 2 * ndim; - - std::vector host_buffer; - host_buffer.insert(host_buffer.end(), in_dims.begin(), in_dims.end()); - host_buffer.insert(host_buffer.end(), in_strides.begin(), in_strides.end()); - host_buffer.insert(host_buffer.end(), out_strides.begin(), out_strides.end()); - - cudaMemcpyAsync(device_buffer, host_buffer.data(), 3 * ndim * sizeof(int64_t), cudaMemcpyHostToDevice, 0); - - int threads_per_block = 256; - int num_blocks = (num_elements + threads_per_block - 1) / threads_per_block; - - TransposeForwardKernel<<>>( - static_cast(input->DataPtr()), static_cast(output->DataPtr()), in_dims_dev, - in_strides_dev, out_strides_dev, ndim, dim0, dim1, num_elements); - - cudaFreeAsync(device_buffer, 0); - - return output; -} - -std::shared_ptr TransposeBackward(const std::shared_ptr &grad_output, int64_t dim0, int64_t dim1) { - return TransposeForward(grad_output, dim1, dim0); -} - -__global__ void MaskForwardKernel(const float *input, const float *mask, float *output, float value, int batch_size, - int mask_size) { - int i = blockIdx.x * blockDim.x + threadIdx.x; - if (i < batch_size * mask_size) { - output[i] = (mask[i % mask_size] == 1.0f) ? value : input[i]; - } -} - -std::shared_ptr MaskForward(const std::shared_ptr &input, const std::shared_ptr &mask, - float value) { - auto input_shape = input->Dims(); - auto mask_shape = mask->Dims(); - CHECK_EQ(static_cast(input->Dtype()), static_cast(mask->Dtype())); - - int64_t input_dims = input_shape.size(); - int64_t mask_dims = mask_shape.size(); - for (int i = 0; i < mask_dims; ++i) { - int input_dim = input_shape[input_dims - mask_dims + i]; - int mask_dim = mask_shape[i]; - CHECK(input_dim == mask_dim || mask_dim == 1); - } - - int64_t mask_size = mask->NumElements(); - int64_t batch_size = input->NumElements() / mask_size; - - auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); - - int threads_per_block = 256; - int num_blocks = (input->NumElements() + threads_per_block - 1) / threads_per_block; - - MaskForwardKernel<<>>( - static_cast(input->DataPtr()), static_cast(mask->DataPtr()), - static_cast(output->DataPtr()), value, batch_size, mask_size); - return output; -} - -__global__ void MaskBackwardKernel(const float *grad_output, const float *mask, float *grad_input, int batch_size, - int mask_size) { - int i = blockIdx.x * blockDim.x + threadIdx.x; - if (i < batch_size * mask_size) { - grad_input[i] = (mask[i % mask_size] == 1.0f) ? 0.0f : grad_output[i]; - } -} - -std::shared_ptr MaskBackward(const std::shared_ptr &grad_output, const std::shared_ptr &mask) { - auto output_shape = grad_output->Dims(); - auto mask_shape = mask->Dims(); - CHECK_EQ(static_cast(grad_output->Dtype()), static_cast(mask->Dtype())); - - int64_t output_dims = output_shape.size(); - int64_t mask_dims = mask_shape.size(); - for (int i = 0; i < mask_dims; ++i) { - int out_dim = output_shape[output_dims - mask_dims + i]; - int mask_dim = mask_shape[i]; - CHECK(out_dim == mask_dim || mask_dim == 1); - } - - int64_t mask_size = mask->NumElements(); - int64_t batch_size = grad_output->NumElements() / mask_size; - - auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); - grad_input->Fill(0.0f); - - int threads_per_block = 256; - int num_blocks = (grad_output->NumElements() + threads_per_block - 1) / threads_per_block; - - MaskBackwardKernel<<>>( - static_cast(grad_output->DataPtr()), static_cast(mask->DataPtr()), - static_cast(grad_input->DataPtr()), batch_size, mask_size); - return grad_input; -} - -__global__ void RepeatInterleaveForwardKernel(const float *input, float *output, int64_t outer, int64_t dim_size, - int64_t inner, int64_t repeat) { - int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; - int64_t total = outer * dim_size * repeat * inner; - if (idx >= total) { - return; - } - - int64_t i = idx / inner; - int64_t j = idx % inner; - - int64_t o = i / (dim_size * repeat); - int64_t di = (i / repeat) % dim_size; - - output[idx] = input[(o * dim_size + di) * inner + j]; -} - -std::shared_ptr RepeatInterleaveForward(const std::shared_ptr &input, int64_t repeat, int64_t dim) { - CHECK_GT(repeat, 0); - CHECK_GE(dim, 0); - CHECK_LT(dim, input->Dims().size()); - - const auto &input_dims = input->Dims(); - const int64_t outer = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); - const int64_t inner - = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); - const int64_t dim_size = input_dims[dim]; - - std::vector output_dims = input_dims; - output_dims[dim] = dim_size * repeat; - auto output = std::make_shared(output_dims, input->Dtype(), input->GetDevice()); - - const float *input_ptr = static_cast(input->DataPtr()); - float *output_ptr = static_cast(output->DataPtr()); - - int64_t total_elements = outer * dim_size * repeat * inner; - int threads_per_block = 256; - int num_blocks = (total_elements + threads_per_block - 1) / threads_per_block; - RepeatInterleaveForwardKernel<<>>(input_ptr, output_ptr, outer, dim_size, inner, - repeat); - - return output; -} - -__global__ void RepeatInterleaveBackwardKernel(const float *grad_output, float *grad_input, int64_t outer, - int64_t dim_size, int64_t inner, int64_t repeat) { - int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; - int64_t total = outer * dim_size * inner; - if (idx >= total) { - return; - } - - int64_t i = idx / inner; - int64_t j = idx % inner; - - int64_t o = i / dim_size; - int64_t di = i % dim_size; - - float sum = 0.0f; - for (int64_t r = 0; r < repeat; ++r) { - int64_t out_idx = ((o * dim_size * repeat + di * repeat + r) * inner) + j; - sum += grad_output[out_idx]; - } - grad_input[idx] = sum; -} - -std::shared_ptr RepeatInterleaveBackward(const std::shared_ptr &grad_output, - const std::vector &input_dims, int64_t dim) { - CHECK_GE(dim, 0); - CHECK_LT(dim, input_dims.size()); - - const int64_t outer = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); - const int64_t inner - = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); - const int64_t dim_size = input_dims[dim]; - - int64_t repeat = grad_output->Dims()[dim] / dim_size; - CHECK_EQ(grad_output->Dims()[dim], dim_size * repeat); - - auto grad_input = std::make_shared(input_dims, grad_output->Dtype(), grad_output->GetDevice()); - grad_input->Fill(0.0f); - - const float *grad_out_ptr = static_cast(grad_output->DataPtr()); - float *grad_in_ptr = static_cast(grad_input->DataPtr()); - - int64_t total_elements = outer * dim_size * inner; - int threads_per_block = 256; - int num_blocks = (total_elements + threads_per_block - 1) / threads_per_block; - RepeatInterleaveBackwardKernel<<>>(grad_out_ptr, grad_in_ptr, outer, dim_size, inner, - repeat); - - return grad_input; -} -} // namespace infini_train::kernels::cuda - -#define REGISTER_CUDA_TRANSFORM_KERNEL(kernel_name) \ - REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) - -REGISTER_CUDA_TRANSFORM_KERNEL(TrilForward) -REGISTER_CUDA_TRANSFORM_KERNEL(TrilBackward) -REGISTER_CUDA_TRANSFORM_KERNEL(TriuForward) -REGISTER_CUDA_TRANSFORM_KERNEL(TriuBackward) -REGISTER_CUDA_TRANSFORM_KERNEL(TransposeForward) -REGISTER_CUDA_TRANSFORM_KERNEL(TransposeBackward) -REGISTER_CUDA_TRANSFORM_KERNEL(MaskForward) -REGISTER_CUDA_TRANSFORM_KERNEL(MaskBackward) -REGISTER_CUDA_TRANSFORM_KERNEL(RepeatInterleaveForward) -REGISTER_CUDA_TRANSFORM_KERNEL(RepeatInterleaveBackward) - -#undef REGISTER_CUDA_TRANSFORM_KERNEL +#include "cuda_runtime.h" +#include "glog/logging.h" + +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::kernels::cuda { + +__global__ void TrilForwardKernel(const float *input, float *output, int rows, int cols, int64_t diagonal) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= rows * cols) { + return; + } + + int row = idx / cols; + int col = idx % cols; + + if (row - col + diagonal >= 0) { + output[idx] = input[idx]; + } else { + output[idx] = 0.0f; + } +} + +std::shared_ptr TrilForward(const std::shared_ptr &input, int64_t diagonal) { + CHECK_EQ(input->Dims().size(), 2); + int64_t rows = input->Dims()[0]; + int64_t cols = input->Dims()[1]; + + auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); + + int threads_per_block = 256; + int num_blocks = (rows * cols + threads_per_block - 1) / threads_per_block; + + TrilForwardKernel<<>>(static_cast(input->DataPtr()), + static_cast(output->DataPtr()), rows, cols, diagonal); + return output; +} + +__global__ void TrilBackwardKernel(const float *grad_output, float *grad_input, int rows, int cols, int64_t diagonal) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= rows * cols) { + return; + } + + int row = idx / cols; + int col = idx % cols; + + if (row - col + diagonal >= 0) { + grad_input[idx] = grad_output[idx]; + } else { + grad_input[idx] = 0.0f; + } +} + +std::shared_ptr TrilBackward(const std::shared_ptr &grad_output, int64_t diagonal) { + int rows = grad_output->Dims()[0]; + int cols = grad_output->Dims()[1]; + + auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); + grad_input->Fill(0.0f); + + int threads_per_block = 256; + int num_blocks = (rows * cols + threads_per_block - 1) / threads_per_block; + + TrilBackwardKernel<<>>(static_cast(grad_output->DataPtr()), + static_cast(grad_input->DataPtr()), rows, cols, + diagonal); + + return grad_input; +} + +__global__ void TriuForwardKernel(const float *input, float *output, int rows, int cols, int64_t diagonal) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= rows * cols) { + return; + } + + int row = idx / cols; + int col = idx % cols; + + if (row - col + diagonal <= 0) { + output[idx] = input[idx]; + } else { + output[idx] = 0.0f; + } +} + +std::shared_ptr TriuForward(const std::shared_ptr &input, int64_t diagonal) { + CHECK_EQ(input->Dims().size(), 2); + int64_t rows = input->Dims()[0]; + int64_t cols = input->Dims()[1]; + + auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); + + int threads_per_block = 256; + int num_blocks = (rows * cols + threads_per_block - 1) / threads_per_block; + + TriuForwardKernel<<>>(static_cast(input->DataPtr()), + static_cast(output->DataPtr()), rows, cols, diagonal); + + return output; +} + +__global__ void TriuBackwardKernel(const float *grad_output, float *grad_input, int rows, int cols, int64_t diagonal) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= rows * cols) { + return; + } + + int row = idx / cols; + int col = idx % cols; + + if (row - col + diagonal <= 0) { + grad_input[idx] = grad_output[idx]; + } else { + grad_input[idx] = 0.0f; + } +} + +std::shared_ptr TriuBackward(const std::shared_ptr &grad_output, int64_t diagonal) { + int rows = grad_output->Dims()[0]; + int cols = grad_output->Dims()[1]; + + auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); + grad_input->Fill(0.0f); + + int threads_per_block = 256; + int num_blocks = (rows * cols + threads_per_block - 1) / threads_per_block; + + TriuBackwardKernel<<>>(static_cast(grad_output->DataPtr()), + static_cast(grad_input->DataPtr()), rows, cols, + diagonal); + + return grad_input; +} + +__global__ void TransposeForwardKernel(const float *input, float *output, const int64_t *in_dims, + const int64_t *in_strides, const int64_t *out_strides, int64_t ndim, + int64_t dim0, int64_t dim1, int64_t num_elements) { + int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= num_elements) { + return; + } + + int64_t remaining = idx; + int64_t coords[8]; + + // 1. decode coord from output index + for (int i = 0; i < ndim; ++i) { + coords[i] = remaining / out_strides[i]; + remaining %= out_strides[i]; + } + + // 2. swap the coordinates + int64_t tmp = coords[dim0]; + coords[dim0] = coords[dim1]; + coords[dim1] = tmp; + + // 3. compute input flat index + int64_t in_flat_idx = 0; + for (int i = 0; i < ndim; ++i) { in_flat_idx += coords[i] * in_strides[i]; } + + output[idx] = input[in_flat_idx]; +} + +std::shared_ptr TransposeForward(const std::shared_ptr &input, int64_t dim0, int64_t dim1) { + CHECK_LE(input->Dims().size(), 8); + dim0 = dim0 < 0 ? dim0 + input->Dims().size() : dim0; + dim1 = dim1 < 0 ? dim1 + input->Dims().size() : dim1; + CHECK(dim0 >= 0 && dim0 < input->Dims().size() && dim1 >= 0 && dim1 < input->Dims().size()); + + auto in_dims = input->Dims(); + std::vector out_dims = in_dims; + std::swap(out_dims[dim0], out_dims[dim1]); + + auto output = std::make_shared(out_dims, input->Dtype(), input->GetDevice()); + output->Fill(0.0f); + int64_t ndim = in_dims.size(); + int64_t num_elements = output->NumElements(); + + // compute strides of in_dims and out_dims + std::vector in_strides(ndim, 1); + std::vector out_strides(ndim, 1); + for (int i = ndim - 2; i >= 0; --i) { + in_strides[i] = in_strides[i + 1] * in_dims[i + 1]; + out_strides[i] = out_strides[i + 1] * out_dims[i + 1]; + } + + // Allocate device memory for dims and strides + int64_t *device_buffer; + cudaMallocAsync(&device_buffer, 3 * ndim * sizeof(int64_t), 0); + + int64_t *in_dims_dev = device_buffer; + int64_t *in_strides_dev = device_buffer + ndim; + int64_t *out_strides_dev = device_buffer + 2 * ndim; + + std::vector host_buffer; + host_buffer.insert(host_buffer.end(), in_dims.begin(), in_dims.end()); + host_buffer.insert(host_buffer.end(), in_strides.begin(), in_strides.end()); + host_buffer.insert(host_buffer.end(), out_strides.begin(), out_strides.end()); + + cudaMemcpyAsync(device_buffer, host_buffer.data(), 3 * ndim * sizeof(int64_t), cudaMemcpyHostToDevice, 0); + + int threads_per_block = 256; + int num_blocks = (num_elements + threads_per_block - 1) / threads_per_block; + + TransposeForwardKernel<<>>( + static_cast(input->DataPtr()), static_cast(output->DataPtr()), in_dims_dev, + in_strides_dev, out_strides_dev, ndim, dim0, dim1, num_elements); + + cudaFreeAsync(device_buffer, 0); + + return output; +} + +std::shared_ptr TransposeBackward(const std::shared_ptr &grad_output, int64_t dim0, int64_t dim1) { + return TransposeForward(grad_output, dim1, dim0); +} + +__global__ void MaskForwardKernel(const float *input, const float *mask, float *output, float value, int batch_size, + int mask_size) { + int i = blockIdx.x * blockDim.x + threadIdx.x; + if (i < batch_size * mask_size) { + output[i] = (mask[i % mask_size] == 1.0f) ? value : input[i]; + } +} + +std::shared_ptr MaskForward(const std::shared_ptr &input, const std::shared_ptr &mask, + float value) { + auto input_shape = input->Dims(); + auto mask_shape = mask->Dims(); + CHECK_EQ(static_cast(input->Dtype()), static_cast(mask->Dtype())); + + int64_t input_dims = input_shape.size(); + int64_t mask_dims = mask_shape.size(); + for (int i = 0; i < mask_dims; ++i) { + int input_dim = input_shape[input_dims - mask_dims + i]; + int mask_dim = mask_shape[i]; + CHECK(input_dim == mask_dim || mask_dim == 1); + } + + int64_t mask_size = mask->NumElements(); + int64_t batch_size = input->NumElements() / mask_size; + + auto output = std::make_shared(input->Dims(), input->Dtype(), input->GetDevice()); + + int threads_per_block = 256; + int num_blocks = (input->NumElements() + threads_per_block - 1) / threads_per_block; + + MaskForwardKernel<<>>( + static_cast(input->DataPtr()), static_cast(mask->DataPtr()), + static_cast(output->DataPtr()), value, batch_size, mask_size); + return output; +} + +__global__ void MaskBackwardKernel(const float *grad_output, const float *mask, float *grad_input, int batch_size, + int mask_size) { + int i = blockIdx.x * blockDim.x + threadIdx.x; + if (i < batch_size * mask_size) { + grad_input[i] = (mask[i % mask_size] == 1.0f) ? 0.0f : grad_output[i]; + } +} + +std::shared_ptr MaskBackward(const std::shared_ptr &grad_output, const std::shared_ptr &mask) { + auto output_shape = grad_output->Dims(); + auto mask_shape = mask->Dims(); + CHECK_EQ(static_cast(grad_output->Dtype()), static_cast(mask->Dtype())); + + int64_t output_dims = output_shape.size(); + int64_t mask_dims = mask_shape.size(); + for (int i = 0; i < mask_dims; ++i) { + int out_dim = output_shape[output_dims - mask_dims + i]; + int mask_dim = mask_shape[i]; + CHECK(out_dim == mask_dim || mask_dim == 1); + } + + int64_t mask_size = mask->NumElements(); + int64_t batch_size = grad_output->NumElements() / mask_size; + + auto grad_input = std::make_shared(grad_output->Dims(), grad_output->Dtype(), grad_output->GetDevice()); + grad_input->Fill(0.0f); + + int threads_per_block = 256; + int num_blocks = (grad_output->NumElements() + threads_per_block - 1) / threads_per_block; + + MaskBackwardKernel<<>>( + static_cast(grad_output->DataPtr()), static_cast(mask->DataPtr()), + static_cast(grad_input->DataPtr()), batch_size, mask_size); + return grad_input; +} + +__global__ void RepeatInterleaveForwardKernel(const float *input, float *output, int64_t outer, int64_t dim_size, + int64_t inner, int64_t repeat) { + int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; + int64_t total = outer * dim_size * repeat * inner; + if (idx >= total) { + return; + } + + int64_t i = idx / inner; + int64_t j = idx % inner; + + int64_t o = i / (dim_size * repeat); + int64_t di = (i / repeat) % dim_size; + + output[idx] = input[(o * dim_size + di) * inner + j]; +} + +std::shared_ptr RepeatInterleaveForward(const std::shared_ptr &input, int64_t repeat, int64_t dim) { + CHECK_GT(repeat, 0); + CHECK_GE(dim, 0); + CHECK_LT(dim, input->Dims().size()); + + const auto &input_dims = input->Dims(); + const int64_t outer = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); + const int64_t inner + = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); + const int64_t dim_size = input_dims[dim]; + + std::vector output_dims = input_dims; + output_dims[dim] = dim_size * repeat; + auto output = std::make_shared(output_dims, input->Dtype(), input->GetDevice()); + + const float *input_ptr = static_cast(input->DataPtr()); + float *output_ptr = static_cast(output->DataPtr()); + + int64_t total_elements = outer * dim_size * repeat * inner; + int threads_per_block = 256; + int num_blocks = (total_elements + threads_per_block - 1) / threads_per_block; + RepeatInterleaveForwardKernel<<>>(input_ptr, output_ptr, outer, dim_size, inner, + repeat); + + return output; +} + +__global__ void RepeatInterleaveBackwardKernel(const float *grad_output, float *grad_input, int64_t outer, + int64_t dim_size, int64_t inner, int64_t repeat) { + int64_t idx = blockIdx.x * blockDim.x + threadIdx.x; + int64_t total = outer * dim_size * inner; + if (idx >= total) { + return; + } + + int64_t i = idx / inner; + int64_t j = idx % inner; + + int64_t o = i / dim_size; + int64_t di = i % dim_size; + + float sum = 0.0f; + for (int64_t r = 0; r < repeat; ++r) { + int64_t out_idx = ((o * dim_size * repeat + di * repeat + r) * inner) + j; + sum += grad_output[out_idx]; + } + grad_input[idx] = sum; +} + +std::shared_ptr RepeatInterleaveBackward(const std::shared_ptr &grad_output, + const std::vector &input_dims, int64_t dim) { + CHECK_GE(dim, 0); + CHECK_LT(dim, input_dims.size()); + + const int64_t outer = std::accumulate(input_dims.begin(), input_dims.begin() + dim, 1, std::multiplies()); + const int64_t inner + = std::accumulate(input_dims.begin() + dim + 1, input_dims.end(), 1, std::multiplies()); + const int64_t dim_size = input_dims[dim]; + + int64_t repeat = grad_output->Dims()[dim] / dim_size; + CHECK_EQ(grad_output->Dims()[dim], dim_size * repeat); + + auto grad_input = std::make_shared(input_dims, grad_output->Dtype(), grad_output->GetDevice()); + grad_input->Fill(0.0f); + + const float *grad_out_ptr = static_cast(grad_output->DataPtr()); + float *grad_in_ptr = static_cast(grad_input->DataPtr()); + + int64_t total_elements = outer * dim_size * inner; + int threads_per_block = 256; + int num_blocks = (total_elements + threads_per_block - 1) / threads_per_block; + RepeatInterleaveBackwardKernel<<>>(grad_out_ptr, grad_in_ptr, outer, dim_size, inner, + repeat); + + return grad_input; +} +} // namespace infini_train::kernels::cuda + +#define REGISTER_CUDA_TRANSFORM_KERNEL(kernel_name) \ + REGISTER_KERNEL(infini_train::DeviceType::kCUDA, kernel_name, infini_train::kernels::cuda::kernel_name) + +REGISTER_CUDA_TRANSFORM_KERNEL(TrilForward) +REGISTER_CUDA_TRANSFORM_KERNEL(TrilBackward) +REGISTER_CUDA_TRANSFORM_KERNEL(TriuForward) +REGISTER_CUDA_TRANSFORM_KERNEL(TriuBackward) +REGISTER_CUDA_TRANSFORM_KERNEL(TransposeForward) +REGISTER_CUDA_TRANSFORM_KERNEL(TransposeBackward) +REGISTER_CUDA_TRANSFORM_KERNEL(MaskForward) +REGISTER_CUDA_TRANSFORM_KERNEL(MaskBackward) +REGISTER_CUDA_TRANSFORM_KERNEL(RepeatInterleaveForward) +REGISTER_CUDA_TRANSFORM_KERNEL(RepeatInterleaveBackward) + +#undef REGISTER_CUDA_TRANSFORM_KERNEL diff --git a/infini_train/src/nn/functional.cc b/infini_train/src/nn/functional.cc index b01e88b..c493e2e 100644 --- a/infini_train/src/nn/functional.cc +++ b/infini_train/src/nn/functional.cc @@ -1,65 +1,65 @@ -#include "infini_train/include/nn/functional.h" - -#include -#include -#include - -#include "infini_train/include/autograd/activations.h" -#include "infini_train/include/autograd/elementwise.h" -#include "infini_train/include/autograd/misc.h" -#include "infini_train/include/autograd/reduction.h" -#include "infini_train/include/autograd/softmax.h" -#include "infini_train/include/autograd/transform.h" -#include "infini_train/include/nn/init.h" - -namespace infini_train::nn::function { -std::shared_ptr Tril(const std::shared_ptr &input, int64_t diagonal) { - return std::make_shared(diagonal)->Apply({input})[0]; -} - -std::shared_ptr Triu(const std::shared_ptr &input, int64_t diagonal) { - return std::make_shared(diagonal)->Apply({input})[0]; -} - -std::shared_ptr Ones(const std::vector size) { - auto ones = std::make_shared(size, DataType::kFLOAT32); - return init::Ones(ones); -} - -std::shared_ptr Reciprocal(const std::shared_ptr &input) { return input->Reciprocal(); } - -std::shared_ptr Sin(const std::shared_ptr &input) { return input->Sin(); } - -std::shared_ptr Cos(const std::shared_ptr &input) { return input->Cos(); } - -std::shared_ptr Tanh(const std::shared_ptr &input) { return input->Tanh(); } - -std::shared_ptr Pow(const std::shared_ptr &input, float exponent) { return input->Pow(exponent); } - -std::shared_ptr Pow(float base, const std::shared_ptr &input) { - return std::make_shared(base, true)->Apply({input})[0]; -} - -std::shared_ptr Rsqrt(const std::shared_ptr &input) { return input->Rsqrt(); } - -std::shared_ptr Mean(const std::shared_ptr &input, int64_t dim, bool keep_dim) { - return std::make_shared(dim, keep_dim)->Apply({input})[0]; -} - -std::shared_ptr Slice(const std::shared_ptr &input, const std::vector &starts, - const std::vector &ends, const std::vector &steps) { - return input->Slice(starts, ends, steps); -} - -std::shared_ptr Stack(const std::vector> &inputs, int64_t dim) { - return std::make_shared(dim)->Apply(inputs)[0]; -} - -std::shared_ptr Softmax(const std::shared_ptr &input, int64_t dim) { - return std::make_shared(dim)->Apply({input})[0]; -} - -std::shared_ptr Sigmoid(const std::shared_ptr &input) { - return std::make_shared()->Apply({input})[0]; -} -} // namespace infini_train::nn::function +#include "infini_train/include/nn/functional.h" + +#include +#include +#include + +#include "infini_train/include/autograd/activations.h" +#include "infini_train/include/autograd/elementwise.h" +#include "infini_train/include/autograd/misc.h" +#include "infini_train/include/autograd/reduction.h" +#include "infini_train/include/autograd/softmax.h" +#include "infini_train/include/autograd/transform.h" +#include "infini_train/include/nn/init.h" + +namespace infini_train::nn::function { +std::shared_ptr Tril(const std::shared_ptr &input, int64_t diagonal) { + return std::make_shared(diagonal)->Apply({input})[0]; +} + +std::shared_ptr Triu(const std::shared_ptr &input, int64_t diagonal) { + return std::make_shared(diagonal)->Apply({input})[0]; +} + +std::shared_ptr Ones(const std::vector size) { + auto ones = std::make_shared(size, DataType::kFLOAT32); + return init::Ones(ones); +} + +std::shared_ptr Reciprocal(const std::shared_ptr &input) { return input->Reciprocal(); } + +std::shared_ptr Sin(const std::shared_ptr &input) { return input->Sin(); } + +std::shared_ptr Cos(const std::shared_ptr &input) { return input->Cos(); } + +std::shared_ptr Tanh(const std::shared_ptr &input) { return input->Tanh(); } + +std::shared_ptr Pow(const std::shared_ptr &input, float exponent) { return input->Pow(exponent); } + +std::shared_ptr Pow(float base, const std::shared_ptr &input) { + return std::make_shared(base, true)->Apply({input})[0]; +} + +std::shared_ptr Rsqrt(const std::shared_ptr &input) { return input->Rsqrt(); } + +std::shared_ptr Mean(const std::shared_ptr &input, int64_t dim, bool keep_dim) { + return std::make_shared(dim, keep_dim)->Apply({input})[0]; +} + +std::shared_ptr Slice(const std::shared_ptr &input, const std::vector &starts, + const std::vector &ends, const std::vector &steps) { + return input->Slice(starts, ends, steps); +} + +std::shared_ptr Stack(const std::vector> &inputs, int64_t dim) { + return std::make_shared(dim)->Apply(inputs)[0]; +} + +std::shared_ptr Softmax(const std::shared_ptr &input, int64_t dim) { + return std::make_shared(dim)->Apply({input})[0]; +} + +std::shared_ptr Sigmoid(const std::shared_ptr &input) { + return std::make_shared()->Apply({input})[0]; +} +} // namespace infini_train::nn::function diff --git a/infini_train/src/nn/init.cc b/infini_train/src/nn/init.cc index 4c1a05b..f1aa5a6 100644 --- a/infini_train/src/nn/init.cc +++ b/infini_train/src/nn/init.cc @@ -1,249 +1,249 @@ -#include "infini_train/include/nn/init.h" - -#include -#include -#include -#include -#include -#include -#include - -#include "glog/logging.h" -#ifdef USE_CUDA -#include "cuda_runtime_api.h" -#endif - -#include "infini_train/include/tensor.h" - -namespace infini_train::nn::init { -namespace { -static std::random_device rd; -static std::mt19937 gen(rd()); -} // namespace - -std::shared_ptr Normal(const std::shared_ptr &tensor, float mean, float std, - std::optional generator) { - const int64_t num_elements = tensor->NumElements(); - std::vector buffer(num_elements); - std::normal_distribution dis(mean, std); - std::generate(buffer.begin(), buffer.end(), [&]() { return generator ? dis(generator.value()) : dis(gen); }); - - switch (tensor->GetDevice().Type()) { - case DeviceType::kCPU: { - memcpy(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float)); - break; - } -#ifdef USE_CUDA - case DeviceType::kCUDA: { - cudaMemcpyAsync(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float), cudaMemcpyHostToDevice, 0); - break; - } -#endif - default: { - LOG(FATAL) << "Unsupported device type: " << static_cast(tensor->GetDevice().Type()); - break; - } - } - return tensor; -} - -std::pair CalculateFanInAndFanOut(const std::shared_ptr &tensor) { - if (tensor->Dims().size() < 2) { - LOG(FATAL) << "Fan in and fan out can not be computed for tensor with less than 2 dimensions"; - } - const auto num_input_fmaps = tensor->Dims()[1]; - const auto num_output_fmaps = tensor->Dims()[0]; - int64_t receptive_field_size = 1; - if (tensor->Dims().size() > 2) { - receptive_field_size - *= std::accumulate(tensor->Dims().begin() + 2, tensor->Dims().end(), 1, std::multiplies()); - } - const auto fan_in = num_input_fmaps * receptive_field_size; - const auto fan_out = num_output_fmaps * receptive_field_size; - return {fan_in, fan_out}; -} - -namespace { -int64_t CalculateCorrectFan(const std::shared_ptr &tensor, KaimingMode mode) { - const auto [fan_in, fan_out] = CalculateFanInAndFanOut(tensor); - return mode == KaimingMode::kFanIn ? fan_in : fan_out; -} - -float CalculateGain(NonLinearityType nonlinearity, std::optional param = std::nullopt) { - static std::unordered_set kLinearFns = { - NonLinearityType::kLinear, NonLinearityType::kConv1D, NonLinearityType::kConv2D, - NonLinearityType::kConv3D, NonLinearityType::kConvTransposed1d, NonLinearityType::kConvTransposed2d, - NonLinearityType::kConvTransposed3d, - }; - if (kLinearFns.contains(nonlinearity) || nonlinearity == NonLinearityType::kSigmoid) { - return 1.0f; - } else if (nonlinearity == NonLinearityType::kTanh) { - return 5.0f / 3; - } else if (nonlinearity == NonLinearityType::kReLU) { - return sqrt(2.0f); - } else if (nonlinearity == NonLinearityType::kLeakyReLU) { - const float negative_slope = param ? *param : 0.01f; - return sqrt(2.0f / (1 + negative_slope * negative_slope)); - } else if (nonlinearity == NonLinearityType::kSELU) { - return 3.0f / 4; // Value found empirically (https://github.com/pytorch/pytorch/pull/50664) - } else { - LOG(FATAL) << "Unsupported non-linearity type: " << static_cast(nonlinearity); - } - return -1.0f; -} -} // namespace - -std::shared_ptr KaimingUniform(const std::shared_ptr &tensor, float a, KaimingMode mode, - NonLinearityType nonlinearity, std::optional generator) { - for (const auto dim : tensor->Dims()) { - if (dim == 0) { - LOG(WARNING) << "Initializing zero-element tensors is a no-op"; - return tensor; - } - } - const auto fan = CalculateCorrectFan(tensor, mode); - const auto gain = CalculateGain(nonlinearity, a); - const float std = gain / sqrt(fan); - const float bound = sqrt(3.0f) * std; // Calculate uniform bounds from standard deviation - return tensor->Uniform(-bound, bound, generator); -} - -std::shared_ptr Uniform(const std::shared_ptr &tensor, float a, float b, - std::optional generator) { - const int64_t num_elements = tensor->NumElements(); - std::vector buffer(num_elements); - std::uniform_real_distribution dis(a, b); - std::generate(buffer.begin(), buffer.end(), [&]() { return generator ? dis(generator.value()) : dis(gen); }); - - switch (tensor->GetDevice().Type()) { - case DeviceType::kCPU: { - memcpy(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float)); - break; - } -#ifdef USE_CUDA - case DeviceType::kCUDA: { - cudaMemcpyAsync(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float), cudaMemcpyHostToDevice, 0); - break; - } -#endif - default: { - LOG(FATAL) << "Unsupported device type: " << static_cast(tensor->GetDevice().Type()); - break; - } - } - return tensor; -} - -std::shared_ptr Ones(const std::shared_ptr &tensor) { - CHECK_EQ(static_cast(tensor->Dtype()), static_cast(DataType::kFLOAT32)); - const int64_t num_elements = tensor->NumElements(); - std::vector buffer(num_elements, 1.0f); - - switch (tensor->GetDevice().Type()) { - case DeviceType::kCPU: { - memcpy(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float)); - break; - } -#ifdef USE_CUDA - case DeviceType::kCUDA: { - cudaMemcpyAsync(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float), cudaMemcpyHostToDevice, 0); - break; - } -#endif - default: { - LOG(FATAL) << "Unsupported device type: " << static_cast(tensor->GetDevice().Type()); - break; - } - } - return tensor; -} - -std::shared_ptr Zeros(const std::shared_ptr &tensor) { - CHECK_EQ(static_cast(tensor->Dtype()), static_cast(DataType::kFLOAT32)); - const int64_t num_elements = tensor->NumElements(); - std::vector buffer(num_elements, 0.0f); - - switch (tensor->GetDevice().Type()) { - case DeviceType::kCPU: { - memcpy(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float)); - break; - } -#ifdef USE_CUDA - case DeviceType::kCUDA: { - cudaMemcpyAsync(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float), cudaMemcpyHostToDevice, 0); - break; - } -#endif - default: { - LOG(FATAL) << "Unsupported device type: " << static_cast(tensor->GetDevice().Type()); - break; - } - } - return tensor; -} - -#define CASE(DATA_TYPE, TYPE) \ - case DATA_TYPE: { \ - std::vector buffer(num_elements); \ - std::iota(buffer.begin(), buffer.end(), static_cast(start)); \ - memcpy(tensor->DataPtr(), buffer.data(), num_elements * sizeof(TYPE)); \ - break; \ - } -#define CUDA_CASE(DATA_TYPE, TYPE) \ - case DATA_TYPE: { \ - std::vector buffer(num_elements); \ - std::iota(buffer.begin(), buffer.end(), static_cast(start)); \ - cudaMemcpyAsync(tensor->DataPtr(), buffer.data(), num_elements * sizeof(TYPE), cudaMemcpyHostToDevice, 0); \ - break; \ - } - -std::shared_ptr Arange(int64_t start, int64_t end, DataType dtype, Device device) { - int64_t num_elements = end - start; - auto tensor = std::make_shared(std::vector{num_elements}, dtype, device); - if (device == Device()) { - switch (dtype) { - CASE(DataType::kUINT8, uint8_t) - CASE(DataType::kINT8, int8_t) - CASE(DataType::kUINT16, uint16_t) - CASE(DataType::kINT16, int16_t) - CASE(DataType::kUINT32, uint32_t) - CASE(DataType::kINT32, int32_t) - CASE(DataType::kUINT64, uint64_t) - CASE(DataType::kINT64, int64_t) - // CASE(DataType::kBFLOAT16, bf16) - // CASE(DataType::kFLOAT16, fp16) - CASE(DataType::kFLOAT32, float) - CASE(DataType::kFLOAT64, double) - default: - LOG(FATAL) << "Unsupported data type: " << static_cast(dtype); - break; - } - } else { -#ifdef USE_CUDA - switch (dtype) { - CUDA_CASE(DataType::kUINT8, uint8_t) - CUDA_CASE(DataType::kINT8, int8_t) - CUDA_CASE(DataType::kUINT16, uint16_t) - CUDA_CASE(DataType::kINT16, int16_t) - CUDA_CASE(DataType::kUINT32, uint32_t) - CUDA_CASE(DataType::kINT32, int32_t) - CUDA_CASE(DataType::kUINT64, uint64_t) - CUDA_CASE(DataType::kINT64, int64_t) - // CUDA_CASE(DataType::kBFLOAT16, bf16) - // CUDA_CASE(DataType::kFLOAT16, fp16) - CUDA_CASE(DataType::kFLOAT32, float) - CUDA_CASE(DataType::kFLOAT64, double) - default: - LOG(FATAL) << "Unsupported data type: " << static_cast(dtype); - break; - } -#else - LOG(FATAL) << "Unsupported device type: " << static_cast(device.Type()); -#endif - } - return tensor; -} - -#undef CASE -#undef CUDA_CASE -} // namespace infini_train::nn::init +#include "infini_train/include/nn/init.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "glog/logging.h" +#ifdef USE_CUDA +#include "cuda_runtime_api.h" +#endif + +#include "infini_train/include/tensor.h" + +namespace infini_train::nn::init { +namespace { +static std::random_device rd; +static std::mt19937 gen(rd()); +} // namespace + +std::shared_ptr Normal(const std::shared_ptr &tensor, float mean, float std, + std::optional generator) { + const int64_t num_elements = tensor->NumElements(); + std::vector buffer(num_elements); + std::normal_distribution dis(mean, std); + std::generate(buffer.begin(), buffer.end(), [&]() { return generator ? dis(generator.value()) : dis(gen); }); + + switch (tensor->GetDevice().Type()) { + case DeviceType::kCPU: { + memcpy(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float)); + break; + } +#ifdef USE_CUDA + case DeviceType::kCUDA: { + cudaMemcpyAsync(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float), cudaMemcpyHostToDevice, 0); + break; + } +#endif + default: { + LOG(FATAL) << "Unsupported device type: " << static_cast(tensor->GetDevice().Type()); + break; + } + } + return tensor; +} + +std::pair CalculateFanInAndFanOut(const std::shared_ptr &tensor) { + if (tensor->Dims().size() < 2) { + LOG(FATAL) << "Fan in and fan out can not be computed for tensor with less than 2 dimensions"; + } + const auto num_input_fmaps = tensor->Dims()[1]; + const auto num_output_fmaps = tensor->Dims()[0]; + int64_t receptive_field_size = 1; + if (tensor->Dims().size() > 2) { + receptive_field_size + *= std::accumulate(tensor->Dims().begin() + 2, tensor->Dims().end(), 1, std::multiplies()); + } + const auto fan_in = num_input_fmaps * receptive_field_size; + const auto fan_out = num_output_fmaps * receptive_field_size; + return {fan_in, fan_out}; +} + +namespace { +int64_t CalculateCorrectFan(const std::shared_ptr &tensor, KaimingMode mode) { + const auto [fan_in, fan_out] = CalculateFanInAndFanOut(tensor); + return mode == KaimingMode::kFanIn ? fan_in : fan_out; +} + +float CalculateGain(NonLinearityType nonlinearity, std::optional param = std::nullopt) { + static std::unordered_set kLinearFns = { + NonLinearityType::kLinear, NonLinearityType::kConv1D, NonLinearityType::kConv2D, + NonLinearityType::kConv3D, NonLinearityType::kConvTransposed1d, NonLinearityType::kConvTransposed2d, + NonLinearityType::kConvTransposed3d, + }; + if (kLinearFns.contains(nonlinearity) || nonlinearity == NonLinearityType::kSigmoid) { + return 1.0f; + } else if (nonlinearity == NonLinearityType::kTanh) { + return 5.0f / 3; + } else if (nonlinearity == NonLinearityType::kReLU) { + return sqrt(2.0f); + } else if (nonlinearity == NonLinearityType::kLeakyReLU) { + const float negative_slope = param ? *param : 0.01f; + return sqrt(2.0f / (1 + negative_slope * negative_slope)); + } else if (nonlinearity == NonLinearityType::kSELU) { + return 3.0f / 4; // Value found empirically (https://github.com/pytorch/pytorch/pull/50664) + } else { + LOG(FATAL) << "Unsupported non-linearity type: " << static_cast(nonlinearity); + } + return -1.0f; +} +} // namespace + +std::shared_ptr KaimingUniform(const std::shared_ptr &tensor, float a, KaimingMode mode, + NonLinearityType nonlinearity, std::optional generator) { + for (const auto dim : tensor->Dims()) { + if (dim == 0) { + LOG(WARNING) << "Initializing zero-element tensors is a no-op"; + return tensor; + } + } + const auto fan = CalculateCorrectFan(tensor, mode); + const auto gain = CalculateGain(nonlinearity, a); + const float std = gain / sqrt(fan); + const float bound = sqrt(3.0f) * std; // Calculate uniform bounds from standard deviation + return tensor->Uniform(-bound, bound, generator); +} + +std::shared_ptr Uniform(const std::shared_ptr &tensor, float a, float b, + std::optional generator) { + const int64_t num_elements = tensor->NumElements(); + std::vector buffer(num_elements); + std::uniform_real_distribution dis(a, b); + std::generate(buffer.begin(), buffer.end(), [&]() { return generator ? dis(generator.value()) : dis(gen); }); + + switch (tensor->GetDevice().Type()) { + case DeviceType::kCPU: { + memcpy(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float)); + break; + } +#ifdef USE_CUDA + case DeviceType::kCUDA: { + cudaMemcpyAsync(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float), cudaMemcpyHostToDevice, 0); + break; + } +#endif + default: { + LOG(FATAL) << "Unsupported device type: " << static_cast(tensor->GetDevice().Type()); + break; + } + } + return tensor; +} + +std::shared_ptr Ones(const std::shared_ptr &tensor) { + CHECK_EQ(static_cast(tensor->Dtype()), static_cast(DataType::kFLOAT32)); + const int64_t num_elements = tensor->NumElements(); + std::vector buffer(num_elements, 1.0f); + + switch (tensor->GetDevice().Type()) { + case DeviceType::kCPU: { + memcpy(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float)); + break; + } +#ifdef USE_CUDA + case DeviceType::kCUDA: { + cudaMemcpyAsync(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float), cudaMemcpyHostToDevice, 0); + break; + } +#endif + default: { + LOG(FATAL) << "Unsupported device type: " << static_cast(tensor->GetDevice().Type()); + break; + } + } + return tensor; +} + +std::shared_ptr Zeros(const std::shared_ptr &tensor) { + CHECK_EQ(static_cast(tensor->Dtype()), static_cast(DataType::kFLOAT32)); + const int64_t num_elements = tensor->NumElements(); + std::vector buffer(num_elements, 0.0f); + + switch (tensor->GetDevice().Type()) { + case DeviceType::kCPU: { + memcpy(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float)); + break; + } +#ifdef USE_CUDA + case DeviceType::kCUDA: { + cudaMemcpyAsync(tensor->DataPtr(), buffer.data(), num_elements * sizeof(float), cudaMemcpyHostToDevice, 0); + break; + } +#endif + default: { + LOG(FATAL) << "Unsupported device type: " << static_cast(tensor->GetDevice().Type()); + break; + } + } + return tensor; +} + +#define CASE(DATA_TYPE, TYPE) \ + case DATA_TYPE: { \ + std::vector buffer(num_elements); \ + std::iota(buffer.begin(), buffer.end(), static_cast(start)); \ + memcpy(tensor->DataPtr(), buffer.data(), num_elements * sizeof(TYPE)); \ + break; \ + } +#define CUDA_CASE(DATA_TYPE, TYPE) \ + case DATA_TYPE: { \ + std::vector buffer(num_elements); \ + std::iota(buffer.begin(), buffer.end(), static_cast(start)); \ + cudaMemcpyAsync(tensor->DataPtr(), buffer.data(), num_elements * sizeof(TYPE), cudaMemcpyHostToDevice, 0); \ + break; \ + } + +std::shared_ptr Arange(int64_t start, int64_t end, DataType dtype, Device device) { + int64_t num_elements = end - start; + auto tensor = std::make_shared(std::vector{num_elements}, dtype, device); + if (device == Device()) { + switch (dtype) { + CASE(DataType::kUINT8, uint8_t) + CASE(DataType::kINT8, int8_t) + CASE(DataType::kUINT16, uint16_t) + CASE(DataType::kINT16, int16_t) + CASE(DataType::kUINT32, uint32_t) + CASE(DataType::kINT32, int32_t) + CASE(DataType::kUINT64, uint64_t) + CASE(DataType::kINT64, int64_t) + // CASE(DataType::kBFLOAT16, bf16) + // CASE(DataType::kFLOAT16, fp16) + CASE(DataType::kFLOAT32, float) + CASE(DataType::kFLOAT64, double) + default: + LOG(FATAL) << "Unsupported data type: " << static_cast(dtype); + break; + } + } else { +#ifdef USE_CUDA + switch (dtype) { + CUDA_CASE(DataType::kUINT8, uint8_t) + CUDA_CASE(DataType::kINT8, int8_t) + CUDA_CASE(DataType::kUINT16, uint16_t) + CUDA_CASE(DataType::kINT16, int16_t) + CUDA_CASE(DataType::kUINT32, uint32_t) + CUDA_CASE(DataType::kINT32, int32_t) + CUDA_CASE(DataType::kUINT64, uint64_t) + CUDA_CASE(DataType::kINT64, int64_t) + // CUDA_CASE(DataType::kBFLOAT16, bf16) + // CUDA_CASE(DataType::kFLOAT16, fp16) + CUDA_CASE(DataType::kFLOAT32, float) + CUDA_CASE(DataType::kFLOAT64, double) + default: + LOG(FATAL) << "Unsupported data type: " << static_cast(dtype); + break; + } +#else + LOG(FATAL) << "Unsupported device type: " << static_cast(device.Type()); +#endif + } + return tensor; +} + +#undef CASE +#undef CUDA_CASE +} // namespace infini_train::nn::init diff --git a/infini_train/src/nn/modules/activations.cc b/infini_train/src/nn/modules/activations.cc index 77b7ad3..80d8891 100644 --- a/infini_train/src/nn/modules/activations.cc +++ b/infini_train/src/nn/modules/activations.cc @@ -1,13 +1,13 @@ -#include "infini_train/include/nn/modules/activations.h" - -#include -#include - -#include "infini_train/include/autograd/activations.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -std::vector> Sigmoid::Forward(const std::vector> &input_tensors) { - return std::make_shared()->Apply(input_tensors); -} -} // namespace infini_train::nn +#include "infini_train/include/nn/modules/activations.h" + +#include +#include + +#include "infini_train/include/autograd/activations.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +std::vector> Sigmoid::Forward(const std::vector> &input_tensors) { + return std::make_shared()->Apply(input_tensors); +} +} // namespace infini_train::nn diff --git a/infini_train/src/nn/modules/container.cc b/infini_train/src/nn/modules/container.cc index 8e2cb3d..19fc05b 100644 --- a/infini_train/src/nn/modules/container.cc +++ b/infini_train/src/nn/modules/container.cc @@ -1,31 +1,31 @@ -#include "infini_train/include/nn/modules/container.h" - -#include -#include -#include - -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -Sequential::Sequential(std::vector> &&layers) { - int idx = 0; - for (auto &layer : layers) { - modules_[std::to_string(idx)] = std::move(layer); - ++idx; - } -} - -std::vector> Sequential::Forward(const std::vector> &input_tensors) { - auto &x = const_cast> &>(input_tensors); - for (int idx = 0; idx < modules_.size(); ++idx) { x = modules_[std::to_string(idx)]->Forward(x); } - return x; -} - -ModuleDict::ModuleDict(std::unordered_map> &&modules) { - for (auto &[name, layer] : modules) { modules_[name] = std::move(layer); } -} - -std::vector> ModuleDict::Forward(const std::vector> &input_tensors) { - LOG(FATAL) << "Not implemented"; -} -} // namespace infini_train::nn +#include "infini_train/include/nn/modules/container.h" + +#include +#include +#include + +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +Sequential::Sequential(std::vector> &&layers) { + int idx = 0; + for (auto &layer : layers) { + modules_[std::to_string(idx)] = std::move(layer); + ++idx; + } +} + +std::vector> Sequential::Forward(const std::vector> &input_tensors) { + auto &x = const_cast> &>(input_tensors); + for (int idx = 0; idx < modules_.size(); ++idx) { x = modules_[std::to_string(idx)]->Forward(x); } + return x; +} + +ModuleDict::ModuleDict(std::unordered_map> &&modules) { + for (auto &[name, layer] : modules) { modules_[name] = std::move(layer); } +} + +std::vector> ModuleDict::Forward(const std::vector> &input_tensors) { + LOG(FATAL) << "Not implemented"; +} +} // namespace infini_train::nn diff --git a/infini_train/src/nn/modules/linear.cc b/infini_train/src/nn/modules/linear.cc index b385f07..e2ace45 100644 --- a/infini_train/src/nn/modules/linear.cc +++ b/infini_train/src/nn/modules/linear.cc @@ -1,41 +1,41 @@ -#include "infini_train/include/nn/modules/linear.h" - -#include -#include -#include - -#include "infini_train/include/autograd/linear.h" -#include "infini_train/include/device.h" -#include "infini_train/include/nn/init.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -Linear::Linear(int64_t in_features, int64_t out_features, bool bias, Device device) : Module(kType), bias_(bias) { - device_ = device; - - parameters_[kParamWeightName] - = std::make_shared(std::vector{out_features, in_features}, DataType::kFLOAT32, device) - ->RequiresGrad(); - if (bias) { - parameters_[kParamBiasName] - = std::make_shared(std::vector{out_features}, DataType::kFLOAT32, device)->RequiresGrad(); - } - ResetParameters(); -} - -std::vector> Linear::Forward(const std::vector> &input_tensors) { - return std::make_shared()->Apply( - bias_ ? std::vector>{input_tensors[0], parameters_[kParamWeightName], - parameters_[kParamBiasName]} - : std::vector>{input_tensors[0], parameters_[kParamWeightName]}); -} - -void Linear::ResetParameters() { - init::KaimingUniform(parameters_[kParamWeightName], sqrt(5.0f)); - if (bias_) { - const auto [fan_in, _] = init::CalculateFanInAndFanOut(parameters_[kParamWeightName]); - const float bound = fan_in > 0 ? 1.0 / sqrt(fan_in) : 0.0; - init::Uniform(parameters_[kParamBiasName], -bound, bound); - } -} -} // namespace infini_train::nn +#include "infini_train/include/nn/modules/linear.h" + +#include +#include +#include + +#include "infini_train/include/autograd/linear.h" +#include "infini_train/include/device.h" +#include "infini_train/include/nn/init.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +Linear::Linear(int64_t in_features, int64_t out_features, bool bias, Device device) : Module(kType), bias_(bias) { + device_ = device; + + parameters_[kParamWeightName] + = std::make_shared(std::vector{out_features, in_features}, DataType::kFLOAT32, device) + ->RequiresGrad(); + if (bias) { + parameters_[kParamBiasName] + = std::make_shared(std::vector{out_features}, DataType::kFLOAT32, device)->RequiresGrad(); + } + ResetParameters(); +} + +std::vector> Linear::Forward(const std::vector> &input_tensors) { + return std::make_shared()->Apply( + bias_ ? std::vector>{input_tensors[0], parameters_[kParamWeightName], + parameters_[kParamBiasName]} + : std::vector>{input_tensors[0], parameters_[kParamWeightName]}); +} + +void Linear::ResetParameters() { + init::KaimingUniform(parameters_[kParamWeightName], sqrt(5.0f)); + if (bias_) { + const auto [fan_in, _] = init::CalculateFanInAndFanOut(parameters_[kParamWeightName]); + const float bound = fan_in > 0 ? 1.0 / sqrt(fan_in) : 0.0; + init::Uniform(parameters_[kParamBiasName], -bound, bound); + } +} +} // namespace infini_train::nn diff --git a/infini_train/src/nn/modules/loss.cc b/infini_train/src/nn/modules/loss.cc index a3f9763..ee07bd8 100644 --- a/infini_train/src/nn/modules/loss.cc +++ b/infini_train/src/nn/modules/loss.cc @@ -1,14 +1,14 @@ -#include "infini_train/include/nn/modules/loss.h" - -#include -#include - -#include "infini_train/include/autograd/loss.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -std::vector> -CrossEntropyLoss::Forward(const std::vector> &input_tensors) { - return std::make_shared()->Apply(input_tensors); -} -} // namespace infini_train::nn +#include "infini_train/include/nn/modules/loss.h" + +#include +#include + +#include "infini_train/include/autograd/loss.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +std::vector> +CrossEntropyLoss::Forward(const std::vector> &input_tensors) { + return std::make_shared()->Apply(input_tensors); +} +} // namespace infini_train::nn diff --git a/infini_train/src/nn/modules/module.cc b/infini_train/src/nn/modules/module.cc index b315075..b7d3d90 100644 --- a/infini_train/src/nn/modules/module.cc +++ b/infini_train/src/nn/modules/module.cc @@ -1,77 +1,77 @@ -#include "infini_train/include/nn/modules/module.h" - -#include -#include - -#include "infini_train/include/device.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -const std::string &Module::type() const { return type_; } - -std::vector> Module::Parameters() const { - std::vector> params; - for (auto &[_, param] : parameters_) { params.push_back(param); } - for (auto &[_, layer] : modules_) { - for (auto ¶m : layer->Parameters()) { params.push_back(param); } - } - return params; -} - -bool Module::has_parameter(const std::string &name) const { return parameters_.find(name) != parameters_.end(); } - -std::shared_ptr *Module::mutable_parameter(const std::string &name) { - CHECK(parameters_.find(name) != parameters_.end()); - return ¶meters_.at(name); -} - -const std::shared_ptr &Module::parameter(const std::string &name) const { - CHECK(parameters_.find(name) != parameters_.end()); - return parameters_.at(name); -} - -std::vector Module::modules() const { - std::vector modules; - for (auto &[_, module] : modules_) { modules.push_back(module.get()); } - return modules; -} - -Module *Module::mutable_module(const std::string &name) { - CHECK(modules_.find(name) != modules_.end()); - return modules_.at(name).get(); -} - -const Module &Module::module(const std::string &name) const { - CHECK(modules_.find(name) != modules_.end()); - return *modules_.at(name).get(); -} - -std::unordered_map> Module::StateDict() const { - std::unordered_map> state; - for (auto &[name, param] : parameters_) { state.emplace(name, param); } - for (auto &[name, layer] : modules_) { - for (auto &[sub_name, param] : layer->StateDict()) { state.emplace(name + "." + sub_name, param); } - } - return state; -} - -void Module::To(Device device) { - if (device == device_) { - return; - } - - std::unordered_map> new_parameters; - for (auto &[name, param] : parameters_) { - new_parameters.emplace(name, std::make_shared(param->To(device))); - } - parameters_ = std::move(new_parameters); - device_ = device; - - for (auto &[_, layer] : modules_) { layer->To(device); } -} - -void Module::Apply(std::function fn) { - for (auto *module : modules()) { module->Apply(fn); } - fn(this); -} -} // namespace infini_train::nn +#include "infini_train/include/nn/modules/module.h" + +#include +#include + +#include "infini_train/include/device.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +const std::string &Module::type() const { return type_; } + +std::vector> Module::Parameters() const { + std::vector> params; + for (auto &[_, param] : parameters_) { params.push_back(param); } + for (auto &[_, layer] : modules_) { + for (auto ¶m : layer->Parameters()) { params.push_back(param); } + } + return params; +} + +bool Module::has_parameter(const std::string &name) const { return parameters_.find(name) != parameters_.end(); } + +std::shared_ptr *Module::mutable_parameter(const std::string &name) { + CHECK(parameters_.find(name) != parameters_.end()); + return ¶meters_.at(name); +} + +const std::shared_ptr &Module::parameter(const std::string &name) const { + CHECK(parameters_.find(name) != parameters_.end()); + return parameters_.at(name); +} + +std::vector Module::modules() const { + std::vector modules; + for (auto &[_, module] : modules_) { modules.push_back(module.get()); } + return modules; +} + +Module *Module::mutable_module(const std::string &name) { + CHECK(modules_.find(name) != modules_.end()); + return modules_.at(name).get(); +} + +const Module &Module::module(const std::string &name) const { + CHECK(modules_.find(name) != modules_.end()); + return *modules_.at(name).get(); +} + +std::unordered_map> Module::StateDict() const { + std::unordered_map> state; + for (auto &[name, param] : parameters_) { state.emplace(name, param); } + for (auto &[name, layer] : modules_) { + for (auto &[sub_name, param] : layer->StateDict()) { state.emplace(name + "." + sub_name, param); } + } + return state; +} + +void Module::To(Device device) { + if (device == device_) { + return; + } + + std::unordered_map> new_parameters; + for (auto &[name, param] : parameters_) { + new_parameters.emplace(name, std::make_shared(param->To(device))); + } + parameters_ = std::move(new_parameters); + device_ = device; + + for (auto &[_, layer] : modules_) { layer->To(device); } +} + +void Module::Apply(std::function fn) { + for (auto *module : modules()) { module->Apply(fn); } + fn(this); +} +} // namespace infini_train::nn diff --git a/infini_train/src/nn/modules/normalization.cc b/infini_train/src/nn/modules/normalization.cc index 11db0af..c6946cc 100644 --- a/infini_train/src/nn/modules/normalization.cc +++ b/infini_train/src/nn/modules/normalization.cc @@ -1,31 +1,31 @@ -#include "infini_train/include/nn/modules/normalization.h" - -#include -#include - -#include "infini_train/include/autograd/normalization.h" -#include "infini_train/include/device.h" -#include "infini_train/include/nn/init.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { -LayerNorm::LayerNorm(const std::vector &normalized_shape, float eps, Device device) : eps_(eps) { - device_ = device; - - parameters_[kParamWeightName] - = std::make_shared(normalized_shape, DataType::kFLOAT32, device)->RequiresGrad(); - parameters_[kParamBiasName] - = std::make_shared(normalized_shape, DataType::kFLOAT32, device)->RequiresGrad(); - ResetParameters(); -} - -std::vector> LayerNorm::Forward(const std::vector> &input_tensors) { - return std::make_shared(eps_)->Apply( - {input_tensors[0], parameters_[kParamWeightName], parameters_[kParamBiasName]}); -} - -void LayerNorm::ResetParameters() { - init::Ones(parameters_[kParamWeightName]); - init::Zeros(parameters_[kParamBiasName]); -} -} // namespace infini_train::nn +#include "infini_train/include/nn/modules/normalization.h" + +#include +#include + +#include "infini_train/include/autograd/normalization.h" +#include "infini_train/include/device.h" +#include "infini_train/include/nn/init.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { +LayerNorm::LayerNorm(const std::vector &normalized_shape, float eps, Device device) : eps_(eps) { + device_ = device; + + parameters_[kParamWeightName] + = std::make_shared(normalized_shape, DataType::kFLOAT32, device)->RequiresGrad(); + parameters_[kParamBiasName] + = std::make_shared(normalized_shape, DataType::kFLOAT32, device)->RequiresGrad(); + ResetParameters(); +} + +std::vector> LayerNorm::Forward(const std::vector> &input_tensors) { + return std::make_shared(eps_)->Apply( + {input_tensors[0], parameters_[kParamWeightName], parameters_[kParamBiasName]}); +} + +void LayerNorm::ResetParameters() { + init::Ones(parameters_[kParamWeightName]); + init::Zeros(parameters_[kParamBiasName]); +} +} // namespace infini_train::nn diff --git a/infini_train/src/nn/modules/sparse.cc b/infini_train/src/nn/modules/sparse.cc index 57223ca..4fc9930 100644 --- a/infini_train/src/nn/modules/sparse.cc +++ b/infini_train/src/nn/modules/sparse.cc @@ -1,27 +1,27 @@ -#include "infini_train/include/nn/modules/sparse.h" - -#include -#include - -#include "infini_train/include/autograd/sparse.h" -#include "infini_train/include/device.h" -#include "infini_train/include/nn/init.h" -#include "infini_train/include/tensor.h" - -namespace infini_train::nn { - -Embedding::Embedding(int num_embeddings, int embedding_dim, Device device) : Module(kType) { - device_ = device; - - parameters_[kParamWeightName] - = std::make_shared(std::vector{num_embeddings, embedding_dim}, DataType::kFLOAT32, device) - ->RequiresGrad(); - ResetParameters(); -} - -std::vector> Embedding::Forward(const std::vector> &input_tensors) { - return std::make_shared()->Apply({input_tensors[0], parameters_[kParamWeightName]}); -} - -void Embedding::ResetParameters() { init::Normal(parameters_[kParamWeightName]); } -} // namespace infini_train::nn +#include "infini_train/include/nn/modules/sparse.h" + +#include +#include + +#include "infini_train/include/autograd/sparse.h" +#include "infini_train/include/device.h" +#include "infini_train/include/nn/init.h" +#include "infini_train/include/tensor.h" + +namespace infini_train::nn { + +Embedding::Embedding(int num_embeddings, int embedding_dim, Device device) : Module(kType) { + device_ = device; + + parameters_[kParamWeightName] + = std::make_shared(std::vector{num_embeddings, embedding_dim}, DataType::kFLOAT32, device) + ->RequiresGrad(); + ResetParameters(); +} + +std::vector> Embedding::Forward(const std::vector> &input_tensors) { + return std::make_shared()->Apply({input_tensors[0], parameters_[kParamWeightName]}); +} + +void Embedding::ResetParameters() { init::Normal(parameters_[kParamWeightName]); } +} // namespace infini_train::nn diff --git a/infini_train/src/optimizer.cc b/infini_train/src/optimizer.cc index 9ff2b5c..b89a987 100644 --- a/infini_train/src/optimizer.cc +++ b/infini_train/src/optimizer.cc @@ -1,55 +1,55 @@ -#include "infini_train/include/optimizer.h" - -#include - -#include "infini_train/include/device.h" -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/tensor.h" - -namespace infini_train { -Optimizer::Optimizer(const std::vector> ¶ms) : params_(params) {} - -void Optimizer::ZeroGrad() { - for (auto param : params_) { param->ZeroGrad(); } -} - -namespace optimizers { - -SGD::SGD(const std::vector> ¶ms, float learning_rate) - : Optimizer(params), learning_rate_(learning_rate) {} - -void SGD::Step() { - for (auto param : params_) { - auto device = param->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "AccumulateGrad"}); - kernel.Call(param->grad(), -learning_rate_, param); - } -} - -Adam::Adam(const std::vector> ¶ms, float learning_rate, float beta1, float beta2, float eps) - : Optimizer(params), t_(0), learning_rate_(learning_rate), beta1_(beta1), beta2_(beta2), eps_(eps) { - - for (const auto ¶m : params_) { - m_.emplace_back(std::make_shared(param->Dims(), param->Dtype(), param->GetDevice())); - v_.emplace_back(std::make_shared(param->Dims(), param->Dtype(), param->GetDevice())); - m_.back()->Fill(0.0f); - v_.back()->Fill(0.0f); - } -} - -void Adam::Step() { - ++t_; - - for (size_t i = 0; i < params_.size(); ++i) { - auto ¶m = params_[i]; - const auto &grad = param->grad(); - auto &m = m_[i]; - auto &v = v_[i]; - - auto device = param->GetDevice().Type(); - auto kernel = Dispatcher::Instance().GetKernel({device, "AdamAccumulateGrad"}); - kernel.Call(grad, param, m, v, learning_rate_, beta1_, beta2_, eps_, t_); - } -} -} // namespace optimizers -} // namespace infini_train +#include "infini_train/include/optimizer.h" + +#include + +#include "infini_train/include/device.h" +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/tensor.h" + +namespace infini_train { +Optimizer::Optimizer(const std::vector> ¶ms) : params_(params) {} + +void Optimizer::ZeroGrad() { + for (auto param : params_) { param->ZeroGrad(); } +} + +namespace optimizers { + +SGD::SGD(const std::vector> ¶ms, float learning_rate) + : Optimizer(params), learning_rate_(learning_rate) {} + +void SGD::Step() { + for (auto param : params_) { + auto device = param->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "AccumulateGrad"}); + kernel.Call(param->grad(), -learning_rate_, param); + } +} + +Adam::Adam(const std::vector> ¶ms, float learning_rate, float beta1, float beta2, float eps) + : Optimizer(params), t_(0), learning_rate_(learning_rate), beta1_(beta1), beta2_(beta2), eps_(eps) { + + for (const auto ¶m : params_) { + m_.emplace_back(std::make_shared(param->Dims(), param->Dtype(), param->GetDevice())); + v_.emplace_back(std::make_shared(param->Dims(), param->Dtype(), param->GetDevice())); + m_.back()->Fill(0.0f); + v_.back()->Fill(0.0f); + } +} + +void Adam::Step() { + ++t_; + + for (size_t i = 0; i < params_.size(); ++i) { + auto ¶m = params_[i]; + const auto &grad = param->grad(); + auto &m = m_[i]; + auto &v = v_[i]; + + auto device = param->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "AdamAccumulateGrad"}); + kernel.Call(grad, param, m, v, learning_rate_, beta1_, beta2_, eps_, t_); + } +} +} // namespace optimizers +} // namespace infini_train diff --git a/infini_train/src/tensor.cc b/infini_train/src/tensor.cc index 8f8c744..d475c8c 100644 --- a/infini_train/src/tensor.cc +++ b/infini_train/src/tensor.cc @@ -1,717 +1,761 @@ -#include "infini_train/include/tensor.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef USE_CUDA -#include "cuda_runtime_api.h" -#endif -#include "Eigen/Dense" -#include "glog/logging.h" - -#include "infini_train/include/autograd/elementwise.h" -#include "infini_train/include/autograd/matmul.h" -#include "infini_train/include/autograd/misc.h" -#include "infini_train/include/autograd/outer.h" -#include "infini_train/include/autograd/transform.h" -#include "infini_train/include/device.h" -#include "infini_train/include/dispatcher.h" -#include "infini_train/include/nn/init.h" - -namespace infini_train { -namespace { -const std::unordered_map kDataTypeToSize = { - {DataType::kUINT8, 1}, {DataType::kINT8, 1}, {DataType::kUINT16, 2}, {DataType::kINT16, 2}, - {DataType::kUINT32, 4}, {DataType::kINT32, 4}, {DataType::kUINT64, 8}, {DataType::kINT64, 8}, - {DataType::kBFLOAT16, 2}, {DataType::kFLOAT16, 2}, {DataType::kFLOAT32, 4}, {DataType::kFLOAT64, 8}, -}; - -const std::unordered_map kDataTypeToDesc = { - {DataType::kUINT8, "uint8"}, {DataType::kINT8, "int8"}, {DataType::kUINT16, "uint16"}, - {DataType::kINT16, "int16"}, {DataType::kUINT32, "uint32"}, {DataType::kINT32, "int32"}, - {DataType::kUINT64, "uint64"}, {DataType::kINT64, "int64"}, {DataType::kBFLOAT16, "bf16"}, - {DataType::kFLOAT16, "fp16"}, {DataType::kFLOAT32, "fp32"}, {DataType::kFLOAT64, "fp64"}, -}; - -template struct TypeMap; - -template <> struct TypeMap { - using type = float; -}; -template <> struct TypeMap { - using type = double; -}; -template <> struct TypeMap { - using type = int32_t; -}; -template <> struct TypeMap { - using type = int64_t; -}; -} // namespace - -TensorBuffer::TensorBuffer(Device device, size_t size) : device_(device), size_(size) { - switch (device_.Type()) { - case DeviceType::kCPU: - data_ = malloc(size); - break; -#ifdef USE_CUDA - case DeviceType::kCUDA: - cudaMallocAsync(&data_, size, 0); - break; -#endif - default: - LOG(FATAL) << "Unsupported device type: " << static_cast(device_.Type()); - break; - } -} - -TensorBuffer::~TensorBuffer() { - switch (device_.Type()) { - case DeviceType::kCPU: - free(data_); - break; -#ifdef USE_CUDA - case DeviceType::kCUDA: - cudaFreeAsync(data_, 0); - break; -#endif - default: - LOG(FATAL) << "Unsupported device type: " << static_cast(device_.Type()); - break; - } -} - -void *TensorBuffer::DataPtr() { return data_; } - -const void *TensorBuffer::DataPtr() const { return data_; } - -Device TensorBuffer::GetDevice() const { return device_; } - -size_t TensorBuffer::Size() const { return size_; } - -// Tensor implementation -Tensor::Tensor(const std::vector &dims, DataType dtype, Device device) : dims_(dims), dtype_(dtype) { - num_elements_ = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); - buffer_ = std::make_shared(device, kDataTypeToSize.at(dtype) * num_elements_); -} - -Tensor::Tensor(const Tensor &tensor, size_t offset, const std::vector &dims) - : buffer_(tensor.buffer_), offset_(offset), dims_(dims), - num_elements_(std::accumulate(dims.begin(), dims.end(), 1, std::multiplies())), dtype_(tensor.dtype_) { - CHECK_LE(offset_ + kDataTypeToSize.at(dtype_) * num_elements_, buffer_->Size()); -} - -Device Tensor::GetDevice() const { return buffer_->GetDevice(); } - -void *Tensor::DataPtr() { return reinterpret_cast(buffer_->DataPtr()) + offset_; } - -const void *Tensor::DataPtr() const { return reinterpret_cast(buffer_->DataPtr()) + offset_; } - -size_t Tensor::SizeInBytes() const { return kDataTypeToSize.at(dtype_) * num_elements_; } - -const std::vector &Tensor::Dims() const { return dims_; } - -size_t Tensor::NumElements() const { return num_elements_; } - -DataType Tensor::Dtype() const { return dtype_; } - -template void Tensor::Fill(T value) { - DataType dtype = Dtype(); - - uint64_t storage = 0; - - switch (dtype) { - case DataType::kFLOAT32: { - using TargetT = typename TypeMap::type; - TargetT casted_value = static_cast(value); - std::memcpy(&storage, &casted_value, sizeof(TargetT)); - break; - } - case DataType::kFLOAT64: { - using TargetT = typename TypeMap::type; - TargetT casted_value = static_cast(value); - std::memcpy(&storage, &casted_value, sizeof(TargetT)); - break; - } - case DataType::kINT32: { - using TargetT = typename TypeMap::type; - TargetT casted_value = static_cast(value); - std::memcpy(&storage, &casted_value, sizeof(TargetT)); - break; - } - case DataType::kINT64: { - using TargetT = typename TypeMap::type; - TargetT casted_value = static_cast(value); - std::memcpy(&storage, &casted_value, sizeof(TargetT)); - break; - } - default: - throw std::runtime_error("Unsupported data type in Tensor::Fill()"); - } - - auto kernel = Dispatcher::Instance().GetKernel({GetDevice().Type(), "Fill"}); - kernel.Call(shared_from_this(), static_cast(&storage)); -} - -template void Tensor::Fill(float); - -Eigen::Map> Tensor::EigenMatrix() { - const int64_t bs = std::accumulate(dims_.rbegin() + 1, dims_.rend(), 1, std::multiplies()); - return Eigen::Map>( - reinterpret_cast(DataPtr()), bs, *dims_.rbegin()); -} - -Eigen::Map> Tensor::EigenVector() { - CHECK_EQ(dims_.size(), 1); - return Eigen::Map>(reinterpret_cast(DataPtr()), 1, - dims_[0]); -} - -Tensor Tensor::To(Device device) { - if (device == buffer_->GetDevice()) { - auto new_tensor = Tensor(*this, offset_, dims_); - if (grad_) { - new_tensor.grad_ = std::make_unique(*grad_.get(), grad_->offset_, grad_->dims_); - } - return new_tensor; - } - - Tensor new_tensor; - switch (device.Type()) { -#ifdef USE_CUDA - case DeviceType::kCPU: - // CUDA -> CPU - new_tensor = Tensor(dims_, dtype_, Device(DeviceType::kCPU, 0)); - cudaMemcpyAsync(new_tensor.DataPtr(), DataPtr(), SizeInBytes(), cudaMemcpyDeviceToHost, 0); - break; - case DeviceType::kCUDA: - // CPU -> CUDA - new_tensor = Tensor(dims_, dtype_, Device(DeviceType::kCUDA, 0)); - cudaMemcpyAsync(new_tensor.DataPtr(), DataPtr(), SizeInBytes(), cudaMemcpyHostToDevice, 0); - break; -#endif - default: - LOG(FATAL) << "Unsupported device type: " << static_cast(device.Type()); - } - - if (grad_) { - new_tensor.grad_ = std::make_unique(grad_->To(device)); - } - - new_tensor.requires_grad_ = requires_grad_; - - return new_tensor; -} - -// operator overloading -std::shared_ptr Tensor::Equals(float scalar) { - return std::make_shared(scalar)->Apply({shared_from_this()})[0]; -} - -std::shared_ptr Tensor::Add(const std::shared_ptr &other) { - CHECK_EQ(static_cast(GetDevice().Type()), static_cast(other->GetDevice().Type())); - return std::make_shared()->Apply({shared_from_this(), other})[0]; -} - -std::shared_ptr Tensor::Add(float scalar) { - return std::make_shared(scalar)->Apply({shared_from_this()})[0]; -} - -std::shared_ptr Tensor::Sub(const std::shared_ptr &other) { - CHECK_EQ(static_cast(GetDevice().Type()), static_cast(other->GetDevice().Type())); - return std::make_shared()->Apply({shared_from_this(), other})[0]; -} - -std::shared_ptr Tensor::Mul(const std::shared_ptr &other) { - CHECK_EQ(static_cast(GetDevice().Type()), static_cast(other->GetDevice().Type())); - return std::make_shared()->Apply({shared_from_this(), other})[0]; -} - -std::shared_ptr Tensor::Mul(float scalar) { - return std::make_shared(scalar)->Apply({shared_from_this()})[0]; -} - -std::shared_ptr Tensor::Div(const std::shared_ptr &other) { - CHECK_EQ(static_cast(GetDevice().Type()), static_cast(other->GetDevice().Type())); - return std::make_shared()->Apply({shared_from_this(), other})[0]; -} - -std::shared_ptr Tensor::Neg() { return std::make_shared()->Apply({shared_from_this()})[0]; } - -std::shared_ptr Tensor::Reciprocal() { - return std::make_shared()->Apply({shared_from_this()})[0]; -} - -std::shared_ptr Tensor::Sin() { return std::make_shared()->Apply({shared_from_this()})[0]; } - -std::shared_ptr Tensor::Cos() { return std::make_shared()->Apply({shared_from_this()})[0]; } - -std::shared_ptr Tensor::Tanh() { return std::make_shared()->Apply({shared_from_this()})[0]; } - -std::shared_ptr Tensor::Pow(float exponent) { - return std::make_shared(exponent)->Apply({shared_from_this()})[0]; -} - -std::shared_ptr Tensor::Rsqrt() { return std::make_shared()->Apply({shared_from_this()})[0]; } - -std::vector> Tensor::Split(int split_size, int dim) { - return std::make_shared(split_size, dim)->Apply({shared_from_this()}); -} - -std::shared_ptr Tensor::RepeatInterleave(int64_t repeat, int64_t dim) { - return std::make_shared(repeat, dim)->Apply({shared_from_this()})[0]; -} - -std::shared_ptr Tensor::View(const std::vector &dims) { - return std::make_shared(dims)->Apply({shared_from_this()})[0]; -} - -std::shared_ptr Tensor::Contiguous() { - return std::make_shared(dims_)->Apply({shared_from_this()})[0]; -} - -std::shared_ptr Tensor::Flatten(int64_t start, int64_t end) { - // return Contiguous()->View(new_shape); - // =================================== 作业 =================================== - // TODO:实现张量扁平化操作,将指定维度范围[start, end]内的所有维度合并为一个维度 - // HINT: - // =================================== 作业 =================================== - - return std::make_shared(); -} - -std::shared_ptr Tensor::Squeeze(int64_t dim) { - std::vector new_shape = dims_; - if (dim < 0) { - dim += new_shape.size(); - } - CHECK_GE(dim, 0); - CHECK_LT(dim, new_shape.size()); - CHECK_EQ(new_shape[dim], 1) << "Cannot squeeze dim " << dim << " because size (" << new_shape[dim] << ") != 1."; - - new_shape.erase(new_shape.begin() + dim); - - return Contiguous()->View(new_shape); -} - -std::shared_ptr Tensor::Slice(const std::vector &starts, const std::vector &ends, - const std::vector &steps) { - return std::make_shared(starts, ends, steps)->Apply({shared_from_this()})[0]; -} - -std::shared_ptr Tensor::Slice(int64_t dim, int64_t start, int64_t end, int64_t step) { - // Slice only on one dimension - if (dim < 0) { - dim += dims_.size(); - } - CHECK_GE(dim, 0); - CHECK_LT(dim, dims_.size()); - std::vector starts(dims_.size(), 0); - std::vector ends = dims_; - std::vector steps(dims_.size(), 1); - - starts[dim] = start; - ends[dim] = end; - steps[dim] = step; - return Slice(starts, ends, steps); -} - -std::shared_ptr Tensor::Transpose(int dim0, int dim1) { - return std::make_shared(dim0, dim1)->Apply({shared_from_this()})[0]; -} - -std::shared_ptr Tensor::MaskedFill(const std::shared_ptr &mask, float value) { - return std::make_shared(mask, value)->Apply({shared_from_this()})[0]; -} - -std::shared_ptr Tensor::Matmul(const std::shared_ptr &other) { - return std::make_shared()->Apply({shared_from_this(), other})[0]; -} - -std::shared_ptr Tensor::Outer(const std::shared_ptr &other) { - return std::make_shared()->Apply({shared_from_this(), other})[0]; -} - -// distribution -std::shared_ptr Tensor::Uniform(float from, float to, std::optional generator) { - return nn::init::Uniform(shared_from_this(), from, to, generator); -} - -// autograd related -std::shared_ptr Tensor::RequiresGrad() { - requires_grad_ = true; - if (!grad_) { - grad_ = std::make_unique(dims_, dtype_, GetDevice()); - grad_->Fill(0.0f); - } - return shared_from_this(); -} - -void Tensor::Backward(std::shared_ptr gradient, bool retain_graph, bool create_graph) const { - // =================================== 作业 =================================== - // TODO:实现自动微分反向传播 - // 功能描述:1. 计算当前张量对叶子节点的梯度 2. 支持多输出场景的梯度累加 - // =================================== 作业 =================================== -} - -void Tensor::ZeroGrad() { - if (grad_) { - grad_->Fill(0.0f); - } -} - -std::ostream &operator<<(std::ostream &os, const Tensor &tensor) { - os << "Tensor(data_ptr=" << static_cast(tensor.DataPtr()) << ", dims=["; - for (const auto &dim : tensor.Dims()) { os << dim << ", "; } - os << "], dtype=" << kDataTypeToDesc.at(tensor.Dtype()) << ")"; - return os; -} - -std::shared_ptr operator==(const std::shared_ptr &t, float scalar) { return t->Equals(scalar); } - -std::shared_ptr operator+(const std::shared_ptr &t1, const std::shared_ptr &t2) { - return t1->Add(t2); -} - -std::shared_ptr operator+(float scalar, const std::shared_ptr &t) { return t->Add(scalar); } - -std::shared_ptr operator+(const std::shared_ptr &t, float scalar) { return t->Add(scalar); } - -std::shared_ptr operator-(const std::shared_ptr &t1, const std::shared_ptr &t2) { - return t1->Sub(t2); -} - -std::shared_ptr operator-(float scalar, const std::shared_ptr &t) { return t->Neg()->Add(scalar); } - -std::shared_ptr operator-(const std::shared_ptr &t, float scalar) { return t->Add(-scalar); } - -std::shared_ptr operator-(const std::shared_ptr &t) { return t->Neg(); } - -std::shared_ptr operator*(const std::shared_ptr &t1, const std::shared_ptr &t2) { - return t1->Mul(t2); -} - -std::shared_ptr operator*(float scalar, const std::shared_ptr &t) { return t->Mul(scalar); } - -std::shared_ptr operator*(const std::shared_ptr &t, float scalar) { return t->Mul(scalar); } - -std::shared_ptr operator/(const std::shared_ptr &t1, const std::shared_ptr &t2) { - return t1->Div(t2); -} - -std::shared_ptr operator/(float scalar, const std::shared_ptr &t) { - return t->Reciprocal()->Mul(scalar); -} - -std::shared_ptr operator/(const std::shared_ptr &t, float scalar) { return t->Mul(1.0f / scalar); } - -void Tensor::SaveAsNpy(const std::string &path) const { - CHECK(dtype_ == DataType::kFLOAT32); - - const size_t num_elements = NumElements(); - const size_t num_bytes = num_elements * sizeof(float); - - // Prepare host buffer - std::vector host_buffer(num_elements); - - if (GetDevice().Type() == DeviceType::kCPU) { - // If on CPU, direct copy - std::memcpy(host_buffer.data(), DataPtr(), num_bytes); - } -#ifdef USE_CUDA - else if (GetDevice().Type() == DeviceType::kCUDA) { - // If on CUDA, copy back to host - cudaDeviceSynchronize(); - cudaError_t err = cudaMemcpy(host_buffer.data(), DataPtr(), num_bytes, cudaMemcpyDeviceToHost); - CHECK_EQ(err, cudaSuccess) << "cudaMemcpy failed: " << cudaGetErrorString(err); - } -#endif - else { - LOG(FATAL) << "Unsupported device type for SaveAsNpy."; - } - - // Write .npy file - std::ofstream file(path, std::ios::binary); - CHECK(file.is_open()) << "Failed to open file for writing: " << path; - - // Write magic string - file.write("\x93NUMPY", 6); - - // Write version - uint8_t major = 1; - uint8_t minor = 0; - file.put(major); - file.put(minor); - - // Construct header - std::ostringstream header_ss; - header_ss << "{'descr': '(header.size()); - file.write(reinterpret_cast(&header_size), sizeof(header_size)); - file.write(header.c_str(), header.size()); - - // Write data - file.write(reinterpret_cast(host_buffer.data()), num_bytes); - - file.close(); -} - -void Tensor::SetPrintOptions(std::optional precision, std::optional threshold, - std::optional edge_items, std::optional linewidth, - std::optional profile, std::optional sci_mode) { - PrintOptions &opts = PrintOptions::Get(); - if (profile) { - // ref: https://github.com/pytorch/pytorch/blob/main/torch/_tensor_str.py - std::string &profile_name = *profile; - std::transform(profile_name.begin(), profile_name.end(), profile_name.begin(), ::tolower); - if (profile_name == "default") { - opts.precision = 4; - opts.threshold = 1000; - opts.edge_items = 3; - opts.linewidth = 80; - opts.sci_mode = std::nullopt; - } else if (profile_name == "short") { - opts.precision = 4; - opts.threshold = 100; - opts.edge_items = 2; - opts.linewidth = 80; - opts.sci_mode = std::nullopt; - } else if (profile_name == "full") { - opts.precision = 4; - opts.threshold = std::numeric_limits::max(); - opts.edge_items = 3; - opts.linewidth = 80; - opts.sci_mode = std::nullopt; - } else { - LOG(WARNING) << "Undefined profile name: " << profile_name; - } - } - - if (precision) { - opts.precision = *precision; - } - if (threshold) { - opts.threshold = *threshold; - } - if (edge_items) { - opts.edge_items = *edge_items; - } - if (linewidth) { - opts.linewidth = *linewidth; - } - if (sci_mode) { - opts.sci_mode = *sci_mode; - } -} - -void Tensor::Print(std::ostream &os) const { - /* - Print tensor in torch.tensor/np.array style. - */ - CHECK(dtype_ == DataType::kFLOAT32); - - const size_t num_elements = NumElements(); - const size_t num_bytes = num_elements * sizeof(float); - - std::vector host_buffer(num_elements); - - if (GetDevice().Type() == DeviceType::kCPU) { - std::memcpy(host_buffer.data(), DataPtr(), num_bytes); - } -#ifdef USE_CUDA - else if (GetDevice().Type() == DeviceType::kCUDA) { - cudaDeviceSynchronize(); - cudaError_t err = cudaMemcpy(host_buffer.data(), DataPtr(), num_bytes, cudaMemcpyDeviceToHost); - CHECK_EQ(err, cudaSuccess) << "cudaMemcpy failed: " << cudaGetErrorString(err); - } -#endif - else { - LOG(FATAL) << "Unsupported device type for Print."; - } - - const PrintOptions &opts = PrintOptions::Get(); - const int64_t precision = opts.precision; - const int64_t threshold = opts.threshold; - const int64_t edge_items = opts.edge_items; - const int64_t linewidth = opts.linewidth; - const int64_t base_indent = 8; // length of "tensor([" - - bool use_sci = opts.sci_mode.value_or(false); - if (!opts.sci_mode.has_value()) { - for (float v : host_buffer) { - float abs_v = std::fabs(v); - if ((abs_v > 0.0f && abs_v < 1e-4f) || abs_v >= 1e+4f) { - use_sci = true; - break; - } - } - } - - auto format_float = [&](float val) -> std::string { - std::ostringstream ss; - if (use_sci) { - ss << std::scientific << std::setprecision(precision); - } else { - ss << std::fixed << std::setprecision(precision); - } - ss << val; - return ss.str(); - }; - - std::vector str_vals(num_elements); - size_t max_width = 0; - for (size_t i = 0; i < num_elements; ++i) { - str_vals[i] = format_float(host_buffer[i]); - max_width = std::max(max_width, str_vals[i].length()); - } - - const int ndim = dims_.size(); - - std::function print_rec; - print_rec = [&](int dim, size_t offset, int indent) { - os << "["; - size_t step = 1; - for (int d = dim + 1; d < ndim; ++d) { step *= dims_[d]; } - int n = dims_[dim]; - - if (dim == ndim - 1) { - if (n <= 2 * edge_items || num_elements <= threshold) { - int line_len = base_indent + indent + 1; - for (int i = 0; i < n; ++i) { - if (i > 0) { - os << ", "; - line_len += 2; - } - std::string item = str_vals[offset + i]; - if (linewidth > 0 && line_len + max_width > linewidth) { - os << "\n" << std::string(base_indent + indent + 1, ' '); - line_len = base_indent + indent + 1; - } - os << std::setw(max_width) << item; - line_len += max_width; - } - } else { - int line_len = base_indent + indent + 1; - for (int i = 0; i < edge_items; ++i) { - if (i > 0) { - os << ", "; - line_len += 2; - } - std::string item = str_vals[offset + i]; - if (linewidth > 0 && line_len + max_width > linewidth) { - os << "\n" << std::string(base_indent + indent + 1, ' '); - line_len = base_indent + indent + 1; - } - os << std::setw(max_width) << item; - line_len += max_width; - } - os << ", ..."; - line_len += 5; // length of ", ..." - if (linewidth > 0 && line_len + max_width > linewidth) { - os << "\n" << std::string(base_indent + indent + 1, ' '); - line_len = base_indent + indent + 1; - } else { - os << ", "; - line_len += 2; - } - for (int i = n - edge_items; i < n; ++i) { - if (i > n - edge_items) { - os << ", "; - line_len += 2; - } - std::string item = str_vals[offset + i]; - if (linewidth > 0 && line_len + max_width > linewidth) { - os << "\n" << std::string(base_indent + indent + 1, ' '); - line_len = base_indent + indent + 1; - } - os << std::setw(max_width) << item; - line_len += max_width; - } - } - } else { - if (n <= 2 * edge_items || num_elements <= threshold) { - for (int i = 0; i < n; ++i) { - if (i > 0) { - if (dim < ndim - 2) { - os << ",\n\n" << std::string(base_indent + indent, ' '); - } else { - os << ",\n" << std::string(base_indent + indent, ' '); - } - } - print_rec(dim + 1, offset + i * step, indent + 1); - } - } else { - for (int i = 0; i < edge_items; ++i) { - if (i > 0) { - if (dim < ndim - 2) { - os << ",\n\n" << std::string(base_indent + indent, ' '); - } else { - os << ",\n" << std::string(base_indent + indent, ' '); - } - } - print_rec(dim + 1, offset + i * step, indent + 1); - } - os << ",\n" - << std::string(base_indent + indent, ' ') << "...\n" - << std::string(base_indent + indent, ' '); - for (int i = n - edge_items; i < n; ++i) { - if (i > n - edge_items) { - if (dim < ndim - 2) { - os << ",\n\n" << std::string(base_indent + indent, ' '); - } else { - os << ",\n" << std::string(base_indent + indent, ' '); - } - } - print_rec(dim + 1, offset + i * step, indent + 1); - } - } - } - os << "]"; - }; - - os << "Tensor("; - if (num_elements == 0) { - os << "[], "; - } else { - print_rec(0, 0, 0); - os << ", \n"; - } - - os << std::string(base_indent - 1, ' ') << "dtype=float32, shape=("; - for (size_t i = 0; i < dims_.size(); ++i) { - if (i > 0) { - os << ", "; - } - os << dims_[i]; - } - if (dims_.size() == 1) { - os << ","; - } - os << "))\n"; -} -} // namespace infini_train +#include "infini_train/include/tensor.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_CUDA +#include "cuda_runtime_api.h" +#endif +#include "Eigen/Dense" +#include "glog/logging.h" + +#include "infini_train/include/autograd/elementwise.h" +#include "infini_train/include/autograd/matmul.h" +#include "infini_train/include/autograd/misc.h" +#include "infini_train/include/autograd/outer.h" +#include "infini_train/include/autograd/transform.h" +#include "infini_train/include/device.h" +#include "infini_train/include/dispatcher.h" +#include "infini_train/include/nn/init.h" + +namespace infini_train { +namespace { +const std::unordered_map kDataTypeToSize = { + {DataType::kUINT8, 1}, {DataType::kINT8, 1}, {DataType::kUINT16, 2}, {DataType::kINT16, 2}, + {DataType::kUINT32, 4}, {DataType::kINT32, 4}, {DataType::kUINT64, 8}, {DataType::kINT64, 8}, + {DataType::kBFLOAT16, 2}, {DataType::kFLOAT16, 2}, {DataType::kFLOAT32, 4}, {DataType::kFLOAT64, 8}, +}; + +const std::unordered_map kDataTypeToDesc = { + {DataType::kUINT8, "uint8"}, {DataType::kINT8, "int8"}, {DataType::kUINT16, "uint16"}, + {DataType::kINT16, "int16"}, {DataType::kUINT32, "uint32"}, {DataType::kINT32, "int32"}, + {DataType::kUINT64, "uint64"}, {DataType::kINT64, "int64"}, {DataType::kBFLOAT16, "bf16"}, + {DataType::kFLOAT16, "fp16"}, {DataType::kFLOAT32, "fp32"}, {DataType::kFLOAT64, "fp64"}, +}; + +template struct TypeMap; + +template <> struct TypeMap { + using type = float; +}; +template <> struct TypeMap { + using type = double; +}; +template <> struct TypeMap { + using type = int32_t; +}; +template <> struct TypeMap { + using type = int64_t; +}; +} // namespace + +TensorBuffer::TensorBuffer(Device device, size_t size) : device_(device), size_(size) { + switch (device_.Type()) { + case DeviceType::kCPU: + data_ = malloc(size); + break; +#ifdef USE_CUDA + case DeviceType::kCUDA: + cudaMallocAsync(&data_, size, 0); + break; +#endif + default: + LOG(FATAL) << "Unsupported device type: " << static_cast(device_.Type()); + break; + } +} + +TensorBuffer::~TensorBuffer() { + switch (device_.Type()) { + case DeviceType::kCPU: + free(data_); + break; +#ifdef USE_CUDA + case DeviceType::kCUDA: + cudaFreeAsync(data_, 0); + break; +#endif + default: + LOG(FATAL) << "Unsupported device type: " << static_cast(device_.Type()); + break; + } +} + +void *TensorBuffer::DataPtr() { return data_; } + +const void *TensorBuffer::DataPtr() const { return data_; } + +Device TensorBuffer::GetDevice() const { return device_; } + +size_t TensorBuffer::Size() const { return size_; } + +// Tensor implementation +Tensor::Tensor(const std::vector &dims, DataType dtype, Device device) : dims_(dims), dtype_(dtype) { + num_elements_ = std::accumulate(dims.begin(), dims.end(), 1, std::multiplies()); + buffer_ = std::make_shared(device, kDataTypeToSize.at(dtype) * num_elements_); +} + +Tensor::Tensor(const Tensor &tensor, size_t offset, const std::vector &dims) + : buffer_(tensor.buffer_), offset_(offset), dims_(dims), + num_elements_(std::accumulate(dims.begin(), dims.end(), 1, std::multiplies())), dtype_(tensor.dtype_) { + CHECK_LE(offset_ + kDataTypeToSize.at(dtype_) * num_elements_, buffer_->Size()); +} + +Device Tensor::GetDevice() const { return buffer_->GetDevice(); } + +void *Tensor::DataPtr() { return reinterpret_cast(buffer_->DataPtr()) + offset_; } + +const void *Tensor::DataPtr() const { return reinterpret_cast(buffer_->DataPtr()) + offset_; } + +size_t Tensor::SizeInBytes() const { return kDataTypeToSize.at(dtype_) * num_elements_; } + +const std::vector &Tensor::Dims() const { return dims_; } + +size_t Tensor::NumElements() const { return num_elements_; } + +DataType Tensor::Dtype() const { return dtype_; } + +template void Tensor::Fill(T value) { + DataType dtype = Dtype(); + + uint64_t storage = 0; + + switch (dtype) { + case DataType::kFLOAT32: { + using TargetT = typename TypeMap::type; + TargetT casted_value = static_cast(value); + std::memcpy(&storage, &casted_value, sizeof(TargetT)); + break; + } + case DataType::kFLOAT64: { + using TargetT = typename TypeMap::type; + TargetT casted_value = static_cast(value); + std::memcpy(&storage, &casted_value, sizeof(TargetT)); + break; + } + case DataType::kINT32: { + using TargetT = typename TypeMap::type; + TargetT casted_value = static_cast(value); + std::memcpy(&storage, &casted_value, sizeof(TargetT)); + break; + } + case DataType::kINT64: { + using TargetT = typename TypeMap::type; + TargetT casted_value = static_cast(value); + std::memcpy(&storage, &casted_value, sizeof(TargetT)); + break; + } + default: + throw std::runtime_error("Unsupported data type in Tensor::Fill()"); + } + + auto kernel = Dispatcher::Instance().GetKernel({GetDevice().Type(), "Fill"}); + kernel.Call(shared_from_this(), static_cast(&storage)); +} + +template void Tensor::Fill(float); + +Eigen::Map> Tensor::EigenMatrix() { + const int64_t bs = std::accumulate(dims_.rbegin() + 1, dims_.rend(), 1, std::multiplies()); + return Eigen::Map>( + reinterpret_cast(DataPtr()), bs, *dims_.rbegin()); +} + +Eigen::Map> Tensor::EigenVector() { + CHECK_EQ(dims_.size(), 1); + return Eigen::Map>(reinterpret_cast(DataPtr()), 1, + dims_[0]); +} + +Tensor Tensor::To(Device device) { + if (device == buffer_->GetDevice()) { + auto new_tensor = Tensor(*this, offset_, dims_); + if (grad_) { + new_tensor.grad_ = std::make_unique(*grad_.get(), grad_->offset_, grad_->dims_); + } + return new_tensor; + } + + Tensor new_tensor; + switch (device.Type()) { +#ifdef USE_CUDA + case DeviceType::kCPU: + // CUDA -> CPU + new_tensor = Tensor(dims_, dtype_, Device(DeviceType::kCPU, 0)); + cudaMemcpyAsync(new_tensor.DataPtr(), DataPtr(), SizeInBytes(), cudaMemcpyDeviceToHost, 0); + break; + case DeviceType::kCUDA: + // CPU -> CUDA + new_tensor = Tensor(dims_, dtype_, Device(DeviceType::kCUDA, 0)); + cudaMemcpyAsync(new_tensor.DataPtr(), DataPtr(), SizeInBytes(), cudaMemcpyHostToDevice, 0); + break; +#else + case DeviceType::kCPU: + // CPU -> CPU copy + new_tensor = Tensor(dims_, dtype_, Device(DeviceType::kCPU, 0)); + memcpy(new_tensor.DataPtr(), DataPtr(), SizeInBytes()); + break; +#endif + default: + LOG(FATAL) << "Unsupported device type: " << static_cast(device.Type()); + } + + if (grad_) { + new_tensor.grad_ = std::make_unique(grad_->To(device)); + } + + new_tensor.requires_grad_ = requires_grad_; + + return new_tensor; +} + +// operator overloading +std::shared_ptr Tensor::Equals(float scalar) { + return std::make_shared(scalar)->Apply({shared_from_this()})[0]; +} + +std::shared_ptr Tensor::Add(const std::shared_ptr &other) { + CHECK_EQ(static_cast(GetDevice().Type()), static_cast(other->GetDevice().Type())); + return std::make_shared()->Apply({shared_from_this(), other})[0]; +} + +std::shared_ptr Tensor::Add(float scalar) { + return std::make_shared(scalar)->Apply({shared_from_this()})[0]; +} + +std::shared_ptr Tensor::Sub(const std::shared_ptr &other) { + CHECK_EQ(static_cast(GetDevice().Type()), static_cast(other->GetDevice().Type())); + return std::make_shared()->Apply({shared_from_this(), other})[0]; +} + +std::shared_ptr Tensor::Mul(const std::shared_ptr &other) { + CHECK_EQ(static_cast(GetDevice().Type()), static_cast(other->GetDevice().Type())); + return std::make_shared()->Apply({shared_from_this(), other})[0]; +} + +std::shared_ptr Tensor::Mul(float scalar) { + return std::make_shared(scalar)->Apply({shared_from_this()})[0]; +} + +std::shared_ptr Tensor::Div(const std::shared_ptr &other) { + CHECK_EQ(static_cast(GetDevice().Type()), static_cast(other->GetDevice().Type())); + return std::make_shared()->Apply({shared_from_this(), other})[0]; +} + +std::shared_ptr Tensor::Neg() { return std::make_shared()->Apply({shared_from_this()})[0]; } + +std::shared_ptr Tensor::Reciprocal() { + return std::make_shared()->Apply({shared_from_this()})[0]; +} + +std::shared_ptr Tensor::Sin() { return std::make_shared()->Apply({shared_from_this()})[0]; } + +std::shared_ptr Tensor::Cos() { return std::make_shared()->Apply({shared_from_this()})[0]; } + +std::shared_ptr Tensor::Tanh() { return std::make_shared()->Apply({shared_from_this()})[0]; } + +std::shared_ptr Tensor::Pow(float exponent) { + return std::make_shared(exponent)->Apply({shared_from_this()})[0]; +} + +std::shared_ptr Tensor::Rsqrt() { return std::make_shared()->Apply({shared_from_this()})[0]; } + +std::vector> Tensor::Split(int split_size, int dim) { + return std::make_shared(split_size, dim)->Apply({shared_from_this()}); +} + +std::shared_ptr Tensor::RepeatInterleave(int64_t repeat, int64_t dim) { + return std::make_shared(repeat, dim)->Apply({shared_from_this()})[0]; +} + +std::shared_ptr Tensor::View(const std::vector &dims) { + return std::make_shared(dims)->Apply({shared_from_this()})[0]; +} + +std::shared_ptr Tensor::Contiguous() { + return std::make_shared(dims_)->Apply({shared_from_this()})[0]; +} + +std::shared_ptr Tensor::Flatten(int64_t start, int64_t end) { + // return Contiguous()->View(new_shape); + // =================================== 作业 =================================== + // TODO:实现张量扁平化操作,将指定维度范围[start, end]内的所有维度合并为一个维度 + // HINT: + // =================================== 作业 =================================== + + int64_t ndim = static_cast(dims_.size()); + if (end < 0) { + end += ndim; + } + CHECK_GE(start, 0); + CHECK_LT(start, ndim); + CHECK_GE(end, start); + CHECK_LT(end, ndim); + + std::vector new_shape; + for (int64_t i = 0; i < start; ++i) { + new_shape.push_back(dims_[i]); + } + int64_t flattened_size = 1; + for (int64_t i = start; i <= end; ++i) { + flattened_size *= dims_[i]; + } + new_shape.push_back(flattened_size); + for (int64_t i = end + 1; i < ndim; ++i) { + new_shape.push_back(dims_[i]); + } + + return Contiguous()->View(new_shape); +} + +std::shared_ptr Tensor::Squeeze(int64_t dim) { + std::vector new_shape = dims_; + if (dim < 0) { + dim += new_shape.size(); + } + CHECK_GE(dim, 0); + CHECK_LT(dim, new_shape.size()); + CHECK_EQ(new_shape[dim], 1) << "Cannot squeeze dim " << dim << " because size (" << new_shape[dim] << ") != 1."; + + new_shape.erase(new_shape.begin() + dim); + + return Contiguous()->View(new_shape); +} + +std::shared_ptr Tensor::Slice(const std::vector &starts, const std::vector &ends, + const std::vector &steps) { + return std::make_shared(starts, ends, steps)->Apply({shared_from_this()})[0]; +} + +std::shared_ptr Tensor::Slice(int64_t dim, int64_t start, int64_t end, int64_t step) { + // Slice only on one dimension + if (dim < 0) { + dim += dims_.size(); + } + CHECK_GE(dim, 0); + CHECK_LT(dim, dims_.size()); + std::vector starts(dims_.size(), 0); + std::vector ends = dims_; + std::vector steps(dims_.size(), 1); + + starts[dim] = start; + ends[dim] = end; + steps[dim] = step; + return Slice(starts, ends, steps); +} + +std::shared_ptr Tensor::Transpose(int dim0, int dim1) { + return std::make_shared(dim0, dim1)->Apply({shared_from_this()})[0]; +} + +std::shared_ptr Tensor::MaskedFill(const std::shared_ptr &mask, float value) { + return std::make_shared(mask, value)->Apply({shared_from_this()})[0]; +} + +std::shared_ptr Tensor::Matmul(const std::shared_ptr &other) { + return std::make_shared()->Apply({shared_from_this(), other})[0]; +} + +std::shared_ptr Tensor::Outer(const std::shared_ptr &other) { + return std::make_shared()->Apply({shared_from_this(), other})[0]; +} + +// distribution +std::shared_ptr Tensor::Uniform(float from, float to, std::optional generator) { + return nn::init::Uniform(shared_from_this(), from, to, generator); +} + +// autograd related +std::shared_ptr Tensor::RequiresGrad() { + requires_grad_ = true; + if (!grad_) { + grad_ = std::make_unique(dims_, dtype_, GetDevice()); + grad_->Fill(0.0f); + } + return shared_from_this(); +} + +void Tensor::Backward(std::shared_ptr gradient, bool retain_graph, bool create_graph) const { + // =================================== 作业 =================================== + // TODO:实现自动微分反向传播 + // 功能描述:1. 计算当前张量对叶子节点的梯度 2. 支持多输出场景的梯度累加 + // =================================== 作业 =================================== + + if (!gradient) { + gradient = std::make_shared(dims_, dtype_, GetDevice()); + gradient->Fill(1.0f); + } + + if (is_leaf_) { + if (requires_grad_ && grad_) { + auto device = grad_->GetDevice().Type(); + auto kernel = Dispatcher::Instance().GetKernel({device, "AccumulateGrad"}); + kernel.Call(gradient, 1.0f, grad_); + } + } else if (grad_fn_) { + grad_fn_->BackwardPartial(gradient, output_idx_); + } +} + +void Tensor::ZeroGrad() { + if (grad_) { + grad_->Fill(0.0f); + } +} + +std::ostream &operator<<(std::ostream &os, const Tensor &tensor) { + os << "Tensor(data_ptr=" << static_cast(tensor.DataPtr()) << ", dims=["; + for (const auto &dim : tensor.Dims()) { os << dim << ", "; } + os << "], dtype=" << kDataTypeToDesc.at(tensor.Dtype()) << ")"; + return os; +} + +std::shared_ptr operator==(const std::shared_ptr &t, float scalar) { return t->Equals(scalar); } + +std::shared_ptr operator+(const std::shared_ptr &t1, const std::shared_ptr &t2) { + return t1->Add(t2); +} + +std::shared_ptr operator+(float scalar, const std::shared_ptr &t) { return t->Add(scalar); } + +std::shared_ptr operator+(const std::shared_ptr &t, float scalar) { return t->Add(scalar); } + +std::shared_ptr operator-(const std::shared_ptr &t1, const std::shared_ptr &t2) { + return t1->Sub(t2); +} + +std::shared_ptr operator-(float scalar, const std::shared_ptr &t) { return t->Neg()->Add(scalar); } + +std::shared_ptr operator-(const std::shared_ptr &t, float scalar) { return t->Add(-scalar); } + +std::shared_ptr operator-(const std::shared_ptr &t) { return t->Neg(); } + +std::shared_ptr operator*(const std::shared_ptr &t1, const std::shared_ptr &t2) { + return t1->Mul(t2); +} + +std::shared_ptr operator*(float scalar, const std::shared_ptr &t) { return t->Mul(scalar); } + +std::shared_ptr operator*(const std::shared_ptr &t, float scalar) { return t->Mul(scalar); } + +std::shared_ptr operator/(const std::shared_ptr &t1, const std::shared_ptr &t2) { + return t1->Div(t2); +} + +std::shared_ptr operator/(float scalar, const std::shared_ptr &t) { + return t->Reciprocal()->Mul(scalar); +} + +std::shared_ptr operator/(const std::shared_ptr &t, float scalar) { return t->Mul(1.0f / scalar); } + +void Tensor::SaveAsNpy(const std::string &path) const { + CHECK(dtype_ == DataType::kFLOAT32); + + const size_t num_elements = NumElements(); + const size_t num_bytes = num_elements * sizeof(float); + + // Prepare host buffer + std::vector host_buffer(num_elements); + + if (GetDevice().Type() == DeviceType::kCPU) { + // If on CPU, direct copy + std::memcpy(host_buffer.data(), DataPtr(), num_bytes); + } +#ifdef USE_CUDA + else if (GetDevice().Type() == DeviceType::kCUDA) { + // If on CUDA, copy back to host + cudaDeviceSynchronize(); + cudaError_t err = cudaMemcpy(host_buffer.data(), DataPtr(), num_bytes, cudaMemcpyDeviceToHost); + CHECK_EQ(err, cudaSuccess) << "cudaMemcpy failed: " << cudaGetErrorString(err); + } +#endif + else { + LOG(FATAL) << "Unsupported device type for SaveAsNpy."; + } + + // Write .npy file + std::ofstream file(path, std::ios::binary); + CHECK(file.is_open()) << "Failed to open file for writing: " << path; + + // Write magic string + file.write("\x93NUMPY", 6); + + // Write version + uint8_t major = 1; + uint8_t minor = 0; + file.put(major); + file.put(minor); + + // Construct header + std::ostringstream header_ss; + header_ss << "{'descr': '(header.size()); + file.write(reinterpret_cast(&header_size), sizeof(header_size)); + file.write(header.c_str(), header.size()); + + // Write data + file.write(reinterpret_cast(host_buffer.data()), num_bytes); + + file.close(); +} + +void Tensor::SetPrintOptions(std::optional precision, std::optional threshold, + std::optional edge_items, std::optional linewidth, + std::optional profile, std::optional sci_mode) { + PrintOptions &opts = PrintOptions::Get(); + if (profile) { + // ref: https://github.com/pytorch/pytorch/blob/main/torch/_tensor_str.py + std::string &profile_name = *profile; + std::transform(profile_name.begin(), profile_name.end(), profile_name.begin(), ::tolower); + if (profile_name == "default") { + opts.precision = 4; + opts.threshold = 1000; + opts.edge_items = 3; + opts.linewidth = 80; + opts.sci_mode = std::nullopt; + } else if (profile_name == "short") { + opts.precision = 4; + opts.threshold = 100; + opts.edge_items = 2; + opts.linewidth = 80; + opts.sci_mode = std::nullopt; + } else if (profile_name == "full") { + opts.precision = 4; + opts.threshold = std::numeric_limits::max(); + opts.edge_items = 3; + opts.linewidth = 80; + opts.sci_mode = std::nullopt; + } else { + LOG(WARNING) << "Undefined profile name: " << profile_name; + } + } + + if (precision) { + opts.precision = *precision; + } + if (threshold) { + opts.threshold = *threshold; + } + if (edge_items) { + opts.edge_items = *edge_items; + } + if (linewidth) { + opts.linewidth = *linewidth; + } + if (sci_mode) { + opts.sci_mode = *sci_mode; + } +} + +void Tensor::Print(std::ostream &os) const { + /* + Print tensor in torch.tensor/np.array style. + */ + CHECK(dtype_ == DataType::kFLOAT32); + + const size_t num_elements = NumElements(); + const size_t num_bytes = num_elements * sizeof(float); + + std::vector host_buffer(num_elements); + + if (GetDevice().Type() == DeviceType::kCPU) { + std::memcpy(host_buffer.data(), DataPtr(), num_bytes); + } +#ifdef USE_CUDA + else if (GetDevice().Type() == DeviceType::kCUDA) { + cudaDeviceSynchronize(); + cudaError_t err = cudaMemcpy(host_buffer.data(), DataPtr(), num_bytes, cudaMemcpyDeviceToHost); + CHECK_EQ(err, cudaSuccess) << "cudaMemcpy failed: " << cudaGetErrorString(err); + } +#endif + else { + LOG(FATAL) << "Unsupported device type for Print."; + } + + const PrintOptions &opts = PrintOptions::Get(); + const int64_t precision = opts.precision; + const int64_t threshold = opts.threshold; + const int64_t edge_items = opts.edge_items; + const int64_t linewidth = opts.linewidth; + const int64_t base_indent = 8; // length of "tensor([" + + bool use_sci = opts.sci_mode.value_or(false); + if (!opts.sci_mode.has_value()) { + for (float v : host_buffer) { + float abs_v = std::fabs(v); + if ((abs_v > 0.0f && abs_v < 1e-4f) || abs_v >= 1e+4f) { + use_sci = true; + break; + } + } + } + + auto format_float = [&](float val) -> std::string { + std::ostringstream ss; + if (use_sci) { + ss << std::scientific << std::setprecision(precision); + } else { + ss << std::fixed << std::setprecision(precision); + } + ss << val; + return ss.str(); + }; + + std::vector str_vals(num_elements); + size_t max_width = 0; + for (size_t i = 0; i < num_elements; ++i) { + str_vals[i] = format_float(host_buffer[i]); + max_width = std::max(max_width, str_vals[i].length()); + } + + const int ndim = dims_.size(); + + std::function print_rec; + print_rec = [&](int dim, size_t offset, int indent) { + os << "["; + size_t step = 1; + for (int d = dim + 1; d < ndim; ++d) { step *= dims_[d]; } + int n = dims_[dim]; + + if (dim == ndim - 1) { + if (n <= 2 * edge_items || num_elements <= threshold) { + int line_len = base_indent + indent + 1; + for (int i = 0; i < n; ++i) { + if (i > 0) { + os << ", "; + line_len += 2; + } + std::string item = str_vals[offset + i]; + if (linewidth > 0 && line_len + max_width > linewidth) { + os << "\n" << std::string(base_indent + indent + 1, ' '); + line_len = base_indent + indent + 1; + } + os << std::setw(max_width) << item; + line_len += max_width; + } + } else { + int line_len = base_indent + indent + 1; + for (int i = 0; i < edge_items; ++i) { + if (i > 0) { + os << ", "; + line_len += 2; + } + std::string item = str_vals[offset + i]; + if (linewidth > 0 && line_len + max_width > linewidth) { + os << "\n" << std::string(base_indent + indent + 1, ' '); + line_len = base_indent + indent + 1; + } + os << std::setw(max_width) << item; + line_len += max_width; + } + os << ", ..."; + line_len += 5; // length of ", ..." + if (linewidth > 0 && line_len + max_width > linewidth) { + os << "\n" << std::string(base_indent + indent + 1, ' '); + line_len = base_indent + indent + 1; + } else { + os << ", "; + line_len += 2; + } + for (int i = n - edge_items; i < n; ++i) { + if (i > n - edge_items) { + os << ", "; + line_len += 2; + } + std::string item = str_vals[offset + i]; + if (linewidth > 0 && line_len + max_width > linewidth) { + os << "\n" << std::string(base_indent + indent + 1, ' '); + line_len = base_indent + indent + 1; + } + os << std::setw(max_width) << item; + line_len += max_width; + } + } + } else { + if (n <= 2 * edge_items || num_elements <= threshold) { + for (int i = 0; i < n; ++i) { + if (i > 0) { + if (dim < ndim - 2) { + os << ",\n\n" << std::string(base_indent + indent, ' '); + } else { + os << ",\n" << std::string(base_indent + indent, ' '); + } + } + print_rec(dim + 1, offset + i * step, indent + 1); + } + } else { + for (int i = 0; i < edge_items; ++i) { + if (i > 0) { + if (dim < ndim - 2) { + os << ",\n\n" << std::string(base_indent + indent, ' '); + } else { + os << ",\n" << std::string(base_indent + indent, ' '); + } + } + print_rec(dim + 1, offset + i * step, indent + 1); + } + os << ",\n" + << std::string(base_indent + indent, ' ') << "...\n" + << std::string(base_indent + indent, ' '); + for (int i = n - edge_items; i < n; ++i) { + if (i > n - edge_items) { + if (dim < ndim - 2) { + os << ",\n\n" << std::string(base_indent + indent, ' '); + } else { + os << ",\n" << std::string(base_indent + indent, ' '); + } + } + print_rec(dim + 1, offset + i * step, indent + 1); + } + } + } + os << "]"; + }; + + os << "Tensor("; + if (num_elements == 0) { + os << "[], "; + } else { + print_rec(0, 0, 0); + os << ", \n"; + } + + os << std::string(base_indent - 1, ' ') << "dtype=float32, shape=("; + for (size_t i = 0; i < dims_.size(); ++i) { + if (i > 0) { + os << ", "; + } + os << dims_[i]; + } + if (dims_.size() == 1) { + os << ","; + } + os << "))\n"; +} +} // namespace infini_train