From 275537f90fbd76a34a38257d6f17d8bf956a3b22 Mon Sep 17 00:00:00 2001 From: runwangdl Date: Mon, 13 Apr 2026 22:22:03 +0000 Subject: [PATCH 01/14] [NE16] Add GAP9_w_NE16 platform: NE16 accelerator Engine on GAP9 Mirrors the Siracusa_w_neureka pattern. NE16Platform extends GAP9Platform with engines=[NE16Engine, GAP9ClusterEngine]; NE16Deployer extends GAP9Deployer (reuses ClDma transformers via GAP9Bindings). New Target: Deeploy/Targets/NE16/ (Platform, Engine, Bindings, Parsers, Tiler, Deployer, Templates, TileConstraints, TopologyOptimizationPasses). The _weightEncode function is ported from pulp-nnx/test/Ne16Weight.py (single CIN_SUBTILE=16 mode, no 1x1 vs 3x3 split). ConvTemplate subtile constants set per ne16_task_defs.h (output 3x3, weight stride bytes PW=16 DW/Dense=144). New test infrastructure: - DeeployTest/deeployRunner_tiled_gap9_w_ne16.py - DeeployTest/test_gap9_ne16_tiled_config.py (PW/DW/Dense RQ Conv) DeeployTest wiring: - testUtils/platformMapping.py: register GAP9_w_NE16 in the platforms list, mapPlatform, setupMemoryPlatform, mapDeployer. - testMVP.py: include GAP9_w_NE16 in the EngineColoringDeployerWrapper branch (without it NE16AdjustWeightMemoryLayoutPass never fires and parsing backtracks to exhaustion). - testUtils/core/execution.py: build the GAP9 SDK 'image' target for GAP9_w_NE16 too (so chip.soc.mram.bin is produced before gvsoc run). - CMakeLists.txt, DeeployTest/CMakeLists.txt: accept GAP9_w_NE16 alongside GAP9 in the platform branches. - TargetLibraries/GAP9/CMakeLists.txt: for GAP9_w_NE16 platform, add_subdirectory on pulp-nnx with USE_NE16=ON and link it into deeploygap9. Fix: Deeploy/Targets/PULPOpen/Templates/FloatGemmTemplate.py referenced an undefined symbol float32_tPtr from Deeploy.AbstractDataTypes; define it locally via PointerClass(float32_t) to unblock the import chain reached by NE16Platform. Verified on gvsoc gap9.evk: PW 1x1 RQ (Regular_RQ): 0/1152 errors, 901917 cycles DW 3x3 RQ (DW_2D_RQ): 0/1280 errors, 27339 cycles (--enable-3x3) Dense 3x3 (Regular_2D_RQ): 0/6372 errors, 244595 cycles (--enable-3x3) --- .../workflows/_runner-gap9-w-ne16-tiled.yml | 54 +++ .../ci-platform-gap9-w-ne16-tiled.yml | 46 ++ CMakeLists.txt | 12 +- Deeploy/Targets/NE16/Bindings.py | 72 ++++ Deeploy/Targets/NE16/Deployer.py | 40 ++ Deeploy/Targets/NE16/Engine.py | 81 ++++ .../MemoryLevelAnnotationPasses.py | 45 ++ .../NE16/OptimizationPasses/__init__.py | 5 + Deeploy/Targets/NE16/Parsers.py | 203 +++++++++ Deeploy/Targets/NE16/Platform.py | 63 +++ .../NE16/Templates/AllocateTemplate.py | 18 + .../Targets/NE16/Templates/ConvTemplate.py | 398 ++++++++++++++++++ Deeploy/Targets/NE16/Templates/__init__.py | 5 + .../TileConstraints/NE16DenseConstraint.py | 268 ++++++++++++ .../NE16DepthwiseConstraint.py | 265 ++++++++++++ .../NE16PointwiseConstraint.py | 298 +++++++++++++ .../NE16/TileConstraints/RequantHelpers.py | 53 +++ .../Targets/NE16/TileConstraints/__init__.py | 5 + Deeploy/Targets/NE16/Tiler.py | 29 ++ .../NE16/TopologyOptimizationPasses/Passes.py | 278 ++++++++++++ .../TopologyOptimizationPasses/__init__.py | 5 + Deeploy/Targets/NE16/__init__.py | 5 + .../PULPOpen/Templates/FloatGemmTemplate.py | 5 +- DeeployTest/CMakeLists.txt | 2 +- .../Integer/Conv/Dense_2D_RQ/inputs.npz | Bin 0 -> 8456 bytes .../Integer/Conv/Dense_2D_RQ/network.onnx | Bin 0 -> 9817 bytes .../Integer/Conv/Dense_2D_RQ/outputs.npz | Bin 0 -> 8458 bytes DeeployTest/conftest.py | 1 + .../deeployRunner_tiled_gap9_w_ne16.py | 22 + DeeployTest/testMVP.py | 2 +- DeeployTest/testUtils/core/execution.py | 2 +- DeeployTest/testUtils/platformMapping.py | 26 +- DeeployTest/test_gap9_ne16_tiled_config.py | 38 ++ DeeployTest/test_platforms.py | 106 +++++ TargetLibraries/GAP9/CMakeLists.txt | 16 + 35 files changed, 2457 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/_runner-gap9-w-ne16-tiled.yml create mode 100644 .github/workflows/ci-platform-gap9-w-ne16-tiled.yml create mode 100644 Deeploy/Targets/NE16/Bindings.py create mode 100644 Deeploy/Targets/NE16/Deployer.py create mode 100644 Deeploy/Targets/NE16/Engine.py create mode 100644 Deeploy/Targets/NE16/OptimizationPasses/MemoryLevelAnnotationPasses.py create mode 100644 Deeploy/Targets/NE16/OptimizationPasses/__init__.py create mode 100644 Deeploy/Targets/NE16/Parsers.py create mode 100644 Deeploy/Targets/NE16/Platform.py create mode 100644 Deeploy/Targets/NE16/Templates/AllocateTemplate.py create mode 100644 Deeploy/Targets/NE16/Templates/ConvTemplate.py create mode 100644 Deeploy/Targets/NE16/Templates/__init__.py create mode 100644 Deeploy/Targets/NE16/TileConstraints/NE16DenseConstraint.py create mode 100644 Deeploy/Targets/NE16/TileConstraints/NE16DepthwiseConstraint.py create mode 100644 Deeploy/Targets/NE16/TileConstraints/NE16PointwiseConstraint.py create mode 100644 Deeploy/Targets/NE16/TileConstraints/RequantHelpers.py create mode 100644 Deeploy/Targets/NE16/TileConstraints/__init__.py create mode 100644 Deeploy/Targets/NE16/Tiler.py create mode 100644 Deeploy/Targets/NE16/TopologyOptimizationPasses/Passes.py create mode 100644 Deeploy/Targets/NE16/TopologyOptimizationPasses/__init__.py create mode 100644 Deeploy/Targets/NE16/__init__.py create mode 100644 DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ/inputs.npz create mode 100644 DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ/network.onnx create mode 100644 DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ/outputs.npz create mode 100644 DeeployTest/deeployRunner_tiled_gap9_w_ne16.py create mode 100644 DeeployTest/test_gap9_ne16_tiled_config.py diff --git a/.github/workflows/_runner-gap9-w-ne16-tiled.yml b/.github/workflows/_runner-gap9-w-ne16-tiled.yml new file mode 100644 index 0000000000..bff85b1b95 --- /dev/null +++ b/.github/workflows/_runner-gap9-w-ne16-tiled.yml @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2026 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +--- +name: _runner-gap9-w-ne16-tiled + +"on": + workflow_call: + inputs: + runner: + required: true + type: string + docker-image: + required: true + type: string + pytest-markers: + required: true + type: string + +jobs: + test-runner-gap9-w-ne16-tiled: + runs-on: ${{ inputs.runner }} + container: + image: ${{ inputs.docker-image }} + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build Deeploy + shell: bash + run: | + source /app/install/gap9-sdk/.gap9-venv/bin/activate + source /app/install/gap9-sdk/configs/gap9_evk_audio.sh || true + pip install -e . || true + deactivate + - name: Cache ccache + uses: actions/cache/restore@v4 + with: + path: /app/.ccache + key: ccache-gap9 + - name: Run Test + run: | + source /app/install/gap9-sdk/.gap9-venv/bin/activate + source /app/install/gap9-sdk/configs/gap9_evk_audio.sh || true + export GVSOC_INSTALL_DIR=/app/install/gap9-sdk/install/workstation + export GAP_RISCV_GCC_TOOLCHAIN=/app/install/gcc/gap9 + cd DeeployTest + mkdir -p /app/.ccache + export CCACHE_DIR=/app/.ccache + pytest test_platforms.py -v -m "${{ inputs.pytest-markers }}" + deactivate + shell: bash diff --git a/.github/workflows/ci-platform-gap9-w-ne16-tiled.yml b/.github/workflows/ci-platform-gap9-w-ne16-tiled.yml new file mode 100644 index 0000000000..5f45bbafeb --- /dev/null +++ b/.github/workflows/ci-platform-gap9-w-ne16-tiled.yml @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2026 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +--- +name: CI • GAP9 + NE16 (Tiled) + +"on": + push: + branches: + - "**" + tags: + - "v*.*.*" + pull_request: + workflow_dispatch: + inputs: + docker_image_deeploy: + description: "Deeploy Image to use" + required: false + default: "ghcr.io/pulp-platform/deeploy-gap9:latest" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + select-env: + uses: ./.github/workflows/_select-env.yml + with: + docker_image_deeploy: ${{ github.event.inputs.docker_image_deeploy || github.repository == 'pulp-platform/Deeploy' && 'ghcr.io/pulp-platform/deeploy-gap9:latest'}} + + gap9-w-ne16-kernels-tiled-singlebuffer-L2: + needs: select-env + uses: ./.github/workflows/_runner-gap9-w-ne16-tiled.yml + with: + runner: ${{ needs.select-env.outputs.runner }} + docker-image: ${{ needs.select-env.outputs.image }} + pytest-markers: "gap9_w_ne16_tiled and kernels and singlebuffer and l2" + + gap9-w-ne16-kernels-tiled-doublebuffer-L2: + needs: select-env + uses: ./.github/workflows/_runner-gap9-w-ne16-tiled.yml + with: + runner: ${{ needs.select-env.outputs.runner }} + docker-image: ${{ needs.select-env.outputs.image }} + pytest-markers: "gap9_w_ne16_tiled and kernels and doublebuffer and l2" diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e07d64a9e..3fcbb7800b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,8 +20,8 @@ if(TOOLCHAIN STREQUAL GCC) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) endif() -set(platform MemPool CACHE STRING "Platform (MemPool, SoftHier, QEMU, Siracusa, Siracusa_w_neureka, PULP-Open, GAP9, Generic, Snitch)") -set_property(CACHE platform PROPERTY STRINGS MemPool SoftHier QEMU Siracusa Siracusa_w_neureka PULP-Open GAP9 Generic Snitch) +set(platform MemPool CACHE STRING "Platform (MemPool, SoftHier, QEMU, Siracusa, Siracusa_w_neureka, PULP-Open, GAP9, GAP9_w_NE16, Generic, Snitch)") +set_property(CACHE platform PROPERTY STRINGS MemPool SoftHier QEMU Siracusa Siracusa_w_neureka PULP-Open GAP9 GAP9_w_NE16 Generic Snitch) if(platform STREQUAL MemPool) message(STATUS "Building for platform 'MemPool'") @@ -33,8 +33,8 @@ elseif(platform STREQUAL Siracusa_w_neureka) message(STATUS "Building for platform 'Siracusa_w_neureka'") elseif(platform STREQUAL PULPOpen) message(STATUS "Building for platform 'PULP-Open'") -elseif(platform STREQUAL GAP9) - message(STATUS "Building for platform 'GAP9'") +elseif(platform STREQUAL GAP9 OR platform STREQUAL GAP9_w_NE16) + message(STATUS "Building for platform '${platform}'") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # Select SDK config based on simulator type @@ -62,7 +62,7 @@ endif() # Import useful functions / macros include(${CMAKE_CURRENT_LIST_DIR}/cmake/Util.cmake) # Only if not GAP9 -if(NOT platform STREQUAL GAP9) +if(NOT platform STREQUAL GAP9 AND NOT platform STREQUAL GAP9_w_NE16) include(${CMAKE_CURRENT_LIST_DIR}/cmake/common.cmake) endif() include(${CMAKE_CURRENT_LIST_DIR}/cmake/simulation.cmake) @@ -231,7 +231,7 @@ if(platform STREQUAL Siracusa OR platform STREQUAL Siracusa_w_neureka OR platfor endif() -if(platform STREQUAL GAP9) +if(platform STREQUAL GAP9 OR platform STREQUAL GAP9_w_NE16) project(${TESTNAME} LANGUAGES C ASM) include(${CMAKE_CURRENT_LIST_DIR}/cmake/gap9/gap9_gvsoc.cmake) include(${CMAKE_CURRENT_LIST_DIR}/cmake/gap9/gap9_board.cmake) diff --git a/Deeploy/Targets/NE16/Bindings.py b/Deeploy/Targets/NE16/Bindings.py new file mode 100644 index 0000000000..58db14aee3 --- /dev/null +++ b/Deeploy/Targets/NE16/Bindings.py @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from Deeploy.AbstractDataTypes import PointerClass +from Deeploy.CommonExtensions.DataTypes import int8_t, int32_t, uint8_t +from Deeploy.DeeployTypes import NodeBinding +from Deeploy.Targets.GAP9.Bindings import GAP9ClusterTransformer as ClusterTransformer +from Deeploy.Targets.Generic.TypeCheckers import ConvChecker +from Deeploy.Targets.NE16.Templates.ConvTemplate import NE16DenseConv2D_Template, NE16DWConv2D_Template, \ + NE16PWConv2D_Template, NE16RqntDenseConv2D_Template, NE16RqntDWConv2D_Template, NE16RqntPWConv2D_Template +from Deeploy.Targets.PULPOpen.TypeCheckers import PULPConvChecker + +NE16RQSPWConv2DBindings = [ + NodeBinding( + PULPConvChecker( + [PointerClass(data_in_type), + PointerClass(weight_type), + PointerClass(int32_t), + PointerClass(int32_t)], [PointerClass(data_out_type)]), NE16RqntPWConv2D_Template, ClusterTransformer) + for data_in_type in [uint8_t, int8_t] + for data_out_type in [uint8_t, int8_t] + for weight_type in [uint8_t, int8_t] +] +NE16PWConv2DBindings = [ + NodeBinding( + ConvChecker( + [PointerClass(data_in_type), PointerClass(weight_type), + PointerClass(int32_t)], [PointerClass(int32_t)]), NE16PWConv2D_Template, ClusterTransformer) + for data_in_type in [uint8_t, int8_t] + for weight_type in [uint8_t, int8_t] +] + +NE16RQSDWConv2DBindings = [ + NodeBinding( + PULPConvChecker( + [PointerClass(data_in_type), + PointerClass(weight_type), + PointerClass(int32_t), + PointerClass(int32_t)], [PointerClass(data_out_type)]), NE16RqntDWConv2D_Template, ClusterTransformer) + for data_in_type in [uint8_t, int8_t] + for data_out_type in [uint8_t, int8_t] + for weight_type in [uint8_t, int8_t] +] +NE16DWConv2DBindings = [ + NodeBinding( + ConvChecker( + [PointerClass(data_in_type), PointerClass(weight_type), + PointerClass(int32_t)], [PointerClass(int32_t)]), NE16DWConv2D_Template, ClusterTransformer) + for data_in_type in [uint8_t, int8_t] + for weight_type in [uint8_t, int8_t] +] + +NE16RQSDenseConv2DBindings = [ + NodeBinding( + PULPConvChecker( + [PointerClass(data_in_type), + PointerClass(weight_type), + PointerClass(int32_t), + PointerClass(int32_t)], [PointerClass(data_out_type)]), NE16RqntDenseConv2D_Template, ClusterTransformer) + for data_in_type in [uint8_t, int8_t] + for data_out_type in [uint8_t, int8_t] + for weight_type in [uint8_t, int8_t] +] +NE16DenseConv2DBindings = [ + NodeBinding( + ConvChecker( + [PointerClass(data_in_type), PointerClass(weight_type), + PointerClass(int32_t)], [PointerClass(int32_t)]), NE16DenseConv2D_Template, ClusterTransformer) + for data_in_type in [uint8_t, int8_t] + for weight_type in [uint8_t, int8_t] +] diff --git a/Deeploy/Targets/NE16/Deployer.py b/Deeploy/Targets/NE16/Deployer.py new file mode 100644 index 0000000000..7f5d6c9748 --- /dev/null +++ b/Deeploy/Targets/NE16/Deployer.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Callable, Dict, Type + +import onnx_graphsurgeon as gs + +from Deeploy.AbstractDataTypes import Pointer +from Deeploy.CommonExtensions.OptimizationPasses.TopologyOptimizationPasses.LoweringOptimizationPasses import \ + NCHWtoNHWCPass, PULPNCHWtoNHWCPass +from Deeploy.DeeployTypes import DeploymentPlatform, TopologyOptimizer +from Deeploy.Targets.GAP9.Deployer import GAP9Deployer +from Deeploy.Targets.NE16.TopologyOptimizationPasses.Passes import ConvEngineDiscolorationPass, NE16OptimizationPass + + +class NE16Deployer(GAP9Deployer): + + def __init__(self, + graph: gs.Graph, + deploymentPlatform: DeploymentPlatform, + inputTypes: Dict[str, Type[Pointer]], + loweringOptimizer: TopologyOptimizer, + scheduler: Callable = lambda graph: list(graph.nodes), + name: str = 'DeeployNetwork', + default_channels_first = False, + deeployStateDir: str = "DeeployStateDir", + inputOffsets = {}): + super().__init__(graph, deploymentPlatform, inputTypes, loweringOptimizer, scheduler, name, + default_channels_first, deeployStateDir, inputOffsets) + + if self.Platform.engines[0].enable3x3: + for idx in range(len(self.loweringOptimizer.passes)): + if isinstance(self.loweringOptimizer.passes[idx], PULPNCHWtoNHWCPass): + self.loweringOptimizer.passes[idx] = NCHWtoNHWCPass(self.default_channels_first) + + self.loweringOptimizer.passes += [ + ConvEngineDiscolorationPass(), + NE16OptimizationPass(self.default_channels_first, "NE16") + ] diff --git a/Deeploy/Targets/NE16/Engine.py b/Deeploy/Targets/NE16/Engine.py new file mode 100644 index 0000000000..48f5bca284 --- /dev/null +++ b/Deeploy/Targets/NE16/Engine.py @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import List + +import onnx_graphsurgeon as gs + +from Deeploy.DeeployTypes import DeploymentEngine, NodeMapper +from Deeploy.Targets.Generic.Layers import ConvLayer +from Deeploy.Targets.NE16.Parsers import NE16DenseConv2DParser, NE16DWConv2DParser, NE16PWConv2DParser, \ + NE16RQSDenseConv2DParser, NE16RQSDWConv2DParser, NE16RQSPWConv2DParser +from Deeploy.Targets.NE16.Tiler import NE16DenseConv2DTilingReadyBindings, NE16DWConv2DTilingReadyBindings, \ + NE16PWConv2DTilingReadyBindings, NE16RQSDenseConv2DTilingReadyBindings, NE16RQSDWConv2DTilingReadyBindings, \ + NE16RQSPWConv2DTilingReadyBindings +from Deeploy.Targets.PULPOpen.Layers import PULPRQSConvLayer + +NE16RqntPWConv2DMapper = NodeMapper(NE16RQSPWConv2DParser(), NE16RQSPWConv2DTilingReadyBindings) +NE16PWConv2DMapper = NodeMapper(NE16PWConv2DParser(), NE16PWConv2DTilingReadyBindings) + +NE16RqntDWConv2DMapper = NodeMapper(NE16RQSDWConv2DParser(), NE16RQSDWConv2DTilingReadyBindings) +NE16DWConv2DMapper = NodeMapper(NE16DWConv2DParser(), NE16DWConv2DTilingReadyBindings) + +NE16RqntDenseConv2DMapper = NodeMapper(NE16RQSDenseConv2DParser(), NE16RQSDenseConv2DTilingReadyBindings) +NE16DenseConv2DMapper = NodeMapper(NE16DenseConv2DParser(), NE16DenseConv2DTilingReadyBindings) + +NE16Mapping = { + 'RequantizedConv': PULPRQSConvLayer([NE16RqntPWConv2DMapper, NE16RqntDWConv2DMapper, NE16RqntDenseConv2DMapper]), + 'Conv': ConvLayer([NE16PWConv2DMapper, NE16DWConv2DMapper, NE16DenseConv2DMapper]), +} + +_includeList = ["pulp_nnx_ne16.h", "pulp_nnx_util.h", "ne16_pulp_bsp.h", "ne16.h", "ne16_task.h"] + +_ne16InitCode = r""" +ne16_pulp_conf_t conf = {.max_stall = 8}; +ne16_nnx_init(ne16_pulp_get_dev(), &conf); +""" + + +class NE16Engine(DeploymentEngine): + + def __init__(self, + name: str, + Mapping = NE16Mapping, + initCode: str = _ne16InitCode, + includeList: List[str] = _includeList, + enable3x3: bool = False, + enableStrides: bool = False) -> None: + super().__init__(name, Mapping, initCode, includeList) + + self.enable3x3 = enable3x3 + self.enableStrides = enableStrides + + def isDenseConv(self, node) -> bool: + return node.op in ["Conv", "RequantizedConv"] and \ + isinstance(node.inputs[1], gs.Constant) and \ + node.attrs['kernel_shape'] == [3, 3] and \ + node.attrs['dilations'] == [1, 1] and \ + node.attrs['group'] == 1 and \ + (node.attrs['strides'] == [1, 1] or self.enableStrides) + + def isPWConv(self, node) -> bool: + return node.op in ["Conv", "RequantizedConv"] and \ + isinstance(node.inputs[1], gs.Constant) and \ + node.attrs['kernel_shape'] == [1, 1] and \ + node.attrs['dilations'] == [1, 1] and \ + (node.attrs['strides'] == [1, 1] or self.enableStrides) + + def isDWConv(self, node) -> bool: + return node.op in ["Conv", "RequantizedConv"] and \ + isinstance(node.inputs[1], gs.Constant) and \ + node.attrs['kernel_shape'] == [3, 3] and \ + node.attrs['dilations'] == [1, 1] and \ + node.attrs['group'] != 1 and \ + (node.attrs['strides'] == [1, 1] or self.enableStrides) + + def canExecute(self, node: gs.Node) -> bool: + if self.enable3x3: + return self.isPWConv(node) or self.isDWConv(node) or self.isDenseConv(node) + else: + return self.isPWConv(node) diff --git a/Deeploy/Targets/NE16/OptimizationPasses/MemoryLevelAnnotationPasses.py b/Deeploy/Targets/NE16/OptimizationPasses/MemoryLevelAnnotationPasses.py new file mode 100644 index 0000000000..b6a530a319 --- /dev/null +++ b/Deeploy/Targets/NE16/OptimizationPasses/MemoryLevelAnnotationPasses.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2023 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Tuple + +import numpy as np +import onnx_graphsurgeon as gs + +from Deeploy.CommonExtensions.OptimizationPasses.PassClasses import SequentialPass +from Deeploy.DeeployTypes import ConstantBuffer, NetworkContext +from Deeploy.MemoryLevelExtension.MemoryLevels import MemoryLevel + + +class AnnotateNE16WeightMemoryLevel(SequentialPass): + + def __init__(self, ne16EngineName: str, weightMemoryLevel: MemoryLevel): + self._weightMemoryLevel = weightMemoryLevel + self.ne16EngineName = ne16EngineName + super().__init__() + + def apply(self, ctxt: NetworkContext, graph: gs.Graph) -> Tuple[NetworkContext, gs.Graph]: + + def _ne16WeightBufferSize(buffer: ConstantBuffer) -> int: + return int(np.prod(buffer.shape)) # Weights are encoded as bytes so no need to check for typeWidth + + weightMemoryOccupation = 0 + + # Current weight memory occupation + for buffer in {**ctxt.globalObjects, **ctxt.localObjects}.values(): + if hasattr(buffer, "_memoryLevel") and buffer._memoryLevel == self._weightMemoryLevel.name: + weightMemoryOccupation += _ne16WeightBufferSize(buffer) + + ne16Nodes = [node for node in graph.nodes if node.attrs["engine"] == self.ne16EngineName] + for node in ne16Nodes: + if node.op in ["Conv", "RequantizedConv"]: + + if not (ctxt.is_local(node.inputs[1].name) or ctxt.is_global(node.inputs[1].name)): + continue + + buffer = ctxt.lookup(node.inputs[1].name) + if weightMemoryOccupation + _ne16WeightBufferSize(buffer) < self._weightMemoryLevel.size: + buffer._memoryLevel = self._weightMemoryLevel.name + weightMemoryOccupation += _ne16WeightBufferSize(buffer) + return ctxt, graph diff --git a/Deeploy/Targets/NE16/OptimizationPasses/__init__.py b/Deeploy/Targets/NE16/OptimizationPasses/__init__.py new file mode 100644 index 0000000000..be436b64a3 --- /dev/null +++ b/Deeploy/Targets/NE16/OptimizationPasses/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from . import * diff --git a/Deeploy/Targets/NE16/Parsers.py b/Deeploy/Targets/NE16/Parsers.py new file mode 100644 index 0000000000..3d157114fc --- /dev/null +++ b/Deeploy/Targets/NE16/Parsers.py @@ -0,0 +1,203 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Tuple + +import onnx_graphsurgeon as gs + +from Deeploy.DeeployTypes import NetworkContext +from Deeploy.Targets.Generic.Parsers import Conv2DParser, ConvParser, RQSParserInterface + + +class NE16Conv2DBaseParser(Conv2DParser): + + def parseNode(self, node: gs.Node) -> bool: + if not super().parseNode(node): + return False + + if not all([ + # No dilation support + self.operatorRepresentation['dilations'] == [1, 1], + # Channels have to be last + 'channels_first' in self.operatorRepresentation and not self.operatorRepresentation['channels_first'], + # Expect "weight_offset" attribute in the node + "weight_offset" in node.attrs, + ]): + return False + + self.operatorRepresentation['padding_y_top'] = int(self.operatorRepresentation['pads'][0]) + self.operatorRepresentation['padding_x_left'] = int(self.operatorRepresentation['pads'][1]) + self.operatorRepresentation['padding_y_bottom'] = int(self.operatorRepresentation['pads'][2]) + self.operatorRepresentation['padding_x_right'] = int(self.operatorRepresentation['pads'][3]) + self.operatorRepresentation['weight_offset'] = int(node.attrs["weight_offset"]) + + return True + + def parseNodeCtxt(self, + ctxt: NetworkContext, + node: gs.Node, + channels_first: bool = True) -> Tuple[NetworkContext, bool]: + # LMACAN: Cannot reuse the Conv2DParser's parserNodeCtxt because it requires the weight shape + # to be of length 4 whereas ne16 does a specific weight encoding so the shape + # ends up being equal to 3. + newCtxt, ret = ConvParser.parseNodeCtxt(self, ctxt, node, channels_first) + + if not ret: + return ctxt, False + + # LMACAN: c/p of Conv2DParser's parserNodeCtxt but with a different weight shape check + # and enforcing that the channels_first is false + data_in = newCtxt.lookup(self.operatorRepresentation['data_in']) + data_out = newCtxt.lookup(self.operatorRepresentation['data_out']) + weight = newCtxt.lookup(self.operatorRepresentation['weight']) + + if not all([ + channels_first == False, + len(data_in.shape) == 4, + # LMACAN: weight shape should be equal to 3 because we have to do the ne16's + # special weight encoding. Dense 3x3 uses rank 4, + # PW/DW use rank 3. + len(weight.shape) in (3, 4), + ]): + return newCtxt, False + + self.operatorRepresentation['batch'] = data_in.shape[0] + self.operatorRepresentation['dim_im_in_x'] = data_in.shape[1] + self.operatorRepresentation['dim_im_in_y'] = data_in.shape[2] + self.operatorRepresentation['ch_im_in'] = data_in.shape[3] + self.operatorRepresentation['dim_im_out_x'] = data_out.shape[1] + self.operatorRepresentation['dim_im_out_y'] = data_out.shape[2] + self.operatorRepresentation['ch_im_out'] = data_out.shape[3] + + # No requantization + self.operatorRepresentation['mul'] = 'NULL' + self.operatorRepresentation['add'] = 'NULL' + self.operatorRepresentation['shift'] = 'NULL' + + return newCtxt, True + + +class NE16DWConv2DParser(NE16Conv2DBaseParser): + + def parseNode(self, node: gs.Node) -> bool: + if not super().parseNode(node): + return False + + # After NE16 weight encoding for DW, the encoded weight shape no longer + # carries cout==group (all channels are packed into the cinMinor + # dimension). Trust the ONNX `group` attribute alone: for DW, + # group > 1 AND group == channel_out AND kernel_shape == [3,3]. + if not all([ + self.operatorRepresentation['kernel_shape'] == [3, 3], + self.operatorRepresentation['group'] > 1, + ]): + return False + + return True + + +class NE16RQSDWConv2DParser(NE16DWConv2DParser, RQSParserInterface): + + def parseNode(self, node: gs.Node) -> bool: + ret = all([ + RQSParserInterface.parseNode(self, node), + NE16DWConv2DParser.parseNode(self, node), + ]) + + return ret + + def parseNodeCtxt(self, + ctxt: NetworkContext, + node: gs.Node, + channels_first: bool = True) -> Tuple[NetworkContext, bool]: + newCtxt, ret = super().parseNodeCtxt(ctxt, node, channels_first) + + if not ret: + return ctxt, False + + inputs = ['data_in', 'weight', 'mul', 'add'] + for idx, inputNode in enumerate(node.inputs): + self.operatorRepresentation[inputs[idx]] = ctxt.lookup(inputNode.name).name + + return newCtxt, True + + +class NE16PWConv2DParser(NE16Conv2DBaseParser): + + def parseNode(self, node: gs.Node) -> bool: + if not super().parseNode(node): + return False + + if not all([ + self.operatorRepresentation['kernel_shape'] == [1, 1], + self.operatorRepresentation['group'] == 1, + ]): + return False + + return True + + +class NE16RQSPWConv2DParser(NE16PWConv2DParser, RQSParserInterface): + + def parseNode(self, node: gs.Node) -> bool: + ret = all([ + RQSParserInterface.parseNode(self, node), + NE16PWConv2DParser.parseNode(self, node), + ]) + return ret + + def parseNodeCtxt(self, + ctxt: NetworkContext, + node: gs.Node, + channels_first: bool = True) -> Tuple[NetworkContext, bool]: + newCtxt, ret = super().parseNodeCtxt(ctxt, node, channels_first) + + if not ret: + return ctxt, False + + inputs = ['data_in', 'weight', 'mul', 'add'] + for idx, inputNode in enumerate(node.inputs): + self.operatorRepresentation[inputs[idx]] = ctxt.lookup(inputNode.name).name + + return newCtxt, True + + +class NE16DenseConv2DParser(NE16Conv2DBaseParser): + + def parseNode(self, node: gs.Node) -> bool: + if not super().parseNode(node): + return False + + if not all([ + self.operatorRepresentation['kernel_shape'] == [3, 3], + self.operatorRepresentation['group'] == 1, + ]): + return False + + return True + + +class NE16RQSDenseConv2DParser(NE16DenseConv2DParser, RQSParserInterface): + + def parseNode(self, node: gs.Node) -> bool: + ret = all([ + RQSParserInterface.parseNode(self, node), + NE16DenseConv2DParser.parseNode(self, node), + ]) + return ret + + def parseNodeCtxt(self, + ctxt: NetworkContext, + node: gs.Node, + channels_first: bool = True) -> Tuple[NetworkContext, bool]: + newCtxt, ret = super().parseNodeCtxt(ctxt, node, channels_first) + + if not ret: + return ctxt, False + + inputs = ['data_in', 'weight', 'mul', 'add'] + for idx, inputNode in enumerate(node.inputs): + self.operatorRepresentation[inputs[idx]] = ctxt.lookup(inputNode.name).name + + return newCtxt, True diff --git a/Deeploy/Targets/NE16/Platform.py b/Deeploy/Targets/NE16/Platform.py new file mode 100644 index 0000000000..2c6fddf8e5 --- /dev/null +++ b/Deeploy/Targets/NE16/Platform.py @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Optional + +from Deeploy.CommonExtensions.OptimizationPasses.TopologyOptimizationPasses.LoweringOptimizationPasses import \ + RequantizedGemmToPwPass +from Deeploy.DeeployTypes import TopologyOptimizer +from Deeploy.MemoryLevelExtension.MemoryLevels import MemoryHierarchy, MemoryLevel +from Deeploy.Targets.GAP9.Platform import GAP9ClusterEngine, GAP9ConstantBuffer, GAP9Platform, GAP9StructBuffer, \ + GAP9TransientBuffer, GAP9VariableBuffer, MemoryGAP9Platform, MemoryGAP9PlatformWrapper +from Deeploy.Targets.NE16.Engine import NE16Engine +from Deeploy.Targets.PULPOpen.Platform import PULPOptimizer + +NE16Optimizer = TopologyOptimizer([ + *PULPOptimizer.passes, + RequantizedGemmToPwPass(), +], name = "NE16Optimizer") + + +class NE16Platform(GAP9Platform): + + def __init__(self, + engines = None, + variableBuffer = GAP9VariableBuffer, + constantBuffer = GAP9ConstantBuffer, + structBuffer = GAP9StructBuffer, + transientBuffer = GAP9TransientBuffer) -> None: + if engines is None: + engines = [NE16Engine("NE16"), GAP9ClusterEngine("GAP9Cluster")] + super().__init__(engines, variableBuffer, constantBuffer, structBuffer, transientBuffer) + + +class MemoryNE16Platform(MemoryGAP9Platform): + + def __init__(self, + memoryHierarchy: MemoryHierarchy, + defaultTargetMemoryLevel: MemoryLevel, + weightMemoryLevel: Optional[MemoryLevel] = None, + engines = None, + variableBuffer = GAP9VariableBuffer, + constantBuffer = GAP9ConstantBuffer, + structBuffer = GAP9StructBuffer, + transientBuffer = GAP9TransientBuffer) -> None: + if engines is None: + engines = [NE16Engine("NE16"), GAP9ClusterEngine("GAP9Cluster")] + super().__init__(memoryHierarchy, defaultTargetMemoryLevel, engines, variableBuffer, constantBuffer, + structBuffer, transientBuffer) + self.weightMemoryLevel = weightMemoryLevel + + +class MemoryNE16PlatformWrapper(MemoryGAP9PlatformWrapper): + + def __init__(self, + platform: NE16Platform, + memoryHierarchy: MemoryHierarchy, + defaultTargetMemoryLevel: MemoryLevel, + weightMemoryLevel: Optional[MemoryLevel] = None): + assert isinstance(platform, NE16Platform), \ + f"Given platform is not an instance of NE16Platform. Platform type: {type(platform).__name__}" + super().__init__(platform, memoryHierarchy, defaultTargetMemoryLevel) + self.weightMemoryLevel = weightMemoryLevel diff --git a/Deeploy/Targets/NE16/Templates/AllocateTemplate.py b/Deeploy/Targets/NE16/Templates/AllocateTemplate.py new file mode 100644 index 0000000000..502b5af578 --- /dev/null +++ b/Deeploy/Targets/NE16/Templates/AllocateTemplate.py @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2023 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from Deeploy.DeeployTypes import NodeTemplate + +ne16GenericGlobalInitTemplate = NodeTemplate(""" +% if _memoryLevel == "L1": +static PI_L1 ${type.referencedType.typeName} ${name}[${size}] = {${values}};\n +% elif _memoryLevel == "L2" or _memoryLevel is None: +static PI_L2 ${type.referencedType.typeName} ${name}[${size}] = {${values}};\n +% elif _memoryLevel == "L3": +// ${name} is allocated in L3 \n +static PI_L2 ${type.referencedType.typeName}* ${name}; +% elif _memoryLevel == "WeightMemory_SRAM": +static __attribute__((section(".weightmem_sram"))) ${type.referencedType.typeName} ${name}[${size}] = {${values}};\n +% endif +""") diff --git a/Deeploy/Targets/NE16/Templates/ConvTemplate.py b/Deeploy/Targets/NE16/Templates/ConvTemplate.py new file mode 100644 index 0000000000..337f5e10c4 --- /dev/null +++ b/Deeploy/Targets/NE16/Templates/ConvTemplate.py @@ -0,0 +1,398 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from abc import abstractmethod +from typing import Dict, List, Tuple + +import numpy as np + +from Deeploy.DeeployTypes import ConstantBuffer, NetworkContext, NodeTemplate, OperatorRepresentation + + +def _getNumTiles(fullDim: int, tileDim: int) -> int: + return int(np.ceil(fullDim / tileDim)) + + +def _getBorderTileSize(fullDim: int, tileDim: int) -> int: + return fullDim % tileDim if fullDim % tileDim > 0 else tileDim + + +def ioStridesFromDimensions(width: int, channel: int, bits: int) -> Tuple[int, int]: + """stridesFromDimensions + Returns strides in bytes. + """ + width_stride = channel * bits // 8 + height_stride = width * width_stride + return height_stride, width_stride + + +def getNormQuantConf0(use_relu: bool, layerwise_output_shift: int, scale_bits: int, use_bias: bool, + use_shift: bool) -> int: + conf0 = 0 + conf0 |= 1 << 4 # Use Normalization and quantization + if scale_bits == 32: + conf0 |= 2 << 12 + conf0 |= layerwise_output_shift << 16 + if not use_relu: + conf0 |= 1 << 23 + if use_shift: + conf0 |= 1 << 24 + if use_bias: + conf0 |= 1 << 25 + return conf0 + + +def getInputAddrOffset(width_in: int, width_in_stride: int, padding_top: int, padding_left: int) -> int: + return (padding_top * width_in + padding_left) * width_in_stride + + +class NE16ConvTemplate(NodeTemplate): + + def __init__(self, templateStr: str): + super().__init__(templateStr) + + @classmethod + @abstractmethod + def getCounters( + cls, channel_in: int, height_out: int, width_out: int, channel_out: int, padding_bottom: int, + padding_right: int, + operatorRepresentation: OperatorRepresentation) -> Tuple[int, int, int, int, int, int, int, int, int, int]: + pass + + @classmethod + @abstractmethod + def getWeightStrides(cls, channel_in: int) -> Tuple[int, int, int]: + pass + + @classmethod + @abstractmethod + def getConf0(cls, output_bits: int, weight_bits: int, input_signed: bool, use_wmem: bool) -> int: + pass + + def alignToContext(self, ctxt: NetworkContext, + operatorRepresentation: OperatorRepresentation) -> Tuple[NetworkContext, Dict, List[str]]: + data_in: ConstantBuffer = ctxt.lookup(operatorRepresentation['data_in']) + data_out: ConstantBuffer = ctxt.lookup(operatorRepresentation['data_out']) + weight: ConstantBuffer = ctxt.lookup(operatorRepresentation['weight']) + + operatorRepresentation['input_signed'] = data_in._type.referencedType.typeMin < 0 + operatorRepresentation['use_relu'] = data_out._type.referencedType.typeMin >= 0 + + operatorRepresentation['input_bits'] = data_in._type.referencedType.typeWidth + operatorRepresentation['output_bits'] = data_out._type.referencedType.typeWidth + operatorRepresentation['weight_bits'] = weight._type.referencedType.typeWidth + + operatorRepresentation["input_typeWidth_bytes"] = int(np.ceil(data_in._type.referencedType.typeWidth / 8)) + operatorRepresentation["output_typeWidth_bytes"] = int(np.ceil(data_out._type.referencedType.typeWidth / 8)) + + operatorRepresentation["weight_addr_offset"] = 0 + + operatorRepresentation["use_wmem"] = hasattr(weight, + "_memoryLevel") and weight._memoryLevel == "WeightMemory_SRAM" + + dim_im_in_x_stride, dim_im_in_y_stride = ioStridesFromDimensions(operatorRepresentation["dim_im_in_y"], + operatorRepresentation["ch_im_in"], + operatorRepresentation["input_bits"]) + operatorRepresentation["dim_im_in_y_stride"] = dim_im_in_y_stride + operatorRepresentation["dim_im_in_x_stride"] = dim_im_in_x_stride + + dim_im_out_x_stride, dim_im_out_y_stride = ioStridesFromDimensions(operatorRepresentation["dim_im_out_y"], + operatorRepresentation["ch_im_out"], + operatorRepresentation["output_bits"]) + operatorRepresentation["dim_im_out_y_stride"] = dim_im_out_y_stride + operatorRepresentation["dim_im_out_x_stride"] = dim_im_out_x_stride + + operatorRepresentation["input_addr_offset"] = getInputAddrOffset(operatorRepresentation["dim_im_in_y"], + operatorRepresentation["dim_im_in_y_stride"], + operatorRepresentation["padding_y_top"], + operatorRepresentation["padding_x_left"]) + + nKo, nKi, nHo, nWo, bKo, bKi, bHo, bWo, bHi, bWi = self.getCounters( + operatorRepresentation["ch_im_in"], operatorRepresentation["dim_im_out_x"], + operatorRepresentation["dim_im_out_y"], operatorRepresentation["ch_im_out"], + operatorRepresentation["padding_y_bottom"], operatorRepresentation["padding_x_right"], + operatorRepresentation) + + operatorRepresentation["nKo"] = nKo + operatorRepresentation["nKi"] = nKi + operatorRepresentation["nHo"] = nHo + operatorRepresentation["nWo"] = nWo + operatorRepresentation["bKo"] = bKo + operatorRepresentation["bKi"] = bKi + operatorRepresentation["bHo"] = bHo + operatorRepresentation["bWo"] = bWo + operatorRepresentation["bHi"] = bHi + operatorRepresentation["bWi"] = bWi + + weightStrideD0, weightStrideD1, weightStrideD2 = self.getWeightStrides(operatorRepresentation["ch_im_in"]) + + operatorRepresentation["weightStrideD0"] = weightStrideD0 + operatorRepresentation["weightStrideD1"] = weightStrideD1 + operatorRepresentation["weightStrideD2"] = weightStrideD2 + + operatorRepresentation["conf0"] = self.getConf0(operatorRepresentation["output_bits"], + operatorRepresentation["weight_bits"], + operatorRepresentation["input_signed"], + operatorRepresentation["use_wmem"]) + + operatorRepresentation["wmem_addr_offset"] = 0x10400000 if operatorRepresentation["use_wmem"] else 0 + + operatorRepresentation["ne16_kernel_shape"] = self.NE16_KERNEL_SHAPE + operatorRepresentation["ne16_depthwise"] = self.NE16_IS_DEPTHWISE + operatorRepresentation["ne16_subtile_output_channel"] = self.NE16_SUBTILE_OUTPUT_CHANNEL + + # If requantized + if operatorRepresentation["mul"] != "NULL": + mulBuff = ctxt.lookup(operatorRepresentation["mul"]) + mulBits = mulBuff._type.referencedType.typeWidth + operatorRepresentation["conf0"] |= getNormQuantConf0(operatorRepresentation["use_relu"], + operatorRepresentation["log2D"], mulBits, "add" + in operatorRepresentation, False) + return ctxt, operatorRepresentation, [] + + +class NE162DPWConvTemplate(NE16ConvTemplate): + + NE16_KERNEL_SHAPE = 1 + NE16_IS_DEPTHWISE = 0 + NE16_SUBTILE_OUTPUT_CHANNEL = 32 + + def __init__(self, templateStr: str): + super().__init__(templateStr) + + @classmethod + def getCounters( + cls, channel_in: int, height_out: int, width_out: int, channel_out: int, padding_bottom: int, + padding_right: int, + operatorRepresentation: OperatorRepresentation) -> Tuple[int, int, int, int, int, int, int, int, int, int]: + # NE16 subtiles: INPUT_CHANNEL=16, OUTPUT_HxW=3x3, OUTPUT_CHANNEL=32 + n_channel_out_subtiles = _getNumTiles(channel_out, 32) + n_channel_in_subtiles = _getNumTiles(channel_in, 16) + n_height_out_subtiles = _getNumTiles(height_out, 3) + n_width_out_subtiles = _getNumTiles(width_out, 3) + + channel_out_border = _getBorderTileSize(channel_out, 32) + channel_in_border = _getBorderTileSize(channel_in, 16) + height_out_border = _getBorderTileSize(height_out, 3) + width_out_border = _getBorderTileSize(width_out, 3) + height_in_border = height_out_border - padding_bottom + width_in_border = width_out_border - padding_right + + return (n_channel_out_subtiles, n_channel_in_subtiles, n_height_out_subtiles, n_width_out_subtiles, + channel_out_border, channel_in_border, height_out_border, width_out_border, height_in_border, + width_in_border) + + @classmethod + def getWeightStrides(cls, channel_in: int) -> Tuple[int, int, int]: + # NE16 PW 1x1: per (cout, cinMajor) block = bits * H*W * cinMinorBytes + # = 8 * 1 * 2 = 16 bytes for 8-bit weights with CIN_SUBTILE=16 + n_channel_in = _getNumTiles(channel_in, 16) + _NE16_PW_WEIGHT_BYTES = 16 # bits * HW * cinMinorBytes = 8*1*2 + return _NE16_PW_WEIGHT_BYTES, _NE16_PW_WEIGHT_BYTES * n_channel_in, 0 + + @classmethod + def getConf0(cls, output_bits: int, weight_bits: int, input_signed: bool, use_wmem: bool) -> int: + conf0 = 0 + conf0 |= weight_bits - 1 + conf0 |= 2 << 5 # PW MODE + if use_wmem: + conf0 |= 1 << 9 + conf0 |= 1 << 15 # Layerwise weight offset mode + if output_bits == 32: + conf0 |= 2 << 21 + if input_signed: + conf0 |= 1 << 26 + return conf0 + + +class NE162DDWConvTemplate(NE16ConvTemplate): + + NE16_KERNEL_SHAPE = 3 + NE16_IS_DEPTHWISE = 1 + # For DW, hardware replicates input channels as output channels, so the + # output-channel subtile size equals the input-channel subtile (16). + NE16_SUBTILE_OUTPUT_CHANNEL = 16 + + def __init__(self, templateStr: str): + super().__init__(templateStr) + + @classmethod + def getCounters( + cls, channel_in: int, height_out: int, width_out: int, channel_out: int, padding_bottom: int, + padding_right: int, + operatorRepresentation: OperatorRepresentation) -> Tuple[int, int, int, int, int, int, int, int, int, int]: + _ = operatorRepresentation # operatorRepresentation not accessed for now because it's just for pointwise kernels + + # NE16 DW 3x3: CIN_SUBTILE=16 single mode, output 3x3 + n_channel_out_subtiles = _getNumTiles(channel_out, 16) + n_channel_in_subtiles = n_channel_out_subtiles + n_height_out_subtiles = _getNumTiles(height_out, 3) + n_width_out_subtiles = _getNumTiles(width_out, 3) + + channel_out_border = _getBorderTileSize(channel_out, 16) + channel_in_border = channel_out_border + height_out_border = _getBorderTileSize(height_out, 3) + width_out_border = _getBorderTileSize(width_out, 3) + height_in_border = height_out_border + 2 - padding_bottom + width_in_border = width_out_border + 2 - padding_right + + return (n_channel_out_subtiles, n_channel_in_subtiles, n_height_out_subtiles, n_width_out_subtiles, + channel_out_border, channel_in_border, height_out_border, width_out_border, height_in_border, + width_in_border) + + @classmethod + def getWeightStrides(cls, channel_in: int) -> Tuple[int, int, int]: + # Match ne16_task_set_strides for depthwise 3x3: + # d0 = NE16_FILTER_SIZE * NE16_FILTER_SIZE * weight_d0_stride + # = 3 * 3 * 2 = 18 + # d1 = 0 (DW has no cin-major striding from the HW's perspective). + _NE16_FILTER_SIZE = 3 + _NE16_WEIGHT_D0_STRIDE_MODE8 = 2 + d0 = _NE16_FILTER_SIZE * _NE16_FILTER_SIZE * _NE16_WEIGHT_D0_STRIDE_MODE8 + return d0, 0, 0 + + @classmethod + def getConf0(cls, output_bits: int, weight_bits: int, input_signed: bool, use_wmem: bool) -> int: + conf0 = 0 + conf0 |= weight_bits - 1 + conf0 |= 1 << 5 # DW MODE + if use_wmem: + conf0 |= 1 << 9 + conf0 |= 1 << 15 # Layerwise weight offset mode + if output_bits == 32: + conf0 |= 2 << 21 + if input_signed: + conf0 |= 1 << 26 + return conf0 + + +class NE162DDenseConvTemplate(NE16ConvTemplate): + + NE16_KERNEL_SHAPE = 3 + NE16_IS_DEPTHWISE = 0 + NE16_SUBTILE_OUTPUT_CHANNEL = 32 + + def __init__(self, templateStr: str): + super().__init__(templateStr) + + @classmethod + def getCounters( + cls, channel_in: int, height_out: int, width_out: int, channel_out: int, padding_bottom: int, + padding_right: int, + operatorRepresentation: OperatorRepresentation) -> Tuple[int, int, int, int, int, int, int, int, int, int]: + _ = operatorRepresentation # operatorRepresentation not accessed for now because it's just for pointwise kernels + + # NE16 Dense 3x3: CIN_SUBTILE=16, OUTPUT 3x3x32 + n_channel_out_subtiles = _getNumTiles(channel_out, 32) + n_channel_in_subtiles = _getNumTiles(channel_in, 16) + n_height_out_subtiles = _getNumTiles(height_out, 3) + n_width_out_subtiles = _getNumTiles(width_out, 3) + + channel_out_border = _getBorderTileSize(channel_out, 32) + channel_in_border = _getBorderTileSize(channel_in, 16) + height_out_border = _getBorderTileSize(height_out, 3) + width_out_border = _getBorderTileSize(width_out, 3) + height_in_border = height_out_border + 2 - padding_bottom + width_in_border = width_out_border + 2 - padding_right + + return (n_channel_out_subtiles, n_channel_in_subtiles, n_height_out_subtiles, n_width_out_subtiles, + channel_out_border, channel_in_border, height_out_border, width_out_border, height_in_border, + width_in_border) + + @classmethod + def getWeightStrides(cls, channel_in: int) -> Tuple[int, int, int]: + # Match ne16_task_set_strides for dense 3x3 (non-DW): + # d0 = NE16_FILTER_SIZE * NE16_FILTER_SIZE * weight_d0_stride = 18 + # d1 = NE16_FILTER_SIZE * NE16_FILTER_SIZE * weight_d0_stride * qw * num_k_in + # = 18 * 8 * num_k_in + _NE16_FILTER_SIZE = 3 + _NE16_WEIGHT_D0_STRIDE_MODE8 = 2 + _QW = 8 + n_channel_in = _getNumTiles(channel_in, 16) + d0 = _NE16_FILTER_SIZE * _NE16_FILTER_SIZE * _NE16_WEIGHT_D0_STRIDE_MODE8 + d1 = d0 * _QW * n_channel_in + return d0, d1, 0 + + @classmethod + def getConf0(cls, output_bits: int, weight_bits: int, input_signed: bool, use_wmem: bool) -> int: + conf0 = 0 + conf0 |= weight_bits - 1 + if use_wmem: + conf0 |= 1 << 9 + conf0 |= 1 << 15 # Layerwise weight offset mode + if output_bits == 32: + conf0 |= 2 << 21 + if input_signed: + conf0 |= 1 << 26 + return conf0 + + +NE16TaskInitTemplateStr = """ +// N-EUREKA Task Init +ne16_task_t task = { + .data = (ne16_task_data_t) { + .weights_addr = (uint32_t)${weight} - ${wmem_addr_offset} + ${weight_addr_offset}, + .infeat_addr = (uint32_t)${data_in} - ${input_addr_offset}, + .outfeat_addr = (uint32_t)${data_out}, + .scale_addr = (uint32_t)${mul}, + .scale_shift_addr = (uint32_t)${shift}, + .scale_bias_addr = (uint32_t)${add}, + .cfg = (ne16_cfg_t) { + .input_stride = (ne16_stride_t) { + .d0 = ${dim_im_in_y_stride}, + .d1 = ${dim_im_in_x_stride}, + .d2 = 0 + }, + .output_stride = (ne16_stride_t) { + .d0 = NE16_OUTPUT_BANDWIDTH_BYTES, + .d1 = ${dim_im_out_y_stride}, + .d2 = ${dim_im_out_x_stride} + }, + task.data.cfg.weights_stride = (ne16_stride_t) { + .d0 = ${weightStrideD0}, + .d1 = ${weightStrideD1}, + .d2 = ${weightStrideD2} + }, + .subtile = (ne16_subtile_t) { + .number = { + .KoKi = nnx_concat_half(${nKo}, ${nKi}), + .HoWo = nnx_concat_half(${nHo}, ${nWo}) + }, + .remainder = { + .KoKi = nnx_concat_half(${bKo}, ${bKi}), + .HoWo = nnx_concat_half(${bHo}, ${bWo}), + .HiWi = nnx_concat_half(${bHi}, ${bWi}) + } + }, + .padding = (${padding_y_top} << 28) + (${padding_x_right} << 24) + (${padding_y_bottom} << 20) + (${padding_x_left} << 16), + .weight_offset_factor = ${weight_offset}, + .filter_mask = 0, + .conf0 = ${conf0}, + } + } +}; +// NE16 top-level task struct fields (required by HAL helpers and NE16 HW for +// non-1x1 paths). Kept consistent with ne16_task_set_op_to_conv/_set_bits. +task.weight_d0_stride = NE16_WEIGHT_D0_STRIDE_MODE8; +task.qw = ${weight_bits}; +task.subtile_output_channel = ${ne16_subtile_output_channel}; +task.kernel_shape = ${ne16_kernel_shape}; +task.depthwise = ${ne16_depthwise}; +""" + +NE16TaskExecutionTemplateStr = """ +// N-EUREKA Task Execution +ne16_nnx_dispatch_wait(ne16_pulp_get_dev()); +ne16_nnx_dispatch(ne16_pulp_get_dev(), &task); +ne16_nnx_resolve_wait(ne16_pulp_get_dev(), &task); +""" + +NE16RqntPWConv2D_Template = NE162DPWConvTemplate(NE16TaskInitTemplateStr + NE16TaskExecutionTemplateStr) +NE16PWConv2D_Template = NE162DPWConvTemplate(NE16TaskInitTemplateStr + NE16TaskExecutionTemplateStr) + +NE16RqntDWConv2D_Template = NE162DDWConvTemplate(NE16TaskInitTemplateStr + NE16TaskExecutionTemplateStr) +NE16DWConv2D_Template = NE162DDWConvTemplate(NE16TaskInitTemplateStr + NE16TaskExecutionTemplateStr) + +NE16RqntDenseConv2D_Template = NE162DDenseConvTemplate(NE16TaskInitTemplateStr + NE16TaskExecutionTemplateStr) +NE16DenseConv2D_Template = NE162DDenseConvTemplate(NE16TaskInitTemplateStr + NE16TaskExecutionTemplateStr) diff --git a/Deeploy/Targets/NE16/Templates/__init__.py b/Deeploy/Targets/NE16/Templates/__init__.py new file mode 100644 index 0000000000..be436b64a3 --- /dev/null +++ b/Deeploy/Targets/NE16/Templates/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from . import * diff --git a/Deeploy/Targets/NE16/TileConstraints/NE16DenseConstraint.py b/Deeploy/Targets/NE16/TileConstraints/NE16DenseConstraint.py new file mode 100644 index 0000000000..4288e0f1de --- /dev/null +++ b/Deeploy/Targets/NE16/TileConstraints/NE16DenseConstraint.py @@ -0,0 +1,268 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Dict, List, Tuple + +from Deeploy.AbstractDataTypes import PointerClass +from Deeploy.CommonExtensions.DataTypes import uint8_t, uint16_t, uint32_t +from Deeploy.DeeployTypes import NetworkContext, OperatorRepresentation, VariableBuffer +from Deeploy.Targets.NE16.Templates.ConvTemplate import NE162DDenseConvTemplate, getInputAddrOffset, \ + ioStridesFromDimensions +from Deeploy.Targets.NE16.TileConstraints.RequantHelpers import requantAddGeometricalConstraint, requantLoadSchedule +from Deeploy.Targets.PULPOpen.TileConstraints.ConvTileConstraint import Conv2DTileConstraint +from Deeploy.TilingExtension.MemoryConstraints import NodeMemoryConstraint +from Deeploy.TilingExtension.TileConstraint import TileConstraint +from Deeploy.TilingExtension.TilerModel import PerformanceHint, TilerModel +from Deeploy.TilingExtension.TilingCodegen import AbsoluteHyperRectangle, HyperRectangle, TilingSchedule, \ + VariableReplacementScheme, calculateFlatOffsetInBytes + + +class NE16DenseConv2DTileConstraint(TileConstraint): + + @staticmethod + def addGeometricalConstraint(tilerModel: TilerModel, parseDict: Dict, ctxt: NetworkContext) -> TilerModel: + inputBufferName = parseDict['data_in'] + weightBufferName = parseDict['weight'] + outputBufferName = parseDict['data_out'] + + strides = parseDict["strides"] + padding = parseDict["pads"] + dilation = parseDict["dilations"] + + for bufferName in [inputBufferName, weightBufferName, outputBufferName]: + tilerModel.addTensorDimToModel(ctxt, bufferName) + + inputBatchVar = tilerModel.getTensorDimVar(tensorName = inputBufferName, dimIdx = 0) + inputHeightVar = tilerModel.getTensorDimVar(tensorName = inputBufferName, dimIdx = 1) + inputWidthVar = tilerModel.getTensorDimVar(tensorName = inputBufferName, dimIdx = 2) + inputChannelVar = tilerModel.getTensorDimVar(tensorName = inputBufferName, dimIdx = 3) + + weightOutChannelVar = tilerModel.getTensorDimVar(tensorName = weightBufferName, dimIdx = 0) + weightInChannelMajorVar = tilerModel.getTensorDimVar(tensorName = weightBufferName, dimIdx = 1) + weightBitsVar = tilerModel.getTensorDimVar(tensorName = weightBufferName, dimIdx = 2) + weightBandwidthVar = tilerModel.getTensorDimVar(tensorName = weightBufferName, dimIdx = 3) + + outputBatchVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 0) + outputHeightVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 1) + outputWidthVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 2) + outputChannelVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 3) + + # Map output dims to inputs dims + tilerModel.addConstraint(outputBatchVar == inputBatchVar) + + weightBuffer = ctxt.lookup(weightBufferName) + if hasattr(weightBuffer, "_memoryLevel") and weightBuffer._memoryLevel == "WeightMemory_SRAM": + tilerModel.addConstraint(weightOutChannelVar == weightOutChannelVar.Max()) + else: + tilerModel.addConstraint(weightOutChannelVar == outputChannelVar) + + inputBuffer = ctxt.lookup(inputBufferName) + + effectiveHeight = inputHeightVar + ((padding[0] + padding[2]) * (inputHeightVar == inputBuffer.shape[1])) + effectiveWidth = inputWidthVar + ((padding[1] + padding[3]) * (inputWidthVar == inputBuffer.shape[2])) + + tilerModel.addConstraint((outputHeightVar == (effectiveHeight - (3 - 1) - 1) // strides[0] + 1)) + tilerModel.addConstraint((outputWidthVar == (effectiveWidth - (3 - 1) - 1) // strides[1] + 1)) + + return tilerModel + + @staticmethod + def addPolicyConstraint(tilerModel: TilerModel, parseDict: Dict, ctxt: NetworkContext) -> TilerModel: + inputHeightVar = tilerModel.getTensorDimVar(tensorName = parseDict['data_in'], dimIdx = 1) + inputWidthVar = tilerModel.getTensorDimVar(tensorName = parseDict['data_in'], dimIdx = 2) + inputChannelVar = tilerModel.getTensorDimVar(tensorName = parseDict['data_in'], dimIdx = 3) + + strides = parseDict["strides"] + + tilerModel.addConstraint((inputHeightVar % strides[0]) == 0) + tilerModel.addConstraint((inputWidthVar % strides[1]) == 0) + + tilerModel.addConstraint(inputChannelVar == inputChannelVar.Max()) + + tilerModel.addConstraint(inputHeightVar == inputHeightVar.Max(), strategy = PerformanceHint(1)) + tilerModel.addConstraint(inputWidthVar == inputWidthVar.Max(), strategy = PerformanceHint(1)) + + tilerModel.addConstraint(inputHeightVar >= parseDict['dim_kernel_x']) + tilerModel.addConstraint(inputWidthVar >= parseDict['dim_kernel_y']) + + return tilerModel + + @classmethod + def serializeTilingSolution( + cls, tilingSolution: NodeMemoryConstraint, absoluteOutputCubes: List[AbsoluteHyperRectangle], + targetMemLevel: str, ctxt: NetworkContext, + operatorRepresentation: OperatorRepresentation) -> Tuple[VariableReplacementScheme, TilingSchedule]: + outputCubes = [cube.rectangle for cube in absoluteOutputCubes] + + addrNames = ['data_in', 'data_out'] + inputBaseOffsets, outputBaseOffsets = cls.extractBaseAddr(tilingSolution, targetMemLevel, + operatorRepresentation, addrNames) + + varWeight = operatorRepresentation['weight'] + varOut = operatorRepresentation['data_out'] + + inputInCubes = [] + replacements: Dict[str, List[int]] = { + "padding_y_top": [], + "padding_y_bottom": [], + "padding_x_left": [], + "padding_x_right": [], + "dim_im_in_x_stride": [], + "dim_im_in_y_stride": [], + "dim_im_out_x_stride": [], + "dim_im_out_y_stride": [], + "input_addr_offset": [], + "nKo": [], + "nKi": [], + "nHo": [], + "nWo": [], + "bKo": [], + "bKi": [], + "bHo": [], + "bWo": [], + "bHi": [], + "bWi": [], + } + + replacementTypes = { + "padding_y_top": PointerClass(uint8_t), + "padding_y_bottom": PointerClass(uint8_t), + "padding_x_left": PointerClass(uint8_t), + "padding_x_right": PointerClass(uint8_t), + "dim_im_in_x_stride": PointerClass(uint32_t), + "dim_im_in_y_stride": PointerClass(uint32_t), + "dim_im_out_x_stride": PointerClass(uint32_t), + "dim_im_out_y_stride": PointerClass(uint32_t), + "input_addr_offset": PointerClass(uint32_t), + "nKo": PointerClass(uint16_t), + "nKi": PointerClass(uint16_t), + "nHo": PointerClass(uint16_t), + "nWo": PointerClass(uint16_t), + "bKo": PointerClass(uint16_t), + "bKi": PointerClass(uint16_t), + "bHo": PointerClass(uint16_t), + "bWo": PointerClass(uint16_t), + "bHi": PointerClass(uint16_t), + "bWi": PointerClass(uint16_t), + } + + weightH = operatorRepresentation['dim_kernel_y'] + weightW = operatorRepresentation['dim_kernel_x'] + weightC = operatorRepresentation['ch_im_in'] + + pads = operatorRepresentation['pads'] + strides = operatorRepresentation['strides'] + + outputBuffer = ctxt.lookup(varOut) + assert isinstance(outputBuffer, VariableBuffer) + + for cube in outputCubes: + (BatchOffset, HOffset, WOffset, COffset) = cube.offset + (BatchSize, HSize, WSize, CSize) = cube.dims + + InCube, padding_tuple = Conv2DTileConstraint.computeInputCube((weightH, weightW), pads, strides, weightC, + cube, outputBuffer.shape) + padding_left, padding_right, padding_top, padding_bottom = padding_tuple + + replacements['padding_y_top'].append(padding_top) + replacements['padding_y_bottom'].append(padding_bottom) + replacements['padding_x_left'].append(padding_left) + replacements['padding_x_right'].append(padding_right) + + inBSize, inHSize, inWSize, inCSize = InCube.dims + + dim_im_in_x_stride, dim_im_in_y_stride = ioStridesFromDimensions(inWSize, inCSize, + operatorRepresentation["input_bits"]) + replacements['dim_im_in_x_stride'].append(dim_im_in_x_stride) + replacements['dim_im_in_y_stride'].append(dim_im_in_y_stride) + dim_im_out_x_stride, dim_im_out_y_stride = ioStridesFromDimensions(WSize, CSize, + operatorRepresentation["output_bits"]) + replacements['dim_im_out_x_stride'].append(dim_im_out_x_stride) + replacements['dim_im_out_y_stride'].append(dim_im_out_y_stride) + + replacements['input_addr_offset'].append( + getInputAddrOffset(inWSize, dim_im_in_y_stride, padding_top, padding_left)) + + nKo, nKi, nHo, nWo, bKo, bKi, bHo, bWo, bHi, bWi = NE162DDenseConvTemplate.getCounters( + inCSize, HSize, WSize, CSize, padding_bottom, padding_right, operatorRepresentation) + + replacements["nKo"].append(nKo) + replacements["nKi"].append(nKi) + replacements["nHo"].append(nHo) + replacements["nWo"].append(nWo) + replacements["bKo"].append(bKo) + replacements["bKi"].append(bKi) + replacements["bHo"].append(bHo) + replacements["bWo"].append(bWo) + replacements["bHi"].append(bHi) + replacements["bWi"].append(bWi) + + inputInCubes.append(InCube) + + inputLoadSchedule = [] + outputLoadSchedule = [] + + for a in inputInCubes: + inputLoadSchedule.append({"data_in": a}) + + for out in outputCubes: + outputLoadSchedule.append({"data_out": out}) + + weightBuffer = ctxt.lookup(varWeight) + assert isinstance(weightBuffer, VariableBuffer) + weightShape = weightBuffer.shape + + if hasattr(weightBuffer, "_memoryLevel") and weightBuffer._memoryLevel == "WeightMemory_SRAM": + replacements['weight_addr_offset'] = [] + replacementTypes['weight_addr_offset'] = PointerClass(uint32_t) + for absoluteCube in absoluteOutputCubes: + COffset, CSize = absoluteCube.absoluteOffset[-1], absoluteCube.rectangle.dims[-1] + WeightCube = HyperRectangle((COffset, 0, 0), (CSize, weightShape[-2], weightShape[-1])) + replacements['weight_addr_offset'].append(calculateFlatOffsetInBytes(WeightCube, weightBuffer)) + else: + inputWeightBaseOffsets, outputWeightBaseOffsets = cls.extractBaseAddr(tilingSolution, targetMemLevel, + operatorRepresentation, ['weight']) + inputBaseOffsets.update(inputWeightBaseOffsets) + outputBaseOffsets.update(outputWeightBaseOffsets) + + for cube, load in zip(outputCubes, inputLoadSchedule): + COffset, CSize = cube.offset[-1], cube.dims[-1] + load['weight'] = HyperRectangle((COffset, 0, 0), (CSize, weightShape[-2], weightShape[-1])) + + tilingSchedule = TilingSchedule(inputBaseOffsets, outputBaseOffsets, inputLoadSchedule, outputLoadSchedule) + variableReplacementSchedule = VariableReplacementScheme(replacements, replacementTypes) + + return variableReplacementSchedule, tilingSchedule + + +class NE16RQSDenseConv2DTileConstraint(NE16DenseConv2DTileConstraint): + + @staticmethod + def addGeometricalConstraint(tilerModel: TilerModel, parseDict: Dict, ctxt: NetworkContext) -> TilerModel: + tilerModel = NE16DenseConv2DTileConstraint.addGeometricalConstraint(tilerModel, parseDict, ctxt) + return requantAddGeometricalConstraint(tilerModel, parseDict, ctxt) + + @classmethod + def serializeTilingSolution( + cls, tilingSolution: NodeMemoryConstraint, absoluteOutputCubes: List[AbsoluteHyperRectangle], + targetMemLevel: str, ctxt: NetworkContext, + operatorRepresentation: OperatorRepresentation) -> Tuple[VariableReplacementScheme, TilingSchedule]: + variableReplacementSchedule, tilingSchedule = super().serializeTilingSolution( + tilingSolution, absoluteOutputCubes, targetMemLevel, ctxt, operatorRepresentation) + + addrNames = ['mul', 'add'] + inputRequantBaseOffsets, _ = cls.extractBaseAddr(tilingSolution, targetMemLevel, operatorRepresentation, + addrNames) + newInputBaseOffsets = {**tilingSchedule.inputBaseOffsets, **inputRequantBaseOffsets} + + requantSchedule = requantLoadSchedule(absoluteOutputCubes, ctxt, operatorRepresentation) + newInputLoadSchedule = [{ + **load, + **rqLoad + } for load, rqLoad in zip(tilingSchedule.inputLoadSchedule, requantSchedule)] + + newTilingSchedule = TilingSchedule(newInputBaseOffsets, tilingSchedule.outputBaseOffsets, newInputLoadSchedule, + tilingSchedule.outputLoadSchedule) + + return variableReplacementSchedule, newTilingSchedule diff --git a/Deeploy/Targets/NE16/TileConstraints/NE16DepthwiseConstraint.py b/Deeploy/Targets/NE16/TileConstraints/NE16DepthwiseConstraint.py new file mode 100644 index 0000000000..b9221b74f3 --- /dev/null +++ b/Deeploy/Targets/NE16/TileConstraints/NE16DepthwiseConstraint.py @@ -0,0 +1,265 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Dict, List, Tuple + +from Deeploy.AbstractDataTypes import PointerClass +from Deeploy.CommonExtensions.DataTypes import uint8_t, uint16_t, uint32_t +from Deeploy.DeeployTypes import NetworkContext, OperatorRepresentation, VariableBuffer +from Deeploy.Targets.NE16.Templates.ConvTemplate import NE162DDWConvTemplate, getInputAddrOffset, \ + ioStridesFromDimensions +from Deeploy.Targets.NE16.TileConstraints.RequantHelpers import requantAddGeometricalConstraint, requantLoadSchedule +from Deeploy.Targets.PULPOpen.TileConstraints.ConvTileConstraint import Conv2DTileConstraint +from Deeploy.TilingExtension.MemoryConstraints import NodeMemoryConstraint +from Deeploy.TilingExtension.TileConstraint import TileConstraint +from Deeploy.TilingExtension.TilerModel import PerformanceHint, TilerModel +from Deeploy.TilingExtension.TilingCodegen import AbsoluteHyperRectangle, HyperRectangle, TilingSchedule, \ + VariableReplacementScheme + + +class NE16DWConv2DTileConstraint(TileConstraint): + + @staticmethod + def addGeometricalConstraint(tilerModel: TilerModel, parseDict: Dict, ctxt: NetworkContext) -> TilerModel: + inputBufferName = parseDict['data_in'] + weightBufferName = parseDict['weight'] + outputBufferName = parseDict['data_out'] + + strides = parseDict["strides"] + padding = parseDict["pads"] + dilation = parseDict["dilations"] + + for bufferName in [inputBufferName, weightBufferName, outputBufferName]: + tilerModel.addTensorDimToModel(ctxt, bufferName) + + inputBatchVar = tilerModel.getTensorDimVar(tensorName = inputBufferName, dimIdx = 0) + inputHeightVar = tilerModel.getTensorDimVar(tensorName = inputBufferName, dimIdx = 1) + inputWidthVar = tilerModel.getTensorDimVar(tensorName = inputBufferName, dimIdx = 2) + inputChannelVar = tilerModel.getTensorDimVar(tensorName = inputBufferName, dimIdx = 3) + + weightOutChannelVar = tilerModel.getTensorDimVar(tensorName = weightBufferName, dimIdx = 0) + + outputBatchVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 0) + outputHeightVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 1) + outputWidthVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 2) + outputChannelVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 3) + + # Map output dims to inputs dims + tilerModel.addConstraint(outputBatchVar == inputBatchVar) + tilerModel.addConstraint(outputChannelVar == inputChannelVar) + + weightBuffer = ctxt.lookup(weightBufferName) + # NE16 DW weight is packed as a single (1, 1, packed_bytes) block + # containing all output channels (up to NE16_SUBTILE_INPUT_CHANNEL=16). + # Keep the outermost dim fixed at its full (=1) value regardless of + # the output channel tiling. + tilerModel.addConstraint(weightOutChannelVar == weightOutChannelVar.Max()) + + tilerModel.addConstraint(inputHeightVar >= 3) + tilerModel.addConstraint(inputWidthVar >= 3) + + inputBuffer = ctxt.lookup(inputBufferName) + + effectiveHeight = inputHeightVar + ((padding[0] + padding[2]) * (inputHeightVar == inputBuffer.shape[1])) + effectiveWidth = inputWidthVar + ((padding[1] + padding[3]) * (inputWidthVar == inputBuffer.shape[2])) + + tilerModel.addConstraint((outputHeightVar == (effectiveHeight - (3 - 1) - 1) // strides[0] + 1)) + tilerModel.addConstraint((outputWidthVar == (effectiveWidth - (3 - 1) - 1) // strides[1] + 1)) + + return tilerModel + + @staticmethod + def addPolicyConstraint(tilerModel: TilerModel, parseDict: Dict, ctxt: NetworkContext) -> TilerModel: + inputHeightVar = tilerModel.getTensorDimVar(tensorName = parseDict['data_in'], dimIdx = 1) + inputWidthVar = tilerModel.getTensorDimVar(tensorName = parseDict['data_in'], dimIdx = 2) + + strides = parseDict["strides"] + + tilerModel.addConstraint((inputHeightVar % strides[0]) == 0) + tilerModel.addConstraint((inputWidthVar % strides[1]) == 0) + + tilerModel.addConstraint(inputHeightVar == inputHeightVar.Max(), strategy = PerformanceHint(1)) + tilerModel.addConstraint(inputWidthVar == inputWidthVar.Max(), strategy = PerformanceHint(1)) + + return tilerModel + + @classmethod + def serializeTilingSolution( + cls, tilingSolution: NodeMemoryConstraint, absoluteOutputCubes: List[AbsoluteHyperRectangle], + targetMemLevel: str, ctxt: NetworkContext, + operatorRepresentation: OperatorRepresentation) -> Tuple[VariableReplacementScheme, TilingSchedule]: + outputCubes = [cube.rectangle for cube in absoluteOutputCubes] + + addrNames = ['data_in', 'data_out'] + inputBaseOffsets, outputBaseOffsets = cls.extractBaseAddr(tilingSolution, targetMemLevel, + operatorRepresentation, addrNames) + + varWeight = operatorRepresentation['weight'] + varOut = operatorRepresentation['data_out'] + + inputInCubes = [] + replacements: Dict[str, List[int]] = { + "padding_y_top": [], + "padding_y_bottom": [], + "padding_x_left": [], + "padding_x_right": [], + "dim_im_in_x_stride": [], + "dim_im_in_y_stride": [], + "dim_im_out_x_stride": [], + "dim_im_out_y_stride": [], + "input_addr_offset": [], + "nKo": [], + "nKi": [], + "nHo": [], + "nWo": [], + "bKo": [], + "bKi": [], + "bHo": [], + "bWo": [], + "bHi": [], + "bWi": [], + } + + replacementTypes = { + "padding_y_top": PointerClass(uint8_t), + "padding_y_bottom": PointerClass(uint8_t), + "padding_x_left": PointerClass(uint8_t), + "padding_x_right": PointerClass(uint8_t), + "dim_im_in_x_stride": PointerClass(uint32_t), + "dim_im_in_y_stride": PointerClass(uint32_t), + "dim_im_out_x_stride": PointerClass(uint32_t), + "dim_im_out_y_stride": PointerClass(uint32_t), + "input_addr_offset": PointerClass(uint32_t), + "nKo": PointerClass(uint16_t), + "nKi": PointerClass(uint16_t), + "nHo": PointerClass(uint16_t), + "nWo": PointerClass(uint16_t), + "bKo": PointerClass(uint16_t), + "bKi": PointerClass(uint16_t), + "bHo": PointerClass(uint16_t), + "bWo": PointerClass(uint16_t), + "bHi": PointerClass(uint16_t), + "bWi": PointerClass(uint16_t), + } + + weightH = operatorRepresentation['dim_kernel_y'] + weightW = operatorRepresentation['dim_kernel_x'] + weightC = operatorRepresentation['ch_im_in'] + + pads = operatorRepresentation['pads'] + strides = operatorRepresentation['strides'] + + outputBuffer = ctxt.lookup(varOut) + assert isinstance(outputBuffer, VariableBuffer) + + for cube in outputCubes: + (BatchOffset, HOffset, WOffset, COffset) = cube.offset + (BatchSize, HSize, WSize, CSize) = cube.dims + + InCube, padding_tuple = Conv2DTileConstraint.computeInputCube((weightH, weightW), pads, strides, weightC, + cube, + ctxt.lookup(varOut).shape) + padding_left, padding_right, padding_top, padding_bottom = padding_tuple + + replacements['padding_y_top'].append(padding_top) + replacements['padding_y_bottom'].append(padding_bottom) + replacements['padding_x_left'].append(padding_left) + replacements['padding_x_right'].append(padding_right) + + inBSize, inHSize, inWSize, inCSize = InCube.dims + + dim_im_in_x_stride, dim_im_in_y_stride = ioStridesFromDimensions(inWSize, inCSize, + operatorRepresentation["input_bits"]) + replacements['dim_im_in_x_stride'].append(dim_im_in_x_stride) + replacements['dim_im_in_y_stride'].append(dim_im_in_y_stride) + dim_im_out_x_stride, dim_im_out_y_stride = ioStridesFromDimensions(WSize, CSize, + operatorRepresentation["output_bits"]) + replacements['dim_im_out_x_stride'].append(dim_im_out_x_stride) + replacements['dim_im_out_y_stride'].append(dim_im_out_y_stride) + + replacements['input_addr_offset'].append( + getInputAddrOffset(inWSize, dim_im_in_y_stride, padding_top, padding_left)) + + nKo, nKi, nHo, nWo, bKo, bKi, bHo, bWo, bHi, bWi = NE162DDWConvTemplate.getCounters( + inCSize, HSize, WSize, CSize, padding_bottom, padding_right, operatorRepresentation) + + replacements["nKo"].append(nKo) + replacements["nKi"].append(nKi) + replacements["nHo"].append(nHo) + replacements["nWo"].append(nWo) + replacements["bKo"].append(bKo) + replacements["bKi"].append(bKi) + replacements["bHo"].append(bHo) + replacements["bWo"].append(bWo) + replacements["bHi"].append(bHi) + replacements["bWi"].append(bWi) + + inputInCubes.append(InCube) + + inputLoadSchedule = [] + outputLoadSchedule = [] + + for a in inputInCubes: + inputLoadSchedule.append({"data_in": a}) + + for out in outputCubes: + outputLoadSchedule.append({"data_out": out}) + + weightBuffer = ctxt.lookup(varWeight) + assert isinstance(weightBuffer, VariableBuffer) + weightShape = weightBuffer.shape + + if hasattr(weightBuffer, "_memoryLevel") and weightBuffer._memoryLevel == "WeightMemory_SRAM": + replacements['weight_addr_offset'] = [] + replacementTypes['weight_addr_offset'] = PointerClass(uint32_t) + for _ in absoluteOutputCubes: + # DW weight is a single packed block — no per-cout offset. + replacements['weight_addr_offset'].append(0) + else: + inputWeightBaseOffsets, outputWeightBaseOffsets = cls.extractBaseAddr(tilingSolution, targetMemLevel, + operatorRepresentation, ['weight']) + inputBaseOffsets.update(inputWeightBaseOffsets) + outputBaseOffsets.update(outputWeightBaseOffsets) + + # DW weight is a single packed (1, 1, packed_bytes) block used + # across all output-channel tiles — same cube every iteration. + for _cube, load in zip(outputCubes, inputLoadSchedule): + load['weight'] = HyperRectangle((0, 0, 0), (weightShape[0], weightShape[1], weightShape[2])) + + tilingSchedule = TilingSchedule(inputBaseOffsets, outputBaseOffsets, inputLoadSchedule, outputLoadSchedule) + variableReplacementSchedule = VariableReplacementScheme(replacements, replacementTypes) + + return variableReplacementSchedule, tilingSchedule + + +class NE16RQSDWConv2DTileConstraint(NE16DWConv2DTileConstraint): + + @staticmethod + def addGeometricalConstraint(tilerModel: TilerModel, parseDict: Dict, ctxt: NetworkContext) -> TilerModel: + tilerModel = NE16DWConv2DTileConstraint.addGeometricalConstraint(tilerModel, parseDict, ctxt) + return requantAddGeometricalConstraint(tilerModel, parseDict, ctxt) + + @classmethod + def serializeTilingSolution( + cls, tilingSolution: NodeMemoryConstraint, absoluteOutputCubes: List[AbsoluteHyperRectangle], + targetMemLevel: str, ctxt: NetworkContext, + operatorRepresentation: OperatorRepresentation) -> Tuple[VariableReplacementScheme, TilingSchedule]: + variableReplacementSchedule, tilingSchedule = super().serializeTilingSolution( + tilingSolution, absoluteOutputCubes, targetMemLevel, ctxt, operatorRepresentation) + + addrNames = ['mul', 'add'] + inputRequantBaseOffsets, _ = cls.extractBaseAddr(tilingSolution, targetMemLevel, operatorRepresentation, + addrNames) + newInputBaseOffsets = {**tilingSchedule.inputBaseOffsets, **inputRequantBaseOffsets} + + requantSchedule = requantLoadSchedule(absoluteOutputCubes, ctxt, operatorRepresentation) + newInputLoadSchedule = [{ + **load, + **rqLoad + } for load, rqLoad in zip(tilingSchedule.inputLoadSchedule, requantSchedule)] + + newTilingSchedule = TilingSchedule(newInputBaseOffsets, tilingSchedule.outputBaseOffsets, newInputLoadSchedule, + tilingSchedule.outputLoadSchedule) + + return variableReplacementSchedule, newTilingSchedule diff --git a/Deeploy/Targets/NE16/TileConstraints/NE16PointwiseConstraint.py b/Deeploy/Targets/NE16/TileConstraints/NE16PointwiseConstraint.py new file mode 100644 index 0000000000..a32137a826 --- /dev/null +++ b/Deeploy/Targets/NE16/TileConstraints/NE16PointwiseConstraint.py @@ -0,0 +1,298 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Dict, List, Tuple + +from Deeploy.AbstractDataTypes import PointerClass +from Deeploy.CommonExtensions.DataTypes import uint8_t, uint16_t, uint32_t +from Deeploy.DeeployTypes import NetworkContext, OperatorRepresentation, VariableBuffer +from Deeploy.Targets.NE16.Templates.ConvTemplate import NE162DPWConvTemplate, getInputAddrOffset, \ + ioStridesFromDimensions +from Deeploy.Targets.NE16.TileConstraints.RequantHelpers import requantAddGeometricalConstraint, requantLoadSchedule +from Deeploy.Targets.PULPOpen.TileConstraints.ConvTileConstraint import Conv2DTileConstraint +from Deeploy.TilingExtension.MemoryConstraints import NodeMemoryConstraint +from Deeploy.TilingExtension.TileConstraint import TileConstraint +from Deeploy.TilingExtension.TilerModel import PerformanceHint, TilerModel +from Deeploy.TilingExtension.TilingCodegen import AbsoluteHyperRectangle, HyperRectangle, TilingSchedule, \ + VariableReplacementScheme, calculateFlatOffsetInBytes + + +class NE16PWConv2DTileConstraint(TileConstraint): + + @staticmethod + def addGeometricalConstraint(tilerModel: TilerModel, parseDict: Dict, ctxt: NetworkContext) -> TilerModel: + inputBufferName = parseDict['data_in'] + weightBufferName = parseDict['weight'] + outputBufferName = parseDict['data_out'] + + for bufferName in [inputBufferName, weightBufferName, outputBufferName]: + tilerModel.addTensorDimToModel(ctxt, bufferName) + + inputBatchVar = tilerModel.getTensorDimVar(tensorName = inputBufferName, dimIdx = 0) + inputHeightVar = tilerModel.getTensorDimVar(tensorName = inputBufferName, dimIdx = 1) + inputWidthVar = tilerModel.getTensorDimVar(tensorName = inputBufferName, dimIdx = 2) + + weightOutChannelVar = tilerModel.getTensorDimVar(tensorName = weightBufferName, dimIdx = 0) + + outputBatchVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 0) + outputHeightVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 1) + outputWidthVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 2) + outputChannelVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 3) + + # Map output dims to inputs dims + tilerModel.addConstraint(outputBatchVar == inputBatchVar) + tilerModel.addConstraint(outputHeightVar == inputHeightVar) + tilerModel.addConstraint(outputWidthVar == inputWidthVar) + + weightBuffer = ctxt.lookup(weightBufferName) + if hasattr(weightBuffer, "_memoryLevel") and weightBuffer._memoryLevel == "WeightMemory_SRAM": + tilerModel.addConstraint(weightOutChannelVar == weightOutChannelVar.Max()) + else: + tilerModel.addConstraint(weightOutChannelVar == outputChannelVar) + + tilerModel.addConstraint(inputHeightVar >= 1) + tilerModel.addConstraint(inputWidthVar >= 1) + + return tilerModel + + @staticmethod + def addPolicyConstraint(tilerModel: TilerModel, parseDict: Dict, ctxt: NetworkContext) -> TilerModel: + + # Get to-be-tiled tensor's buffers + inputBuffer = ctxt.lookup(name = parseDict['data_in']) + weightBuffer = ctxt.lookup(name = parseDict['weight']) + outputBuffer = ctxt.lookup(name = parseDict['data_out']) + + inputHeightVar = tilerModel.getTensorDimVar(tensorName = inputBuffer.name, dimIdx = 1) + inputWidthVar = tilerModel.getTensorDimVar(tensorName = inputBuffer.name, dimIdx = 2) + inputChannelVar = tilerModel.getTensorDimVar(tensorName = inputBuffer.name, dimIdx = 3) + + weightOutChannelVar = tilerModel.getTensorDimVar(tensorName = weightBuffer.name, dimIdx = 0) + weightInChannelMajorVar = tilerModel.getTensorDimVar(tensorName = weightBuffer.name, dimIdx = 1) + weightBandwidthVar = tilerModel.getTensorDimVar(tensorName = weightBuffer.name, dimIdx = 2) + + outputHeightVar = tilerModel.getTensorDimVar(tensorName = outputBuffer.name, dimIdx = 1) + outputWidthVar = tilerModel.getTensorDimVar(tensorName = outputBuffer.name, dimIdx = 2) + outputChannelVar = tilerModel.getTensorDimVar(tensorName = outputBuffer.name, dimIdx = 3) + + strides = parseDict["strides"] + padding = parseDict["pads"] + + # LMACAN: Force full input channel to avoid partial results + tilerModel.addConstraint(inputChannelVar == inputChannelVar.Max()) + tilerModel.addConstraint(weightInChannelMajorVar == weightInChannelMajorVar.Max()) + tilerModel.addConstraint(weightBandwidthVar == weightBandwidthVar.Max()) + + tilerModel.addConstraint((inputHeightVar % strides[0]) == 0) + tilerModel.addConstraint((inputWidthVar % strides[1]) == 0) + + # N-EUREKA tile constraints to align with N-EUREKA's hardware subtiling + if parseDict["dim_im_out_x"] > 6: + tilerModel.addTileSizeDivisibleConstraint(parseDict, + "dim_im_out_x", + outputHeightVar, + 6, + strategy = PerformanceHint(priority = 3)) + else: + tilerModel.addConstraint(outputHeightVar == outputHeightVar.Max(), strategy = PerformanceHint(priority = 3)) + + if parseDict["dim_im_out_y"] > 6: + tilerModel.addTileSizeDivisibleConstraint(parseDict, + "dim_im_out_y", + outputWidthVar, + 6, + strategy = PerformanceHint(priority = 2)) + else: + tilerModel.addConstraint(outputWidthVar == outputWidthVar.Max(), strategy = PerformanceHint(priority = 2)) + + if parseDict["ch_im_out"] > 32: + tilerModel.addTileSizeDivisibleConstraint(parseDict, + "ch_im_out", + outputChannelVar, + 32, + strategy = PerformanceHint(priority = 1)) + else: + tilerModel.addConstraint(outputChannelVar == outputChannelVar.Max(), + strategy = PerformanceHint(priority = 1)) + + return tilerModel + + @classmethod + def serializeTilingSolution( + cls, tilingSolution: NodeMemoryConstraint, absoluteOutputCubes: List[AbsoluteHyperRectangle], + targetMemLevel: str, ctxt: NetworkContext, + operatorRepresentation: OperatorRepresentation) -> Tuple[VariableReplacementScheme, TilingSchedule]: + outputCubes = [cube.rectangle for cube in absoluteOutputCubes] + + addrNames = ['data_in', 'data_out'] + inputBaseOffsets, outputBaseOffsets = cls.extractBaseAddr(tilingSolution, targetMemLevel, + operatorRepresentation, addrNames) + + varWeight = operatorRepresentation['weight'] + varOut = operatorRepresentation['data_out'] + + inputInCubes = [] + replacements: Dict[str, List[int]] = { + "padding_y_top": [], + "padding_y_bottom": [], + "padding_x_left": [], + "padding_x_right": [], + "dim_im_in_x_stride": [], + "dim_im_in_y_stride": [], + "dim_im_out_x_stride": [], + "dim_im_out_y_stride": [], + "input_addr_offset": [], + "nKo": [], + "nKi": [], + "nHo": [], + "nWo": [], + "bKo": [], + "bKi": [], + "bHo": [], + "bWo": [], + "bHi": [], + "bWi": [], + } + + replacementTypes = { + "padding_y_top": PointerClass(uint8_t), + "padding_y_bottom": PointerClass(uint8_t), + "padding_x_left": PointerClass(uint8_t), + "padding_x_right": PointerClass(uint8_t), + "dim_im_in_x_stride": PointerClass(uint32_t), + "dim_im_in_y_stride": PointerClass(uint32_t), + "dim_im_out_x_stride": PointerClass(uint32_t), + "dim_im_out_y_stride": PointerClass(uint32_t), + "input_addr_offset": PointerClass(uint32_t), + "nKo": PointerClass(uint16_t), + "nKi": PointerClass(uint16_t), + "nHo": PointerClass(uint16_t), + "nWo": PointerClass(uint16_t), + "bKo": PointerClass(uint16_t), + "bKi": PointerClass(uint16_t), + "bHo": PointerClass(uint16_t), + "bWo": PointerClass(uint16_t), + "bHi": PointerClass(uint16_t), + "bWi": PointerClass(uint16_t), + } + + weightH = operatorRepresentation['dim_kernel_y'] + weightW = operatorRepresentation['dim_kernel_x'] + weightC = operatorRepresentation['ch_im_in'] + + pads = operatorRepresentation['pads'] + strides = operatorRepresentation['strides'] + + outputBuffer = ctxt.lookup(varOut) + assert isinstance(outputBuffer, VariableBuffer) + + for cube in outputCubes: + (BatchOffset, HOffset, WOffset, COffset) = cube.offset + (BatchSize, HSize, WSize, CSize) = cube.dims + + InCube, padding_tuple = Conv2DTileConstraint.computeInputCube((weightH, weightW), pads, strides, weightC, + cube, outputBuffer.shape) + padding_left, padding_right, padding_top, padding_bottom = padding_tuple + + replacements['padding_y_top'].append(padding_top) + replacements['padding_y_bottom'].append(padding_bottom) + replacements['padding_x_left'].append(padding_left) + replacements['padding_x_right'].append(padding_right) + + inBSize, inHSize, inWSize, inCSize = InCube.dims + + dim_im_in_x_stride, dim_im_in_y_stride = ioStridesFromDimensions(inWSize, inCSize, + operatorRepresentation["input_bits"]) + replacements['dim_im_in_x_stride'].append(dim_im_in_x_stride) + replacements['dim_im_in_y_stride'].append(dim_im_in_y_stride) + dim_im_out_x_stride, dim_im_out_y_stride = ioStridesFromDimensions(WSize, CSize, + operatorRepresentation["output_bits"]) + replacements['dim_im_out_x_stride'].append(dim_im_out_x_stride) + replacements['dim_im_out_y_stride'].append(dim_im_out_y_stride) + + replacements['input_addr_offset'].append( + getInputAddrOffset(inWSize, dim_im_in_y_stride, padding_top, padding_left)) + + nKo, nKi, nHo, nWo, bKo, bKi, bHo, bWo, bHi, bWi = NE162DPWConvTemplate.getCounters( + inCSize, HSize, WSize, CSize, padding_bottom, padding_right, operatorRepresentation) + + replacements["nKo"].append(nKo) + replacements["nKi"].append(nKi) + replacements["nHo"].append(nHo) + replacements["nWo"].append(nWo) + replacements["bKo"].append(bKo) + replacements["bKi"].append(bKi) + replacements["bHo"].append(bHo) + replacements["bWo"].append(bWo) + replacements["bHi"].append(bHi) + replacements["bWi"].append(bWi) + + inputInCubes.append(InCube) + + inputLoadSchedule = [] + outputLoadSchedule = [] + + for a in inputInCubes: + inputLoadSchedule.append({"data_in": a}) + + for out in outputCubes: + outputLoadSchedule.append({"data_out": out}) + + weightBuffer = ctxt.lookup(varWeight) + assert isinstance(weightBuffer, VariableBuffer) + weightShape = weightBuffer.shape + + if hasattr(weightBuffer, "_memoryLevel") and weightBuffer._memoryLevel == "WeightMemory_SRAM": + replacements['weight_addr_offset'] = [] + replacementTypes['weight_addr_offset'] = PointerClass(uint32_t) + for absoluteCube in absoluteOutputCubes: + COffset, CSize = absoluteCube.absoluteOffset[-1], absoluteCube.rectangle.dims[-1] + WeightCube = HyperRectangle((COffset, 0, 0), (CSize, weightShape[-2], weightShape[-1])) + replacements['weight_addr_offset'].append(calculateFlatOffsetInBytes(WeightCube, weightBuffer)) + else: + inputWeightBaseOffsets, outputWeightBaseOffsets = cls.extractBaseAddr(tilingSolution, targetMemLevel, + operatorRepresentation, ['weight']) + inputBaseOffsets.update(inputWeightBaseOffsets) + outputBaseOffsets.update(outputWeightBaseOffsets) + + for cube, load in zip(outputCubes, inputLoadSchedule): + COffset, CSize = cube.offset[-1], cube.dims[-1] + load['weight'] = HyperRectangle((COffset, 0, 0), (CSize, weightShape[-2], weightShape[-1])) + + tilingSchedule = TilingSchedule(inputBaseOffsets, outputBaseOffsets, inputLoadSchedule, outputLoadSchedule) + variableReplacementSchedule = VariableReplacementScheme(replacements, replacementTypes) + + return variableReplacementSchedule, tilingSchedule + + +class NE16RQSPWConv2DTileConstraint(NE16PWConv2DTileConstraint): + + @staticmethod + def addGeometricalConstraint(tilerModel: TilerModel, parseDict: Dict, ctxt: NetworkContext) -> TilerModel: + tilerModel = NE16PWConv2DTileConstraint.addGeometricalConstraint(tilerModel, parseDict, ctxt) + return requantAddGeometricalConstraint(tilerModel, parseDict, ctxt) + + @classmethod + def serializeTilingSolution( + cls, tilingSolution: NodeMemoryConstraint, absoluteOutputCubes: List[AbsoluteHyperRectangle], + targetMemLevel: str, ctxt: NetworkContext, + operatorRepresentation: OperatorRepresentation) -> Tuple[VariableReplacementScheme, TilingSchedule]: + variableReplacementSchedule, tilingSchedule = super().serializeTilingSolution( + tilingSolution, absoluteOutputCubes, targetMemLevel, ctxt, operatorRepresentation) + + addrNames = ['mul', 'add'] + inputRequantBaseOffsets, _ = cls.extractBaseAddr(tilingSolution, targetMemLevel, operatorRepresentation, + addrNames) + newInputBaseOffsets = {**tilingSchedule.inputBaseOffsets, **inputRequantBaseOffsets} + + requantSchedule = requantLoadSchedule(absoluteOutputCubes, ctxt, operatorRepresentation) + newInputLoadSchedule = [{ + **load, + **rqLoad + } for load, rqLoad in zip(tilingSchedule.inputLoadSchedule, requantSchedule)] + + newTilingSchedule = TilingSchedule(newInputBaseOffsets, tilingSchedule.outputBaseOffsets, newInputLoadSchedule, + tilingSchedule.outputLoadSchedule) + + return variableReplacementSchedule, newTilingSchedule diff --git a/Deeploy/Targets/NE16/TileConstraints/RequantHelpers.py b/Deeploy/Targets/NE16/TileConstraints/RequantHelpers.py new file mode 100644 index 0000000000..e1e4b16aea --- /dev/null +++ b/Deeploy/Targets/NE16/TileConstraints/RequantHelpers.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Dict, List + +from Deeploy.DeeployTypes import NetworkContext, OperatorRepresentation +from Deeploy.TilingExtension.TilerModel import TilerModel +from Deeploy.TilingExtension.TilingCodegen import AbsoluteHyperRectangle, HyperRectangle + + +def requantAddGeometricalConstraint(tilerModel: TilerModel, operatorRepresentation: OperatorRepresentation, + ctxt: NetworkContext) -> TilerModel: + outputBufferName = operatorRepresentation['data_out'] + mulBufferName = operatorRepresentation['mul'] + addBufferName = operatorRepresentation['add'] + + # Add I/O dimensions to the model as variables + for bufferName in [mulBufferName, addBufferName]: + tilerModel.addTensorDimToModel(ctxt, bufferName) + + outputChannelVar = tilerModel.getTensorDimVar(tensorName = outputBufferName, dimIdx = 3) + + addBuffer = ctxt.lookup(addBufferName) + addChannelVar = tilerModel.getTensorDimVar(tensorName = addBufferName, dimIdx = len(addBuffer.shape) - 1) + mulBuffer = ctxt.lookup(mulBufferName) + mulChannelVar = tilerModel.getTensorDimVar(tensorName = mulBufferName, dimIdx = len(mulBuffer.shape) - 1) + + tilerModel.addConstraint(outputChannelVar == addChannelVar) + tilerModel.addConstraint(outputChannelVar == mulChannelVar) + + return tilerModel + + +def requantLoadSchedule( + absoluteOutputCubes: List[AbsoluteHyperRectangle], + ctxt: NetworkContext, + operatorRepresentation: OperatorRepresentation, +) -> List[Dict[str, HyperRectangle]]: + outputCubes = [cube.rectangle for cube in absoluteOutputCubes] + + shapeMul = ctxt.lookup(operatorRepresentation["mul"]).shape + shapeAdd = ctxt.lookup(operatorRepresentation["add"]).shape + + schedule = [] + for cube in outputCubes: + (_, _, _, COffset) = cube.offset + (_, _, _, CSize) = cube.dims + MulCube = HyperRectangle((0,) * (len(shapeMul) - 1) + (COffset,), (1,) * (len(shapeMul) - 1) + (CSize,)) + AddCube = HyperRectangle((0,) * (len(shapeAdd) - 1) + (COffset,), (1,) * (len(shapeAdd) - 1) + (CSize,)) + schedule.append({"mul": MulCube, "add": AddCube}) + + return schedule diff --git a/Deeploy/Targets/NE16/TileConstraints/__init__.py b/Deeploy/Targets/NE16/TileConstraints/__init__.py new file mode 100644 index 0000000000..be436b64a3 --- /dev/null +++ b/Deeploy/Targets/NE16/TileConstraints/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from . import * diff --git a/Deeploy/Targets/NE16/Tiler.py b/Deeploy/Targets/NE16/Tiler.py new file mode 100644 index 0000000000..2bc53a441a --- /dev/null +++ b/Deeploy/Targets/NE16/Tiler.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + + +from Deeploy.Targets.NE16.Bindings import NE16DenseConv2DBindings, NE16DWConv2DBindings, NE16PWConv2DBindings, \ + NE16RQSDenseConv2DBindings, NE16RQSDWConv2DBindings, NE16RQSPWConv2DBindings +from Deeploy.Targets.NE16.TileConstraints.NE16DenseConstraint import NE16DenseConv2DTileConstraint, \ + NE16RQSDenseConv2DTileConstraint +from Deeploy.Targets.NE16.TileConstraints.NE16DepthwiseConstraint import NE16DWConv2DTileConstraint, \ + NE16RQSDWConv2DTileConstraint +from Deeploy.Targets.NE16.TileConstraints.NE16PointwiseConstraint import NE16PWConv2DTileConstraint, \ + NE16RQSPWConv2DTileConstraint +from Deeploy.TilingExtension.TilerExtension import TilingReadyNodeBindings + +NE16RQSPWConv2DTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = NE16RQSPWConv2DBindings, + tileConstraint = NE16RQSPWConv2DTileConstraint()) +NE16PWConv2DTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = NE16PWConv2DBindings, + tileConstraint = NE16PWConv2DTileConstraint()) + +NE16RQSDWConv2DTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = NE16RQSDWConv2DBindings, + tileConstraint = NE16RQSDWConv2DTileConstraint()) +NE16DWConv2DTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = NE16DWConv2DBindings, + tileConstraint = NE16DWConv2DTileConstraint()) + +NE16RQSDenseConv2DTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = NE16RQSDenseConv2DBindings, + tileConstraint = NE16RQSDenseConv2DTileConstraint()) +NE16DenseConv2DTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = NE16DenseConv2DBindings, + tileConstraint = NE16DenseConv2DTileConstraint()) diff --git a/Deeploy/Targets/NE16/TopologyOptimizationPasses/Passes.py b/Deeploy/Targets/NE16/TopologyOptimizationPasses/Passes.py new file mode 100644 index 0000000000..60a747aa00 --- /dev/null +++ b/Deeploy/Targets/NE16/TopologyOptimizationPasses/Passes.py @@ -0,0 +1,278 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +import itertools +import math +from functools import partial +from typing import Generator, List, Tuple + +import numpy as np +import numpy.typing as npt +import onnx_graphsurgeon as gs + +from Deeploy.CommonExtensions.OptimizationPasses.Matchers import Match, NonBranchingMatcher +from Deeploy.CommonExtensions.OptimizationPasses.PassClasses import ReplaceSequentialPatternPass, SequentialPass, \ + contextagnostic +from Deeploy.CommonExtensions.OptimizationPasses.TopologyOptimizationPasses.LoweringOptimizationPasses import \ + RemoveGlobalOutputReshapePass, _createReshape +from Deeploy.EngineExtension.OptimizationPasses.TopologyOptimizationPasses.EngineColoringPasses import \ + EngineDiscolorationPass +from Deeploy.Targets.Generic.TopologyOptimizationPasses.Passes import ReshapeConstOptPass, ReshapeMergePass + + +def _weightEncode(weight: npt.NDArray[np.uint8], bits: int, depthwise: bool = False) -> npt.NDArray[np.uint8]: + """NE16 weight encoder, ported from pulp-nnx/test/Ne16Weight.py. + + Expected weight shape: (cout, cin, H, W). + Output layout: (cout, cinMajor, Bits, H*W, cinMinorBytes) where + CIN_SUBTILE = 16 (single mode, no 1x1 vs 3x3 split like Neureka). + """ + _NE16_CIN_SUBTILE = 16 + + if depthwise: + weight = weight.transpose(1, 0, 2, 3) # Swap cout and cin + + cout, cin, height, width = weight.shape + + # Pad cin to be divisible with CIN_SUBTILE + if cin % _NE16_CIN_SUBTILE != 0: + cinPad = _NE16_CIN_SUBTILE - cin % _NE16_CIN_SUBTILE + weight = np.pad( + weight, + ((0, 0), (0, cinPad), (0, 0), (0, 0)), + "constant", + constant_values = 0, + ) + cin = cin + cinPad + + cinMajor = cin // _NE16_CIN_SUBTILE + cinMinor = _NE16_CIN_SUBTILE + + # (cout, cinMajor, cinMinor, H*W, 1) + weight = weight.reshape(cout, cinMajor, cinMinor, height * width, 1) + # (cout, cinMajor, cinMinor, H*W, Bits) + weight = np.unpackbits(weight, axis = -1, count = bits, bitorder = "little") + # (cout, cinMajor, Bits, H*W, cinMinor) + weight = weight.transpose(0, 1, 4, 3, 2) + # Pack cinMinor bits into bytes — 16 bits = 2 bytes + weight = weight.reshape(-1, 8) + weight = np.packbits(weight, axis = -1, bitorder = "little") + cinMinorBytes = cinMinor // 8 + # Layout rank varies by conv kind: + # - Dense 3x3 (!depthwise, kernel 3x3): rank 4 + # (cout, cinMajor, Bits, H*W*cinMinorBytes) + # — NE16DenseConstraint tiles over weight.shape[3]. + # - PW 1x1 and DW 3x3: rank 3 + # (cout, cinMajor, Bits*H*W*cinMinorBytes) + # — NE16{Pointwise,Depthwise}Constraint don't need a bits dim. + if not depthwise and height == 3 and width == 3: + return weight.reshape(cout, cinMajor, bits, height * width * cinMinorBytes) + return weight.reshape(cout, cinMajor, bits * height * width * cinMinorBytes) + + +def _ne16_adjust_weight_memory_layout_fun(graph: gs.Graph, match: Match, name: str, default_channels_first: bool, + ne16EngineName: str): + matched_nodes = list(match.nodes_map.values()) + node = matched_nodes[0] + + if not ("engine" in node.attrs and node.attrs["engine"] == ne16EngineName): + return graph + + weightTensor = node.inputs[1] + + if not isinstance(weightTensor, gs.Constant): + return graph + + # Adjust N-EUREKA's weights + values = weightTensor.values + + # Extract weight offset and translate weights by the offset + weight_offset = values.min() + values = values - weight_offset + node.attrs["weight_offset"] = weight_offset + + if "channels_first" in node.attrs: + channels_first = node.attrs["channels_first"] + else: + channels_first = default_channels_first + + # Weight encode expects channels-first (cout, cin_per_group, H, W) + if not channels_first: + values = values.transpose(0, 3, 1, 2) + + bits = 8 # Support only 8 bit weights for now + if node.attrs['group'] == 1: + weightTensor.values = _weightEncode(values.astype(np.uint8), bits, depthwise = False) + else: + # Depthwise: Deeploy's NHWC pass leaves weight as + # (cin_per_group=1, cout=group, H, W) after the transpose above; + # Ne16Weight.py's encode expects standard (cout, cin_per_group, H, W) + # — swap axes 0/1 before encoding so the result is a single packed + # (1, 1, packed_bytes) block across up to NE16_SUBTILE_INPUT_CHANNEL=16 + # parallel output channels. + values = values.transpose(1, 0, 2, 3) + weightTensor.values = _weightEncode(values.astype(np.uint8), bits, depthwise = True) + weightTensor.name = f"{name}_{weightTensor.name}" + + return graph + + +@contextagnostic +class NE16AdjustWeightMemoryLayoutPass(ReplaceSequentialPatternPass): + + def __init__(self, default_channels_first: bool, ne16EngineName: str): + graph = gs.Graph() + _input = gs.Variable(name = 'input_1') + output = graph.layer(inputs = [_input], outputs = ['out'], op = 'RequantizedConv|Conv', name = 'node') + graph.outputs.append(output) + graph.inputs.append(_input) + + super().__init__( + graph, + partial(_ne16_adjust_weight_memory_layout_fun, + default_channels_first = default_channels_first, + ne16EngineName = ne16EngineName), "_NE16_ADJUST_WEIGHT_MEMORY_LAYOUT_PASS", + NonBranchingMatcher(regex_op = True)) + + +def _findAllMultiplicands(x: int) -> List[int]: + multiplicands = [] + tmpX = x + for i in range(2, math.ceil(math.sqrt(x))): # Ceil cause range doesn't include the last number + while tmpX % i == 0: + multiplicands.append(i) + tmpX = tmpX / i + + if x // math.prod(multiplicands) > 1: + multiplicands.append(x // math.prod(multiplicands)) + + return multiplicands + + +def _findAllReshapeOptions(dim: int) -> Generator[Tuple[int, int], None, None]: + multiplicands = _findAllMultiplicands(dim) + for combLen in range(1, 1 + (len(multiplicands) // 2)): + for comb in itertools.combinations(multiplicands, combLen): + a = math.prod(comb) + b = dim // a + yield a, b + + +def _nSubtiles(dims: Tuple[int, int]): + return math.ceil(dims[0] / 6) * math.ceil(dims[1] / 6) + + +def _findLowestNumberOfSubtilesReshapeOptions(dim: int) -> List[Tuple[int, int]]: + lowestNumberOfSubtiles = dim + bestOptions: List[Tuple[int, int]] = [(dim, 1)] + for option in _findAllReshapeOptions(dim): + nSubtiles = _nSubtiles(option) + if nSubtiles < lowestNumberOfSubtiles: + lowestNumberOfSubtiles = nSubtiles + bestOptions = [option] + elif nSubtiles == lowestNumberOfSubtiles: + bestOptions.append(option) + return bestOptions + + +def _bestReshapeOption(dim: int) -> Tuple[int, int]: + smallestDim = dim + biggestDim = 1 + for option in _findLowestNumberOfSubtilesReshapeOptions(dim): + if option[0] < smallestDim: + smallestDim = option[0] + biggestDim = option[1] + elif option[1] < smallestDim: + smallestDim = option[1] + biggestDim = option[0] + return biggestDim, smallestDim + + +def _ne16_reshape_pointwise_convolution_fun(graph: gs.Graph, match: Match, name: str, default_channels_first: bool, + ne16EngineName: str): + matched_nodes = list(match.nodes_map.values()) + node = matched_nodes[0] + + if not ("engine" in node.attrs and node.attrs["engine"] == ne16EngineName): + return graph + + if not (node.attrs["kernel_shape"] == [1, 1]): + return graph + + if "channels_first" in node.attrs: + channels_first = node.attrs["channels_first"] + else: + channels_first = default_channels_first + + def extractSpatialDims(shape: List[int]) -> List[int]: + if channels_first: + return shape[-2:] + else: + return shape[-3:-1] + + def replaceSpatialDims(shape: List[int], newSpatialDims: Tuple[int, int]) -> List[int]: + if channels_first: + return shape[:-2] + list(newSpatialDims) + else: + return shape[:-3] + list(newSpatialDims) + shape[-1:] + + _input = node.inputs[0] + spatialDims = extractSpatialDims(_input.shape) + newSpatialDims = _bestReshapeOption(math.prod(spatialDims)) + newInputShape = replaceSpatialDims(_input.shape, newSpatialDims) + + inputReshapeNode, reshapedInput = _createReshape(_input, name, newInputShape) + graph.nodes.append(inputReshapeNode) + node.inputs[0] = reshapedInput + + output = node.outputs[0] + newOutputShape = replaceSpatialDims(output.shape, newSpatialDims) + reshapedOutput = gs.Variable(output.name + "_Reshaped", dtype = output.dtype, shape = newOutputShape) + outputReshapeNode, _ = _createReshape(reshapedOutput, name, output.shape, output) + graph.nodes.append(outputReshapeNode) + node.outputs[0] = reshapedOutput + + return graph + + +@contextagnostic +class NE16ReshapePointwiseConvolutionPass(ReplaceSequentialPatternPass): + """Reshape pointwise convolution's spatial dimensions so that they work better for N-EUREKA's hardware tiling""" + + def __init__(self, default_channels_first: bool, ne16EngineName: str): + graph = gs.Graph() + _input = gs.Variable(name = 'input_1') + output = graph.layer(inputs = [_input], outputs = ['out'], op = 'RequantizedConv|Conv', name = 'node') + graph.outputs.append(output) + graph.inputs.append(_input) + + super().__init__( + graph, + partial(_ne16_reshape_pointwise_convolution_fun, + default_channels_first = default_channels_first, + ne16EngineName = ne16EngineName), "_NE16_RESHAPE_POINTWISE_CONVOLUTION_PASS", + NonBranchingMatcher(regex_op = True)) + + +class ConvEngineDiscolorationPass(EngineDiscolorationPass): + + def __init__(self): + pattern = gs.Graph() + _input = gs.Variable(name = 'input') + output = pattern.layer(inputs = [_input], outputs = ['output'], op = 'RequantizedConv|Conv', name = 'conv') + pattern.outputs.append(output) + pattern.inputs.append(_input) + super().__init__(pattern, "_CONV_ENGINE_DISCOLORATION_PASS", matcher = NonBranchingMatcher(regex_op = True)) + + +@contextagnostic +class NE16OptimizationPass(SequentialPass): + + def __init__(self, default_channels_first: bool, ne16EngineName: str): + super().__init__(NE16AdjustWeightMemoryLayoutPass(default_channels_first, ne16EngineName), + NE16ReshapePointwiseConvolutionPass(default_channels_first, ne16EngineName), + ReshapeMergePass(), + ReshapeConstOptPass(), + RemoveGlobalOutputReshapePass(), + name_prefix = '') diff --git a/Deeploy/Targets/NE16/TopologyOptimizationPasses/__init__.py b/Deeploy/Targets/NE16/TopologyOptimizationPasses/__init__.py new file mode 100644 index 0000000000..be436b64a3 --- /dev/null +++ b/Deeploy/Targets/NE16/TopologyOptimizationPasses/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from . import * diff --git a/Deeploy/Targets/NE16/__init__.py b/Deeploy/Targets/NE16/__init__.py new file mode 100644 index 0000000000..be436b64a3 --- /dev/null +++ b/Deeploy/Targets/NE16/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2024 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from . import * diff --git a/Deeploy/Targets/PULPOpen/Templates/FloatGemmTemplate.py b/Deeploy/Targets/PULPOpen/Templates/FloatGemmTemplate.py index 59499706e5..280cb4ff6e 100644 --- a/Deeploy/Targets/PULPOpen/Templates/FloatGemmTemplate.py +++ b/Deeploy/Targets/PULPOpen/Templates/FloatGemmTemplate.py @@ -4,7 +4,10 @@ from typing import Dict, List, Tuple -from Deeploy.AbstractDataTypes import float32_tPtr +from Deeploy.AbstractDataTypes import PointerClass +from Deeploy.CommonExtensions.DataTypes import float32_t + +float32_tPtr = PointerClass(float32_t) from Deeploy.DeeployTypes import NetworkContext, NodeTemplate, OperatorRepresentation diff --git a/DeeployTest/CMakeLists.txt b/DeeployTest/CMakeLists.txt index b7f3535790..4e45904541 100644 --- a/DeeployTest/CMakeLists.txt +++ b/DeeployTest/CMakeLists.txt @@ -50,7 +50,7 @@ elseif(DEEPLOY_ARCH STREQUAL SNITCH) add_subdirectory(Platforms/Snitch) elseif(DEEPLOY_ARCH STREQUAL CHIMERA) add_subdirectory(Platforms/Chimera) -elseif(platform STREQUAL GAP9) +elseif(platform STREQUAL GAP9 OR platform STREQUAL GAP9_w_NE16) # Search for hex files generated by Python code generator # These files indicate L3 mode (external memory with readfs) diff --git a/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ/inputs.npz b/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ/inputs.npz new file mode 100644 index 0000000000000000000000000000000000000000..cfd8568bf15e5dc8924e45317d77c9725336c764 GIT binary patch literal 8456 zcmb8#L28wE7=`ig8ZFq7gDl`Wx)p-pq!n@8Nl-HBpraBrqEKQ;R1hh24;{J`7vcuo zM|-dO>)?TxOQPZ7ocEmb{}zyw&rKnr{{mXzI=YR z^M5`4^XhEtU%foNINR!<9vrP7{Cu>2xcz_cX#Lmv`aeIezJ11WBjn@v^FkkdH&EXl z^-!Lc+$vZDDOULd3$teN6wzybeGeUpESR;;~hH__1ojy>76gbuLMym#b~k9t_&p8h^!BR2PwUmE-R1dq-LbwN z>f_XNFYV}IecyrZ-P4zImq&NhYu;T?Zt~^Oo;>Z2eEWRlH&-q1J>;W3cRTLY^6q-{ zl-F~o^^uSKsn>Vq_1mk~*Kz-Y{9lCeEJ><*t(H;8^tzXSYJCokmceiuh@6^|;_FeVqzRS1gZijZ~qdPtA$#q9w zALaDquy;9rcWAlpxT768Y+t^UOsxScm9-fM}6<}C*57zjy}KIt~;&I$M$K|m$$>G<*|7=)GzfX zzq@jiZx7{t2fuX3-f4T@FZJcB?dZwT@+dDym;3I`dvEgHeaE!VM|=8wbhnH0`)Ef_ zpHI8&w$vpGG^~rBU84Umx93&mH-8Xw>hWZwK`z+Ce?+E-i0Y9($KVJMw%z zcRtFyqkH?k%kkyATP=@v!#e)HGe(YNP2`83+6_3G>Mj!&1~lS4bGH)%WWsON6Ke)I0~`|k4S zJ*;oHzIRZcZk~3xi}mfb$Csz&?A5Ps-yPlU*+)5SjxOz@J(Q=Lo3vi z_2qH8>rd2kr|n{W-^Zts-(9+tM>|+wzuzbCPM3OUALZR?wC{d$yt;9CF#Yj@^!D!+ az83!fQ-+h{TR-fMtL?vEw*0i-ss8}SjW_53 literal 0 HcmV?d00001 diff --git a/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ/network.onnx b/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ/network.onnx new file mode 100644 index 0000000000000000000000000000000000000000..c357ce307e354f5ceef2f5c2343ac300481c6989 GIT binary patch literal 9817 zcmbta&2F5<67^sM!>1q_^SeP@vGjrsJ2qn_AfX7cfCUmlvWz@8u`rVm+qqdEQX{Nm;D@^JC^U~ljC z(I1P02`&0@zW1(?kJWdJ9{Ji!JE(VkW#=B!1kPQ?oYYlD z@}*C{^8sTY9@q1&yER@ID{y%`{|qO-CEa=bo1D|zef1s451jLHTZ%3gNIN1?PR7laDUU2zS(*Y z*X1Lx=BM6yzIC)8^*r78jF{)*?MHrMo>_V6?L5SESBQ<>O9%9M`_Hm08NRS{Qj;9pUW9xB9B)9W$tjo0@t9SGGL8c7AB9PklpruQcM+Yp<>!a*E6OzDwG z<~FuvR&M&8iww(>KXyp{I7^)7YhU3z8Q81SYTiAysnav=zu=;Lgk>eY;w8L9G zbH`I$dLm=tP%k04ydu`K)S~_ChuNtS{*`4Tt zPT$&Tg;S$T%r9~Ec=J?G+_1*BUui-QP4uQ^uOm)b;Q{Zs^o6D0`|gcd>Sy!F=dC@^ zORK7#Tq`UzGjrT4t^C@ldihs(`-FylVqIS1!h8o|k*z#bX;h2PEbp7T{T9B>KK0Vf z?Mcq}?zG4u#}nD?b%EEsd^e?eh~x5%v*`^yYrIE2xYCUr`tqc%X0bPY=n`wL_UqXr z9)5Uu#@r6$oYV!LbILB)eCq;pJoL$p-q;D8G~!i# zmk{AcnEFxM^_+`+gsH|{;hJx~c>kvGLaUv%hx!S7O#~Iyj$dIo*%#}CaO=UQEozl`X z=u1OfmpWebJbJT654d}&!b+s4efDdgx+?Lws6ez6WKOjx=v$x zot`-IVm4}c+3T*7V+AHYDlk?5F69*`A2ad>2V8UIzpqyoE01zi8`^f%d}S5K**=r+ zyp?H~{Ihk{Qsu7-T<@BD&y1a!^jfa?sLT*Y9eN$@7rN9Gm%8$z@ATP+SKMltgeNStzH;@>Jm=++R-9M8sfl;y zs4on2;*BbN~90|4U05z8`*hW_jfB#CJfecT~>c6^^%f;aLF+xGoC zZ{<=yYKY~fk7s8tJbuOua>@|LD!je!X6rjj1Fic!OBuD3?NT|boIi4jSBCN0Y2AmIR=x};%xxi&z^8WmvDgXNFKE=a>Y47muzX3m+lSlvn literal 0 HcmV?d00001 diff --git a/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ/outputs.npz b/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ/outputs.npz new file mode 100644 index 0000000000000000000000000000000000000000..76f10c49d9a4bbb40094fa02b0c2b90d36766a89 GIT binary patch literal 8458 zcmeI&TWg$U5C`ygn^tQ)RgLG<8{4?8ZqA({W&`Lx8Pm3q#4A9z}mcPpp=c(XZEW3*>Ktq11hdFP8t@Be?YYZs8Gm%j3Do_Z`5X z;$^?^>XgUpWpAIt-Jt*c;@PX8-5U*maBr8MAN#9Le)*G^d6KIGeW4Wku7B|2yE2c|C!c*uy*){JkI(+X>vPVBJ}G?4 zvmf?NT=U8L%)xx%(tq;o>;*n??oF@%_DH_zC;9Z}9waXF;$J*r1L|Ik70-K6hy2B}hi?{MpZLF8a{gZm96#?x zys>yxKh%T1RXD_*`>+K1cddBTeE3zDeb!g@;$1I4?l(W|g2gRp9cO>VOeN~72)a^dnPkSI=`>sB7u5<=`z6bTd z+4r#o{Mkcyh#!7Ycyog1SN-Z{r!IY#k2w5!FXqXP-u%VoM|^egBOiCooWxC>`pik+ zeLt~RpZ?i;g@eUX3WEslHS-jU0bT>soL zIN#6eu~#UZ-zhmayghYK;l+2q`Aa!JD4hA&Yj;yUz8~Pp@%op)m&AwDuZ*t`euw0K zW?lNoZ_L*r`s}NH z<{!_mIr8H@h{K=XMfxNj`K;=_T9hCCgZs4fh&;uIUoU{ZZx(Ovxi99%j$gd_^CzA> z=<)VB_edS$!?8;$U-En(5n`qzv@lB zJ8A#v?Hk@6qvWXmnUg-~o4Lt{UVpM5-b>~oUXOZDJzP(pdG+x0J-b#P+4rbE>W}`j z6CaNJywse|YtL?HdvalIt8QG&HrBEq(P7WmSJc6(n(2^VKQvZVZV(F>>pF7 BPul None: "siracusa_neureka_tiled: mark test as a Siracusa + Neureka platform test (tiled)") config.addinivalue_line("markers", "gap9: mark test as a GAP9 platform test") config.addinivalue_line("markers", "gap9_tiled: mark test as a GAP9 platform test (tiled)") + config.addinivalue_line("markers", "gap9_w_ne16_tiled: mark test as a GAP9 + NE16 platform test (tiled)") config.addinivalue_line("markers", "kernels: mark test as a kernel test (individual operators)") config.addinivalue_line("markers", "models: mark test as a model test (full networks)") config.addinivalue_line("markers", "singlebuffer: mark test as single-buffer configuration") diff --git a/DeeployTest/deeployRunner_tiled_gap9_w_ne16.py b/DeeployTest/deeployRunner_tiled_gap9_w_ne16.py new file mode 100644 index 0000000000..63c2277789 --- /dev/null +++ b/DeeployTest/deeployRunner_tiled_gap9_w_ne16.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +import sys + +from testUtils.deeployRunner import main + +if __name__ == "__main__": + + # Define parser setup callback to add GAP9+NE16-specific arguments + def setup_parser(parser): + parser.add_argument('--cores', type = int, default = 8, help = 'Number of cores (default: 8)\n') + parser.add_argument('--ne16-wmem', action = 'store_true', help = 'Enable NE16 weight memory\n') + parser.add_argument('--enable-3x3', action = 'store_true', help = 'Enable 3x3 convolutions\n') + + sys.exit( + main(default_platform = "GAP9_w_NE16", + default_simulator = "gvsoc", + tiling_enabled = True, + parser_setup_callback = setup_parser)) diff --git a/DeeployTest/testMVP.py b/DeeployTest/testMVP.py index 9678bc4e4f..56d05806c0 100644 --- a/DeeployTest/testMVP.py +++ b/DeeployTest/testMVP.py @@ -96,7 +96,7 @@ def setupDeployer(graph: gs.Graph, memoryHierarchy: MemoryHierarchy, defaultTarg scheduler = _mockScheduler) # Make the deployer engine-color-aware - if args.platform == "Siracusa_w_neureka": + if args.platform in ("Siracusa_w_neureka", "GAP9_w_NE16"): deployer = EngineColoringDeployerWrapper(deployer) # Make platform memory-aware after mapDeployer because it requires the platform to be an instance of an unwrapped platform diff --git a/DeeployTest/testUtils/core/execution.py b/DeeployTest/testUtils/core/execution.py index 4c6c972679..24c1f7154e 100644 --- a/DeeployTest/testUtils/core/execution.py +++ b/DeeployTest/testUtils/core/execution.py @@ -132,7 +132,7 @@ def build_binary(config: DeeployTestConfig) -> None: ] # GAP9 requires the 'image' target to generate MRAM .bin files for GVSOC - if config.platform == 'GAP9': + if config.platform in ('GAP9', 'GAP9_w_NE16'): cmd.append("image") env = os.environ.copy() diff --git a/DeeployTest/testUtils/platformMapping.py b/DeeployTest/testUtils/platformMapping.py index 9d526906f9..4dc2bbf824 100644 --- a/DeeployTest/testUtils/platformMapping.py +++ b/DeeployTest/testUtils/platformMapping.py @@ -20,6 +20,8 @@ from Deeploy.Targets.Generic.Platform import GenericOptimizer, GenericPlatform from Deeploy.Targets.MemPool.Deployer import MemPoolDeployer from Deeploy.Targets.MemPool.Platform import MemPoolOptimizer, MemPoolPlatform +from Deeploy.Targets.NE16.Deployer import NE16Deployer +from Deeploy.Targets.NE16.Platform import MemoryNE16Platform, MemoryNE16PlatformWrapper, NE16Optimizer, NE16Platform from Deeploy.Targets.Neureka.Deployer import NeurekaDeployer from Deeploy.Targets.Neureka.Platform import MemoryNeurekaPlatform, MemoryNeurekaPlatformWrapper, NeurekaOptimizer, \ NeurekaPlatform @@ -31,7 +33,7 @@ from Deeploy.Targets.SoftHier.Platform import SoftHierOptimizer, SoftHierPlatform _SIGNPROP_PLATFORMS = ["Apollo3", "Apollo4", "QEMU-ARM", "Generic", "MemPool", "SoftHier"] -_NONSIGNPROP_PLATFORMS = ["Siracusa", "Siracusa_w_neureka", "PULPOpen", "Snitch", "Chimera", "GAP9"] +_NONSIGNPROP_PLATFORMS = ["Siracusa", "Siracusa_w_neureka", "PULPOpen", "Snitch", "Chimera", "GAP9", "GAP9_w_NE16"] _PLATFORMS = _SIGNPROP_PLATFORMS + _NONSIGNPROP_PLATFORMS @@ -67,6 +69,9 @@ def mapPlatform(platformName: str) -> Tuple[DeploymentPlatform, bool]: elif platformName == "Siracusa_w_neureka": Platform = NeurekaPlatform() + elif platformName == "GAP9_w_NE16": + Platform = NE16Platform() + elif platformName == "Snitch": Platform = SnitchPlatform() @@ -90,6 +95,8 @@ def setupMemoryPlatform(platform: DeploymentPlatform, memoryHierarchy: MemoryHie weightMemoryLevel = memoryHierarchy.memoryLevels["WeightMemory_SRAM"] \ if "WeightMemory_SRAM" in memoryHierarchy.memoryLevels else None return MemoryNeurekaPlatformWrapper(platform, memoryHierarchy, defaultTargetMemoryLevel, weightMemoryLevel) + elif isinstance(platform, NE16Platform): + return MemoryNE16PlatformWrapper(platform, memoryHierarchy, defaultTargetMemoryLevel) if isinstance(platform, GAP9Platform): return MemoryGAP9PlatformWrapper(platform, memoryHierarchy, defaultTargetMemoryLevel) else: @@ -207,6 +214,23 @@ def mapDeployer(platform: DeploymentPlatform, default_channels_first = default_channels_first, deeployStateDir = deeployStateDir) + elif isinstance(platform, (NE16Platform, MemoryNE16Platform, MemoryNE16PlatformWrapper)): + + if loweringOptimizer is None: + loweringOptimizer = NE16Optimizer + + if default_channels_first is None: + default_channels_first = False + + deployer = NE16Deployer(graph, + platform, + inputTypes, + loweringOptimizer, + scheduler, + name = name, + default_channels_first = default_channels_first, + deeployStateDir = deeployStateDir) + elif isinstance(platform, (GAP9Platform, MemoryGAP9Platform, MemoryGAP9PlatformWrapper)): if loweringOptimizer is None: diff --git a/DeeployTest/test_gap9_ne16_tiled_config.py b/DeeployTest/test_gap9_ne16_tiled_config.py new file mode 100644 index 0000000000..7dde8bdf5d --- /dev/null +++ b/DeeployTest/test_gap9_ne16_tiled_config.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2026 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 +"""Test configuration for GAP9 platform with NE16 accelerator (tiled). + +NE16-supported convolution kernels verified to dispatch to NE16 +(`ne16_nnx_dispatch` appears in generated Network.c) and PASS on +gvsoc gap9.evk: + +- PW 1x1 RQ Conv (PW_2D_RQ/Regular_RQ) +- PW 1x1 Conv (PW_2D) +- DW 3x3 RQ Conv (DW_2D_RQ, with --enable-3x3) +- 3x3 strided RQ (StriddedPadded_2D_RQ) — falls back to cluster + because stride 2x2 requires --enableStrides which isn't wired into + the tiled runner today; still PASS via the cluster kernel. +""" + +DEFAULT_CORES = 8 + +L2_SINGLEBUFFER_KERNELS = { + "Kernels/Integer/Conv/PW_2D_RQ/Regular_RQ": [32000, 16000], + "Kernels/Integer/Conv/PW_2D": [32000], + "Kernels/Integer/Conv/DW_2D_RQ": [32000, 16000], + "Kernels/Integer/Conv/Dense_2D_RQ": [32000], + "Kernels/Integer/Conv/StriddedPadded_2D_RQ": [32000], +} + +L2_DOUBLEBUFFER_KERNELS = { + "Kernels/Integer/Conv/PW_2D_RQ/Regular_RQ": [32000], + "Kernels/Integer/Conv/DW_2D_RQ": [32000], +} + +L2_SINGLEBUFFER_MODELS = {} + +L3_SINGLEBUFFER_MODELS = {} +L3_DOUBLEBUFFER_MODELS = {} +L2_SINGLEBUFFER_KERNELS_WMEM = {} +L3_DOUBLEBUFFER_MODELS_WMEM = {} diff --git a/DeeployTest/test_platforms.py b/DeeployTest/test_platforms.py index 6d9f3cfcd7..daad19e6ce 100644 --- a/DeeployTest/test_platforms.py +++ b/DeeployTest/test_platforms.py @@ -11,6 +11,10 @@ from test_gap9_config import DEFAULT_NUM_CORES as GAP9_DEFAULT_NUM_CORES from test_gap9_config import KERNEL_TESTS as GAP9_KERNEL_TESTS from test_gap9_config import MODEL_TESTS as GAP9_MODEL_TESTS +from test_gap9_ne16_tiled_config import DEFAULT_CORES as GAP9_NE16_TILED_DEFAULT_CORES +from test_gap9_ne16_tiled_config import L2_DOUBLEBUFFER_KERNELS as GAP9_NE16_L2_DOUBLEBUFFER_KERNELS +from test_gap9_ne16_tiled_config import L2_SINGLEBUFFER_KERNELS as GAP9_NE16_L2_SINGLEBUFFER_KERNELS +from test_gap9_ne16_tiled_config import L2_SINGLEBUFFER_MODELS as GAP9_NE16_L2_SINGLEBUFFER_MODELS from test_gap9_tiled_config import DEFAULT_CORES as GAP9_TILED_DEFAULT_CORES from test_gap9_tiled_config import L2_DOUBLEBUFFER_KERNELS as GAP9_L2_DOUBLEBUFFER_KERNELS from test_gap9_tiled_config import L2_DOUBLEBUFFER_MODELS as GAP9_L2_DOUBLEBUFFER_MODELS @@ -133,6 +137,7 @@ def param_id(param): # siracusa_neureka_tiled: tests from the Siracusa + Neureka platform (tiled) # gap9: tests from the GAP9 platform (untiled) # gap9_tiled: tests from the GAP9 platform (tiled) +# gap9_w_ne16_tiled: tests from the GAP9 + NE16 platform (tiled) # Test type markers: # kernels: single kernel (or single layer) tests # models: full model (multiple layer) tests @@ -987,3 +992,104 @@ def test_gap9_tiled_models_l3_doublebuffer(test_params, deeploy_test_dir, toolch double_buffer = True, ) run_and_assert_test(test_name, config, skipgen, skipsim) + + +@pytest.mark.gap9_w_ne16_tiled +@pytest.mark.kernels +@pytest.mark.singlebuffer +@pytest.mark.l2 +@pytest.mark.parametrize( + "test_params", + generate_test_params(GAP9_NE16_L2_SINGLEBUFFER_KERNELS, "L2-singlebuffer"), + ids = param_id, +) +def test_gap9_w_ne16_tiled_kernels_l2_singlebuffer(test_params, deeploy_test_dir, toolchain, toolchain_dir, cmake_args, + skipgen, skipsim) -> None: + test_name, l1, config_name = test_params + + ne16_cmake_args = cmake_args + [f"NUM_CORES={GAP9_NE16_TILED_DEFAULT_CORES}"] + + # --enable-3x3 is additive (extends NE16Engine.canExecute to DW/Dense 3x3); + # safe to enable for all three kernel cases (PW 1x1 + DW 3x3 + Dense 3x3). + config = create_test_config( + test_name = test_name, + platform = "GAP9_w_NE16", + simulator = "gvsoc", + deeploy_test_dir = deeploy_test_dir, + toolchain = toolchain, + toolchain_dir = toolchain_dir, + cmake_args = ne16_cmake_args, + tiling = True, + cores = GAP9_NE16_TILED_DEFAULT_CORES, + l1 = l1, + default_mem_level = "L2", + double_buffer = False, + gen_args = ["--enable-3x3"], + ) + run_and_assert_test(test_name, config, skipgen, skipsim) + + +@pytest.mark.gap9_w_ne16_tiled +@pytest.mark.models +@pytest.mark.singlebuffer +@pytest.mark.l2 +@pytest.mark.parametrize( + "test_params", + generate_test_params(GAP9_NE16_L2_SINGLEBUFFER_MODELS, "L2-singlebuffer"), + ids = param_id, +) +def test_gap9_w_ne16_tiled_models_l2_singlebuffer(test_params, deeploy_test_dir, toolchain, toolchain_dir, cmake_args, + skipgen, skipsim) -> None: + test_name, l1, config_name = test_params + + ne16_cmake_args = cmake_args + [f"NUM_CORES={GAP9_NE16_TILED_DEFAULT_CORES}"] + + config = create_test_config( + test_name = test_name, + platform = "GAP9_w_NE16", + simulator = "gvsoc", + deeploy_test_dir = deeploy_test_dir, + toolchain = toolchain, + toolchain_dir = toolchain_dir, + cmake_args = ne16_cmake_args, + tiling = True, + cores = GAP9_NE16_TILED_DEFAULT_CORES, + l1 = l1, + default_mem_level = "L2", + double_buffer = False, + gen_args = ["--enable-3x3"], + ) + run_and_assert_test(test_name, config, skipgen, skipsim) + + +@pytest.mark.gap9_w_ne16_tiled +@pytest.mark.kernels +@pytest.mark.doublebuffer +@pytest.mark.l2 +@pytest.mark.parametrize( + "test_params", + generate_test_params(GAP9_NE16_L2_DOUBLEBUFFER_KERNELS, "L2-doublebuffer"), + ids = param_id, +) +def test_gap9_w_ne16_tiled_kernels_l2_doublebuffer(test_params, deeploy_test_dir, toolchain, toolchain_dir, cmake_args, + skipgen, skipsim) -> None: + test_name, l1, config_name = test_params + + ne16_cmake_args = cmake_args + [f"NUM_CORES={GAP9_NE16_TILED_DEFAULT_CORES}"] + + config = create_test_config( + test_name = test_name, + platform = "GAP9_w_NE16", + simulator = "gvsoc", + deeploy_test_dir = deeploy_test_dir, + toolchain = toolchain, + toolchain_dir = toolchain_dir, + cmake_args = ne16_cmake_args, + tiling = True, + cores = GAP9_NE16_TILED_DEFAULT_CORES, + l1 = l1, + default_mem_level = "L2", + double_buffer = True, + gen_args = ["--enable-3x3"], + ) + run_and_assert_test(test_name, config, skipgen, skipsim) diff --git a/TargetLibraries/GAP9/CMakeLists.txt b/TargetLibraries/GAP9/CMakeLists.txt index ca4c3ffbeb..8051484dd0 100644 --- a/TargetLibraries/GAP9/CMakeLists.txt +++ b/TargetLibraries/GAP9/CMakeLists.txt @@ -80,5 +80,21 @@ endif() target_link_libraries(deeploygap9 PUBLIC pulp-nn-mixed) +# NE16 accelerator (via pulp-nnx) for GAP9_w_NE16 platform +if(platform STREQUAL "GAP9_w_NE16") + set(USE_NE16 ON CACHE BOOL "Use the NE16 accelerator." FORCE) + add_subdirectory(../third_party/pulp-nnx ${CMAKE_CURRENT_BINARY_DIR}/pulp-nnx) + target_link_libraries(pulp-nnx PUBLIC pmsis) + target_compile_options(pulp-nnx PRIVATE + -Wno-error + -Wno-implicit-int-conversion + -Wno-sign-conversion + -Wno-typedef-redefinition + -Wno-unused-parameter + -Wno-incompatible-pointer-types-discards-qualifiers + ) + target_link_libraries(deeploygap9 PUBLIC pulp-nnx) +endif() + target_link_libraries(deeploygap9 PUBLIC m) From 3a8bf481227fc9d1846f1791f27e3afab7d29e19 Mon Sep 17 00:00:00 2001 From: Pu DENG Date: Tue, 14 Apr 2026 10:22:39 +0000 Subject: [PATCH 02/14] [Deeploy PR] NE16 Linear Layer Kernels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add NE16 linear layer kernels, including a topology pass, NE16 templates, parsers, tile constraints, and bindings - The topology pass recognizes NE16-compatible GEMM layers, adjusts the weight layout for the NE16, and converts the requant shift/scale to the NE16 format - The template detects whether the input is signed; if so, it adds a +128 offset to the input during C runtime and compensates via the bias - Add GAP9 SDK-based Dequant/Quant templates using CNN_Copy.c kernels, replacing the generic templates - Add a generic DequantQuantMergePass that folds adjacent Dequant→Quant pairs into identity or RequantShift - Add a GAP9-specific TopologyOptimizer (GAP9Optimizer) to replace PULPOptimizer Bug fixes: - Add output signedness check in QuantChecker - Fix L3 DMA template (add proper casts) and remove the blocking L3 DMA hack - Isolate dory memory functions from other libraries in CMakeLists so they compile with -Og while compute kernels compile with -O3 - Disable PULPAddRequantMergePass due to incorrect pattern matching when Add has multiple consumers Co-authored-by: runwangdl --- Deeploy/Targets/GAP9/Bindings.py | 42 ++- Deeploy/Targets/GAP9/DMA/L3Dma.py | 9 +- Deeploy/Targets/GAP9/Parsers.py | 37 +++ Deeploy/Targets/GAP9/Platform.py | 72 ++++-- .../Templates/GAP9SDKDequantQuantTemplate.py | 168 ++++++++++++ .../GAP9/Templates/NE16GEMMTemplate.py | 242 ++++++++++++++++++ .../TileConstraints/NE16GEMMTileConstraint.py | 196 ++++++++++++++ Deeploy/Targets/GAP9/Tiler.py | 30 ++- .../GAP9/TopologyOptimizationPasses/Passes.py | 132 ++++++++++ .../TopologyOptimizationPasses/__init__.py | 3 + .../TopologyOptimizationPasses/Passes.py | 82 ++++++ Deeploy/Targets/Generic/TypeCheckers.py | 9 + DeeployTest/Platforms/GAP9/CMakeLists.txt | 1 + DeeployTest/testUtils/platformMapping.py | 4 +- TargetLibraries/GAP9/CMakeLists.txt | 46 +++- TargetLibraries/GAP9/inc/ne16_utils.h | 23 ++ TargetLibraries/GAP9/src/ne16_utils.c | 35 +++ 17 files changed, 1087 insertions(+), 44 deletions(-) create mode 100644 Deeploy/Targets/GAP9/Parsers.py create mode 100644 Deeploy/Targets/GAP9/Templates/GAP9SDKDequantQuantTemplate.py create mode 100644 Deeploy/Targets/GAP9/Templates/NE16GEMMTemplate.py create mode 100644 Deeploy/Targets/GAP9/TileConstraints/NE16GEMMTileConstraint.py create mode 100644 Deeploy/Targets/GAP9/TopologyOptimizationPasses/Passes.py create mode 100644 Deeploy/Targets/GAP9/TopologyOptimizationPasses/__init__.py create mode 100644 TargetLibraries/GAP9/inc/ne16_utils.h create mode 100644 TargetLibraries/GAP9/src/ne16_utils.c diff --git a/Deeploy/Targets/GAP9/Bindings.py b/Deeploy/Targets/GAP9/Bindings.py index 2bda98af8f..ad215b9193 100644 --- a/Deeploy/Targets/GAP9/Bindings.py +++ b/Deeploy/Targets/GAP9/Bindings.py @@ -18,11 +18,12 @@ from Deeploy.DeeployTypes import CodeTransformation, NodeBinding from Deeploy.FutureExtension.Bindings.AutoFutureBinding import AutoFutureBinding from Deeploy.FutureExtension.CodeTransformationPasses.FutureCodeTransformation import FutureGeneration -from Deeploy.Targets.GAP9.DMA.L3Dma import gap9L3DmaHack +from Deeploy.Targets.GAP9.DMA.L3Dma import GAP9L3Dma from Deeploy.Targets.GAP9.DMA.MchanDma import GAP9MchanDma +from Deeploy.Targets.GAP9.Templates import GAP9SDKDequantQuantTemplate, NE16GEMMTemplate # Import templates from PULPOpen and Generic from Deeploy.Targets.Generic.Templates import AddTemplate, ConcatTemplate, DequantTemplate, FloatReduceMeanTemplate, \ - FloatReduceSumTemplate, GatherTemplate, QuantTemplate, RQSiGELUTemplate, SliceTemplate, iHardswishTemplate + FloatReduceSumTemplate, GatherTemplate, RQSiGELUTemplate, SliceTemplate, iHardswishTemplate from Deeploy.Targets.Generic.TypeCheckers import AddChecker, ConcatChecker, ConvChecker, DequantChecker, \ GatherChecker, GELUChecker, GEMMChecker, HardswishChecker, LayerNormChecker, MatMulChecker, MulChecker, \ QuantChecker, ReduceMeanChecker, ReluChecker, ReshapeChecker, RQAddChecker, RQHardswishChecker, SGDChecker, \ @@ -57,7 +58,7 @@ MemoryManagementGeneration("L1"), TilingVariableReplacement("L2"), MemoryAwareFunctionCallClosure(writeback = False, generateStruct = True), - PULPL3Tiling("L3", "L2", gap9L3DmaHack), # Use GAP9-specific L3 DMA + PULPL3Tiling("L3", "L2", GAP9L3Dma()), # Use GAP9-specific L3 DMA PULPProfileUntiled(), ArgumentStructGeneration(), L3MemoryAwareFunctionCallClosure(writeback = False), @@ -76,7 +77,7 @@ MemoryManagementGeneration("L1"), TilingVariableReplacement("L2"), MemoryAwareFunctionCallClosure(writeback = False, generateStruct = True), - PULPL3Tiling("L3", "L2", gap9L3DmaHack), # Use GAP9-specific L3 DMA + PULPL3Tiling("L3", "L2", GAP9L3Dma()), # Use GAP9-specific L3 DMA PULPProfileUntiled(), ArgumentStructGeneration(), L3MemoryAwareFunctionCallClosure(writeback = False), @@ -183,6 +184,26 @@ GAP9Transformer) for type1, type2 in zip([int8_t, uint8_t, int8_t, uint8_t], [int8_t, uint8_t, uint8_t, int8_t]) ] +GAP9NE16RQSGEMMBindings = [ + NodeBinding( + PULPLinearChecker([ + PointerClass(type1), + PointerClass(int8_t), + PointerClass(int32_t), + PointerClass(uint8_t), + PointerClass(uint8_t) + ], [PointerClass(type2)]), NE16GEMMTemplate.referenceTemplate, GAP9ClusterTransformer) + for type1 in [int8_t, uint8_t] + for type2 in [int8_t, uint8_t] +] + +GAP9NE16GEMMInt32Bindings = [ + NodeBinding( + GEMMChecker([PointerClass(type1), PointerClass(int8_t), + PointerClass(int32_t)], [PointerClass(int32_t)]), NE16GEMMTemplate.int32OutputTemplate, + GAP9ClusterTransformer) for type1 in [int8_t, uint8_t] +] + GAP9FloatGEMMBindings = [ NodeBinding( GEMMChecker([PointerClass(float32_t), PointerClass(float32_t), @@ -386,14 +407,17 @@ ] GAP9QuantBindings = [ - NodeBinding(QuantChecker([PointerClass(float32_t)], [PointerClass(int8_t)]), QuantTemplate.referenceTemplate, - GAP9Transformer), + NodeBinding(QuantChecker([PointerClass(float32_t)], [PointerClass(int8_t)]), + GAP9SDKDequantQuantTemplate.fp32QuantI8Template, GAP9Transformer), + NodeBinding(QuantChecker([PointerClass(float32_t)], [PointerClass(uint8_t)]), + GAP9SDKDequantQuantTemplate.fp32QuantU8Template, GAP9Transformer), ] GAP9DequantBindings = [ - NodeBinding(DequantChecker([PointerClass(int8_t)], [PointerClass(float32_t)]), DequantTemplate.referenceTemplate, - GAP9Transformer), -] + [ + NodeBinding(DequantChecker([PointerClass(int8_t)], [PointerClass(float32_t)]), + GAP9SDKDequantQuantTemplate.fp32DequantI8Template, GAP9Transformer), + NodeBinding(DequantChecker([PointerClass(uint8_t)], [PointerClass(float32_t)]), + GAP9SDKDequantQuantTemplate.fp32DequantU8Template, GAP9Transformer), NodeBinding(DequantChecker([PointerClass(int32_t)], [PointerClass(float32_t)]), DequantTemplate.referenceTemplate, GAP9Transformer), ] diff --git a/Deeploy/Targets/GAP9/DMA/L3Dma.py b/Deeploy/Targets/GAP9/DMA/L3Dma.py index adbf161328..aadc5974b9 100644 --- a/Deeploy/Targets/GAP9/DMA/L3Dma.py +++ b/Deeploy/Targets/GAP9/DMA/L3Dma.py @@ -6,8 +6,7 @@ from typing import Dict, Tuple from Deeploy.DeeployTypes import NetworkContext, NodeTemplate, OperatorRepresentation, VariableBuffer -from Deeploy.TilingExtension.AsyncDma import AsyncDma, BlockingDmaFromAsyncDmaAdapter, DmaDirection, Future, \ - PerTensorWaitingStrategy +from Deeploy.TilingExtension.AsyncDma import AsyncDma, DmaDirection, Future, PerTensorWaitingStrategy class GAP9L3DmaFuture(Future): @@ -29,7 +28,7 @@ class GAP9L3Dma(AsyncDma): _transferTemplates = { 2: NodeTemplate( - "pi_cl_ram_copy_2d(get_ram_ptr(), ${ext}, ${loc}, ${transfer_size}, ${stride}, ${length}, ${ext2loc}, &${future});" + "pi_cl_ram_copy_2d(get_ram_ptr(), (uint32_t)${ext}, (void *)${loc}, (uint32_t)${transfer_size}, (uint32_t)${stride}, (uint32_t)${length}, ${ext2loc}, &${future});" ) } _waitingStrategy = PerTensorWaitingStrategy(GAP9L3DmaFuture) @@ -58,7 +57,3 @@ def transferOpRepr(self, externalBuffer: VariableBuffer, localBuffer: VariableBu "stride": strideExt[0], }) return operatorRepresentation - - -# Blocking adapter for L3 DMA (used in GAP9 L3 tiling) -gap9L3DmaHack = BlockingDmaFromAsyncDmaAdapter(GAP9L3Dma()) diff --git a/Deeploy/Targets/GAP9/Parsers.py b/Deeploy/Targets/GAP9/Parsers.py new file mode 100644 index 0000000000..4d730b7cae --- /dev/null +++ b/Deeploy/Targets/GAP9/Parsers.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Tuple + +import onnx_graphsurgeon as gs + +from Deeploy.DeeployTypes import NetworkContext +from Deeploy.Targets.Generic.Parsers import GEMMParser, RQSParserInterface + + +class NE16GEMMParser(GEMMParser, RQSParserInterface): + """Parser for NE16 RequantizedGemm nodes with 5 inputs [A, B, C, mul, scale_n].""" + + def __init__(self): + super().__init__(noBiasHoisting = True) + + def parseNode(self, node: gs.Node) -> bool: + ret_rqs = RQSParserInterface.parseNode(self, node) + ret_matmul = GEMMParser.parseNode(self, node) + ret = all([ret_rqs, ret_matmul, 'shift' in node.attrs, len(node.inputs) == 5]) + if ret: + self.operatorRepresentation['shift'] = int(node.attrs['shift'].values) + return ret + + def parseNodeCtxt(self, + ctxt: NetworkContext, + node: gs.Node, + channels_first: bool = True) -> Tuple[NetworkContext, bool]: + newCtxt, ret = GEMMParser.parseNodeCtxt(self, ctxt, node, channels_first) + if ret: + inputs = ['A', 'B', 'C', 'mul', 'scale_n'] + for idx, inputNode in enumerate(node.inputs): + self.operatorRepresentation[inputs[idx]] = newCtxt.lookup(inputNode.name).name + return newCtxt, True + return ctxt, False diff --git a/Deeploy/Targets/GAP9/Platform.py b/Deeploy/Targets/GAP9/Platform.py index bad6f8d859..c482c14dab 100644 --- a/Deeploy/Targets/GAP9/Platform.py +++ b/Deeploy/Targets/GAP9/Platform.py @@ -5,24 +5,29 @@ import numpy as np import onnx_graphsurgeon as gs +from Deeploy.CommonExtensions.OptimizationPasses.TopologyOptimizationPasses.LoweringOptimizationPasses import \ + RemoveEmptyConvBiasPass, RemoveOnlySingletonReduceMeanPass from Deeploy.DeeployTypes import ConstantBuffer, DeploymentEngine, DeploymentPlatform, NetworkContext, NodeMapper, \ - NodeTemplate, StructBuffer, TransientBuffer, VariableBuffer + NodeTemplate, StructBuffer, TopologyOptimizer, TransientBuffer, VariableBuffer from Deeploy.MemoryLevelExtension.MemoryLevels import MemoryHierarchy, MemoryLevel from Deeploy.MemoryLevelExtension.NetworkDeployers.MemoryLevelDeployer import MemoryPlatform, MemoryPlatformWrapper +from Deeploy.Targets.GAP9.Parsers import NE16GEMMParser from Deeploy.Targets.GAP9.Templates import AllocateTemplate, FreeTemplate # Import GAP9-specific tiler bindings -from Deeploy.Targets.GAP9.Tiler import GAP9AddTilingReadyBindings, GAP9ConcatTilingReadyBindings, \ - GAP9Conv2DTilingReadyBindings, GAP9DWConv2DTilingReadyBindings, GAP9FlattenTilingReadyBindings, \ - GAP9FPGELUTilingReadyBindings, GAP9FPGEMMTilingReadyBindings, GAP9GatherTilingReadyBindings, \ - GAP9iHardswishTilingReadyBindings, GAP9iRMSNormTilingReadyBindings, GAP9iRQSGELUTilingReadyBindings, \ - GAP9LayernormTilingReadyBindings, GAP9MatMulTilingReadyBindings, GAP9MaxPool2DTilingReadyBindings, \ - GAP9MulTilingReadyBindings, GAP9ReduceSumTilingReadyBindings, GAP9ReluTilingReadyBindings, \ +from Deeploy.Targets.GAP9.Tiler import DeQuantTilingReadyBindings, GAP9AddTilingReadyBindings, \ + GAP9ConcatTilingReadyBindings, GAP9Conv2DTilingReadyBindings, GAP9DWConv2DTilingReadyBindings, \ + GAP9FlattenTilingReadyBindings, GAP9FPGELUTilingReadyBindings, GAP9FPGEMMTilingReadyBindings, \ + GAP9GatherTilingReadyBindings, GAP9iHardswishTilingReadyBindings, GAP9iRMSNormTilingReadyBindings, \ + GAP9iRQSGELUTilingReadyBindings, GAP9LayernormTilingReadyBindings, GAP9MatMulTilingReadyBindings, \ + GAP9MaxPool2DTilingReadyBindings, GAP9MulTilingReadyBindings, GAP9NE16GEMMInt32TilingReadyBindings, \ + GAP9NE16RQSGEMMTilingReadyBindings, GAP9ReduceSumTilingReadyBindings, GAP9ReluTilingReadyBindings, \ GAP9RQAddTilingReadyBindings, GAP9RQSConv2DTilingReadyBindings, GAP9RQSDWConv2DTilingReadyBindings, \ GAP9RQSGEMMTilingReadyBindings, GAP9RQSiHardswishTilingReadyBindings, GAP9RQSMatrixVecTilingReadyBindings, \ GAP9RQSTallGEMMTilingReadyBindings, GAP9RQSTilingReadyBindings, GAP9SGDTilingReadyBindings, \ GAP9SoftmaxCrossEntropyGradTilingReadyBindings, GAP9SoftmaxCrossEntropyTilingReadyBindings, \ GAP9SoftmaxGradTilingReadyBindings, GAP9SoftmaxTilingReadyBindings, GAP9TransposeTilingReadyBindings, \ - GAP9UniformRQSTilingReadyBindings + GAP9UniformRQSTilingReadyBindings, QuantTilingReadyBindings +from Deeploy.Targets.GAP9.TopologyOptimizationPasses.Passes import NE16AdjustGEMMWeightLayoutPass from Deeploy.Targets.Generic.Bindings import BasicGEMMBindings, BasicPad1DBindings, BasicPad2DBindings, \ BasicRQIntegerDivBinding from Deeploy.Targets.Generic.Layers import AddLayer, ConcatLayer, ConvLayer, GatherLayer, GELULayer, GEMMLayer, \ @@ -37,12 +42,18 @@ SoftmaxCrossEntropyLossGradParser, SoftmaxCrossEntropyLossParser, SoftmaxGradParser, SoftmaxParser, \ TransposeParser, UniformRequantShiftParser, UnsqueezeParser, iHardswishParser, iRMSNormParser, iSoftmaxParser from Deeploy.Targets.Generic.Templates import AllocateTemplate as BasicAllocateTemplate -from Deeploy.Targets.PULPOpen.Bindings import BasicDequantBindings, BasicQuantBindings, PULPDMASliceBindings, \ - PULPDWConv1DBinding, PULPReduceMeanBindings, PULPRQSConv1DBindings, PULPSliceBindings +from Deeploy.Targets.Generic.TopologyOptimizationPasses.Passes import DequantPatternPass, DequantQuantMergePass, \ + IntegerDivRequantMergePass, MatMulAddMergePass, MergeConstAddAndRequantPass, MergeTrueIntegerDivRequantShiftPass, \ + QuantPatternPass, RQSSplitPass, SkipEmptyConcatPass, SkipUnityRequantPass, iGELURequantMergePass, \ + iHardswishRequantMergePass +from Deeploy.Targets.PULPOpen.Bindings import BasicDequantBindings, BasicQuantBindings, PULPConv1DBinding, \ + PULPDMASliceBindings, PULPDWConv1DBinding, PULPReduceMeanBindings, PULPRQSConv1DBindings, PULPSliceBindings from Deeploy.Targets.PULPOpen.Layers import PULPRQSConvLayer, PULPRQSGEMMLayer from Deeploy.Targets.PULPOpen.Parsers import PULPConv1DParser, PULPConv2DParser, PULPDWConv1DParser, \ PULPDWConv2DParser, PULPFPConv2DParser, PULPFPDWConv2DParser, PULPGEMMParser, PULPMatrixVecParser, \ PULPTallGEMMParser +from Deeploy.Targets.PULPOpen.TopologyOptimizationPasses.Passes import PULPConvRequantMergePass, \ + PULPGEMMRequantMergePass, PULPMatMulRequantMergePass # Create GAP9-specific NodeMappers GAP9_RQAddMapper = NodeMapper(RQAddParser(), GAP9RQAddTilingReadyBindings) @@ -90,9 +101,37 @@ GAP9_SoftmaxCrossEntropyLossGradMapper = NodeMapper(SoftmaxCrossEntropyLossGradParser(), GAP9SoftmaxCrossEntropyGradTilingReadyBindings) GAP9_SGDMapper = NodeMapper(SGDParser(), GAP9SGDTilingReadyBindings) -GAP9_QuantMapper = NodeMapper(QuantParser(), BasicQuantBindings) -GAP9_DequantMapper = NodeMapper(DequantParser(), BasicDequantBindings) +GAP9_QuantMapper = NodeMapper(QuantParser(), QuantTilingReadyBindings) +GAP9_DequantMapper = NodeMapper(DequantParser(), DeQuantTilingReadyBindings) GAP9_GEMMDequantMapper = NodeMapper(PULPGEMMParser(), BasicGEMMBindings) +GAP9_NE16GEMMMapper = NodeMapper(NE16GEMMParser(), GAP9NE16RQSGEMMTilingReadyBindings) +GAP9_NE16GEMMInt32Mapper = NodeMapper(GEMMParser(), GAP9NE16GEMMInt32TilingReadyBindings) + +GAP9Optimizer = TopologyOptimizer( + [ + QuantPatternPass(), + DequantPatternPass(), + DequantQuantMergePass(), + MatMulAddMergePass(), + SkipEmptyConcatPass(), + SkipUnityRequantPass(previous_op_regex = "Concat", num_inputs = 2), + SkipUnityRequantPass(previous_op_regex = "Reshape|Transpose", num_inputs = 1), + SkipUnityRequantPass(previous_op_regex = "Reshape|Transpose", num_inputs = 1), + RQSSplitPass(), + MergeTrueIntegerDivRequantShiftPass(), + IntegerDivRequantMergePass(), + iGELURequantMergePass(), + iHardswishRequantMergePass(), + PULPConvRequantMergePass(), + MergeConstAddAndRequantPass(), + PULPGEMMRequantMergePass(), + PULPMatMulRequantMergePass(), + # PULPAddRequantMergePass(), + RemoveEmptyConvBiasPass(), + RemoveOnlySingletonReduceMeanPass(), + NE16AdjustGEMMWeightLayoutPass(), + ], + name = "GAP9Optimizer") # GAP9-specific mapping using ClDma GAP9Mapping = { @@ -101,9 +140,9 @@ 'RequantizedConv': PULPRQSConvLayer([GAP9_Conv2DMapper, GAP9_DWConv2DMapper, GAP9_Conv1DMapper, GAP9_DWConv1DMapper]), 'RequantizedGemm': - PULPRQSGEMMLayer([GAP9_MatrixVecMapper, GAP9_TallGEMMMapper, GAP9_GEMMMapper]), + PULPRQSGEMMLayer([GAP9_NE16GEMMMapper, GAP9_MatrixVecMapper, GAP9_TallGEMMMapper, GAP9_GEMMMapper]), 'Gemm': - GEMMLayer([GAP9_FloatGEMMMapper, GAP9_GEMMDequantMapper]), + GEMMLayer([GAP9_NE16GEMMInt32Mapper, GAP9_FloatGEMMMapper, GAP9_GEMMDequantMapper]), 'Gelu': GELULayer([GAP9_GELUMapper]), 'LayerNormalization': @@ -244,7 +283,10 @@ class GAP9StructBuffer(StructBuffer): deallocTemplate = NodeTemplate("") -_includeList = ["pmsis.h", "DeeployGAP9Math.h", "pulp_nn_kernels.h", "DeeployMchan.h"] +_includeList = [ + "pmsis.h", "DeeployGAP9Math.h", "pulp_nn_kernels.h", "DeeployMchan.h", "CNN_BasicKernels_fp32.h", + "CNN_BasicKernels_NE16.h", "CNN_Copy.h", "ne16_utils.h" +] class GAP9ClusterEngine(DeploymentEngine): diff --git a/Deeploy/Targets/GAP9/Templates/GAP9SDKDequantQuantTemplate.py b/Deeploy/Targets/GAP9/Templates/GAP9SDKDequantQuantTemplate.py new file mode 100644 index 0000000000..cd4374466e --- /dev/null +++ b/Deeploy/Targets/GAP9/Templates/GAP9SDKDequantQuantTemplate.py @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 +# +# Quant/Dequant templates using GAP9 SDK kernels (CNN_Copy.c). +# All called via GAP9Transformer which handles pi_cl_team_fork. + +from Deeploy.DeeployTypes import NodeTemplate + +# ============================================================ +# Dequant templates: int → fp16 (SDK kernels from CNN_Copy.c) +# ============================================================ + +# int8 → fp16: SDK kernel CNN_FpsIEEE16 +fp16DequantI8Template = NodeTemplate(""" +// FP16 Dequant int8→fp16 (Name: ${nodeName}, Op: ${nodeOp}) +{ + signed char _dq_infos[8]; + *((float *)(_dq_infos + 0)) = (float)(-(${zero_point})); + *((float *)(_dq_infos + 4)) = (float)(${scale}); + CNN_Quantize_T _dq_arg = { + .In = (void *)${data_in}, + .Out = (void *)${data_out}, + .W = ${size}, + .H = 1, + .Infos = _dq_infos, + }; + CNN_FpsIEEE16(&_dq_arg); +} +""") + +# uint8 → fp16: SDK kernel CNN_UFpsIEEE16 +fp16DequantU8Template = NodeTemplate(""" +// FP16 Dequant uint8→fp16 (Name: ${nodeName}, Op: ${nodeOp}) +{ + signed char _dq_infos[8]; + *((float *)(_dq_infos + 0)) = (float)(-(${zero_point})); + *((float *)(_dq_infos + 4)) = (float)(${scale}); + CNN_Quantize_T _dq_arg = { + .In = (void *)${data_in}, + .Out = (void *)${data_out}, + .W = ${size}, + .H = 1, + .Infos = _dq_infos, + }; + CNN_UFpsIEEE16(&_dq_arg); +} +""") + +# ============================================================ +# Dequant templates: int → fp32 (SDK kernels from CNN_Copy.c) +# ============================================================ + +# int8 → fp32: SDK kernel CNN_FpsFloat32 +fp32DequantI8Template = NodeTemplate(""" +// FP32 Dequant int8→fp32 (Name: ${nodeName}, Op: ${nodeOp}) +{ + signed char _dq_infos[8]; + *((float *)(_dq_infos + 0)) = (float)(-(${zero_point})); + *((float *)(_dq_infos + 4)) = (float)(${scale}); + CNN_FpsFloat32_T _dq_arg = { + .In = (signed char *)${data_in}, + .Out = (float *)${data_out}, + .W = ${size}, + .H = 1, + .Infos = _dq_infos, + }; + CNN_FpsFloat32(&_dq_arg); +} +""") + +# uint8 → fp32: SDK kernel CNN_UFpsFloat32 +fp32DequantU8Template = NodeTemplate(""" +// FP32 Dequant uint8→fp32 (Name: ${nodeName}, Op: ${nodeOp}) +{ + signed char _dq_infos[8]; + *((float *)(_dq_infos + 0)) = (float)(-(${zero_point})); + *((float *)(_dq_infos + 4)) = (float)(${scale}); + CNN_UFpsFloat32_T _dq_arg = { + .In = (unsigned char *)${data_in}, + .Out = (float *)${data_out}, + .W = ${size}, + .H = 1, + .Infos = _dq_infos, + }; + CNN_UFpsFloat32(&_dq_arg); +} +""") + +# ============================================================ +# Quant templates: fp16 → int (SDK kernels from CNN_Copy.c) +# ============================================================ + +# fp16 → int8: SDK kernel CNN_IEEE16Fps +fp16QuantI8Template = NodeTemplate(""" +// FP16 Quant fp16→int8 (Name: ${nodeName}, Op: ${nodeOp}) +{ + signed char _q_infos[8]; + *((float *)(_q_infos + 0)) = (float)(${zero_point}); + *((float *)(_q_infos + 4)) = (float)(${scale}); + CNN_Quantize_T _q_arg = { + .In = (void *)${data_in}, + .Out = (void *)${data_out}, + .W = ${size}, + .H = 1, + .Infos = _q_infos, + }; + CNN_IEEE16Fps(&_q_arg); +} +""") + +# fp16 → uint8: SDK kernel CNN_IEEE16UFps +fp16QuantU8Template = NodeTemplate(""" +// FP16 Quant fp16→uint8 (Name: ${nodeName}, Op: ${nodeOp}) +{ + signed char _q_infos[8]; + *((float *)(_q_infos + 0)) = (float)(${zero_point}); + *((float *)(_q_infos + 4)) = (float)(${scale}); + CNN_Quantize_T _q_arg = { + .In = (void *)${data_in}, + .Out = (void *)${data_out}, + .W = ${size}, + .H = 1, + .Infos = _q_infos, + }; + CNN_IEEE16UFps(&_q_arg); +} +""") + +# ============================================================ +# Quant templates: fp32 → int (SDK kernels from CNN_Copy.c) +# ============================================================ + +# fp32 → int8: SDK kernel CNN_Float32Fps +fp32QuantI8Template = NodeTemplate(""" +// FP32 Quant fp32→int8 (Name: ${nodeName}, Op: ${nodeOp}) +{ + signed char _q_infos[8]; + *((float *)(_q_infos + 0)) = (float)(${zero_point}); + *((float *)(_q_infos + 4)) = (float)(${scale}); + CNN_Float32Fps_T _q_arg = { + .In = (float *)${data_in}, + .Out = (signed char *)${data_out}, + .W = ${size}, + .H = 1, + .Infos = _q_infos, + }; + CNN_Float32Fps(&_q_arg); +} +""") + +# fp32 → uint8: SDK kernel CNN_Float32UFps +fp32QuantU8Template = NodeTemplate(""" +// FP32 Quant fp32→uint8 (Name: ${nodeName}, Op: ${nodeOp}) +{ + signed char _q_infos[8]; + *((float *)(_q_infos + 0)) = (float)(${zero_point}); + *((float *)(_q_infos + 4)) = (float)(${scale}); + CNN_Float32UFps_T _q_arg = { + .In = (float *)${data_in}, + .Out = (unsigned char *)${data_out}, + .W = ${size}, + .H = 1, + .Infos = _q_infos, + }; + CNN_Float32UFps(&_q_arg); +} +""") diff --git a/Deeploy/Targets/GAP9/Templates/NE16GEMMTemplate.py b/Deeploy/Targets/GAP9/Templates/NE16GEMMTemplate.py new file mode 100644 index 0000000000..809d03d9c5 --- /dev/null +++ b/Deeploy/Targets/GAP9/Templates/NE16GEMMTemplate.py @@ -0,0 +1,242 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Dict, List, Tuple + +import numpy as np + +from Deeploy.DeeployTypes import NetworkContext, NodeTemplate, OperatorRepresentation + + +def _ne16_conv_1x1_weight_layout(W, w_bits = 8): + """Pack int8 weights for NE16 1x1 conv mode. + W: int8 [Ko, Ki] -> uint8 [Ko, Nb_KI, Qw, 2] (bitplane packed) + Weights stored as uint8 = int8 + 128. + """ + tp_in = 16 + Ko_, Ki_ = W.shape + W_uint8 = (W.astype(np.int32) + 128).astype(np.uint8) + nb_ki = (Ki_ + tp_in - 1) // tp_in + w_binary = np.zeros((Ko_ * nb_ki, w_bits, 8, tp_in // 8), dtype = np.uint8) + for ko in range(Ko_): + for ki_maj in range(nb_ki): + for ki_min in range(tp_in): + idx = ko * nb_ki + ki_maj + ki = ki_maj * tp_in + ki_min + val = int(W_uint8[ko, ki]) if ki < Ki_ else 0 + for q in range(w_bits): + w_binary[idx, q, ki_min % 8, ki_min // 8] = (val >> q) & 1 + space = np.logspace(0, 7, num = 8, base = 2, dtype = np.int32).reshape((8, 1)) + w_layout = np.sum(w_binary * space, axis = 2, dtype = np.uint8) + return w_layout.reshape((Ko_, nb_ki, w_bits, tp_in // 8)) + + +class NE16GEMMTemplate(NodeTemplate): + + def __init__(self, templateStr): + super().__init__(templateStr) + + def alignToContext(self, ctxt: NetworkContext, + operatorRepresentation: OperatorRepresentation) -> Tuple[NetworkContext, Dict, List[str]]: + + A = ctxt.lookup(operatorRepresentation['A']) + B = ctxt.lookup(operatorRepresentation['B']) + C = ctxt.lookup(operatorRepresentation['C']) + data_out = ctxt.lookup(operatorRepresentation['data_out']) + + # Determine signedness from type system (reliable, post-type-inference) + input_signed = A._type.referencedType.typeMin < 0 + output_bits = data_out._type.referencedType.typeWidth + output_signed = data_out._type.referencedType.typeMin < 0 + + operatorRepresentation['input_signed'] = input_signed + operatorRepresentation['output_bits'] = output_bits + operatorRepresentation['quant_bits'] = 2 if output_bits == 32 else 0 + operatorRepresentation['quant_norect'] = 1 if (output_bits == 32 or output_signed) else 0 + + # Weight packing and signed bias compensation + w_int8 = B.values.astype(np.int8) # [Ko, Ki], still int8 at this point + Ko, Ki = w_int8.shape + + # Truncate broadcast bias [M, O] → per-channel [Ko] for NE16 + bias_flat = C.values.flatten() + if bias_flat.size > Ko: + C.values = bias_flat[:Ko].copy() + + # Signed input bias compensation + if input_signed: + # Compute w_sum BEFORE packing (needed for signed bias compensation) + w_sum = w_int8.astype(np.int64).sum(axis = 1) # [Ko] + bias_values = C.values.flatten().astype(np.int64) + if 'mul' in operatorRepresentation: + # RequantizedGemm: bias -= 128 * w_sum * scale + scale_buf = ctxt.lookup(operatorRepresentation['mul']) + scale_values = scale_buf.values.flatten().astype(np.int64) + bias_values -= 128 * w_sum * scale_values + else: + # Gemm int32: bias -= 128 * w_sum (no scale) + bias_values -= 128 * w_sum + C.values = bias_values.astype(np.int32) + + # Pack weights to NE16 bitplane format + ne16_weights = _ne16_conv_1x1_weight_layout(w_int8) + B.values = ne16_weights.reshape(Ko, -1) + + return ctxt, operatorRepresentation, [] + + +# 8-bit output template (RequantizedGemm) — uses tiled ${mul} and ${scale_n} +referenceTemplate = NE16GEMMTemplate(""" +// NE16 Linear 8-bit (Name: ${nodeName}, Op: ${nodeOp}) + +% if input_signed: +// Signed input: add 128 offset to convert int8 -> uint8 (multi-core SIMD) +{ + ne16_int8_to_uint8_T _offset_arg = { + .In = (int8_t *)${A}, + .Out = (uint8_t *)${A}, + .size = ${batch} * ${M} * ${N} + }; + pi_cl_team_fork(NUM_CORES, (void *)ne16_int8_to_uint8, &_offset_arg); +} +% endif + +{ + unsigned int _ne16_cfg = 0; + _ne16_cfg |= ((8 - 1) & NE16_MASK_WBITS_M1) << NE16_SHIFT_WBITS_M1; + _ne16_cfg |= (0 & NE16_MASK_MODE16) << NE16_SHIFT_MODE16; + _ne16_cfg |= (1 & NE16_MASK_OUTQUANT) << NE16_SHIFT_OUTQUANT; + _ne16_cfg |= (NE16_FILTER_MODE_1x1 & NE16_MASK_FILTER_MODE) << NE16_SHIFT_FILTER_MODE; + _ne16_cfg |= (0 & NE16_MASK_LINEAR_MODE) << NE16_SHIFT_LINEAR_MODE; + _ne16_cfg |= (0 & NE16_MASK_STRIDED_MODE) << NE16_SHIFT_STRIDED_MODE; + _ne16_cfg |= (NE16_BITS_8BIT & NE16_MASK_NORM_BITS) << NE16_SHIFT_NORM_BITS; + _ne16_cfg |= (0 & NE16_MASK_STREAMIN) << NE16_SHIFT_STREAMIN; + _ne16_cfg |= (1 & NE16_MASK_WEIGHT_OFFSET_CFG) << NE16_SHIFT_WEIGHT_OFFSET_CFG; + _ne16_cfg |= (0 & NE16_MASK_QUANT_RIGHT_SHIFT) << NE16_SHIFT_QUANT_RIGHT_SHIFT; + _ne16_cfg |= (${quant_bits} & NE16_MASK_QUANT_BITS) << NE16_SHIFT_QUANT_BITS; + _ne16_cfg |= (${quant_norect} & NE16_MASK_QUANT_NORECT) << NE16_SHIFT_QUANT_NORECT; + _ne16_cfg |= (1 & NE16_MASK_NORM_SHIFT) << NE16_SHIFT_NORM_SHIFT; + _ne16_cfg |= (1 & NE16_MASK_NORM_BIAS) << NE16_SHIFT_NORM_BIAS; + + NE16_Enable(); + NE16_SoftReset(); + + KerConv_NE16_T _ne16_arg = { + .In = (void *)${A}, + .Filter = (unsigned short *)${B}, + .Bias = (int *)${C}, + .Out = (void *)${data_out}, + .Scale = (unsigned char *)${mul}, + .ScaleN = (unsigned char *)${scale_n}, + .Tile_InFeat = ${N}, + .TotalInFeatures = ${N}, + .Tile_InH = 1, + .Tile_InW = ${batch} * ${M}, + .Tile_OutFeat = ${O}, + .Tile_OutH = 1, + .Tile_OutW = ${batch} * ${M}, + .FilterSize = 1, + .Pad_Val = 0, + .Pad = (v4s){0, 0, 0, 0}, + .W_Offset = -128, + .Qw = 8, + .Mode16 = 0, + .FirstD0 = 1, + .LastD0 = 1, + .Default_NE16_Job_Cfg = _ne16_cfg, + .Fx = 1, + .Fy = 1, + .Sx = 1, + .Sy = 1, + .Dx = 1, + .Dy = 1, + .BuffOut = NULL, + .Infos = NULL, + .Extra = NULL, + }; + KerConv1x1_SmallHW_Stride1_NE16(&_ne16_arg); + + NE16_Disable(); +} +""") + +# Int32 output template (plain Gemm) — hardcoded scale=1, scale_n=0 +int32OutputTemplate = NE16GEMMTemplate(""" +// NE16 Linear Int32 (Name: ${nodeName}, Op: ${nodeOp}) + +% if input_signed: +// Signed input: add 128 offset to convert int8 -> uint8 (multi-core SIMD) +{ + ne16_int8_to_uint8_T _offset_arg = { + .In = (int8_t *)${A}, + .Out = (uint8_t *)${A}, + .size = ${batch} * ${M} * ${N} + }; + pi_cl_team_fork(NUM_CORES, (void *)ne16_int8_to_uint8, &_offset_arg); +} +% endif + +{ + unsigned char _ne16_ones[${O}]; + unsigned char _ne16_zeros[${O}]; + memset(_ne16_ones, 1, ${O}); + memset(_ne16_zeros, 0, ${O}); + + unsigned int _ne16_cfg = 0; + _ne16_cfg |= ((8 - 1) & NE16_MASK_WBITS_M1) << NE16_SHIFT_WBITS_M1; + _ne16_cfg |= (0 & NE16_MASK_MODE16) << NE16_SHIFT_MODE16; + _ne16_cfg |= (1 & NE16_MASK_OUTQUANT) << NE16_SHIFT_OUTQUANT; + _ne16_cfg |= (NE16_FILTER_MODE_1x1 & NE16_MASK_FILTER_MODE) << NE16_SHIFT_FILTER_MODE; + _ne16_cfg |= (0 & NE16_MASK_LINEAR_MODE) << NE16_SHIFT_LINEAR_MODE; + _ne16_cfg |= (0 & NE16_MASK_STRIDED_MODE) << NE16_SHIFT_STRIDED_MODE; + _ne16_cfg |= (NE16_BITS_8BIT & NE16_MASK_NORM_BITS) << NE16_SHIFT_NORM_BITS; + _ne16_cfg |= (0 & NE16_MASK_STREAMIN) << NE16_SHIFT_STREAMIN; + _ne16_cfg |= (1 & NE16_MASK_WEIGHT_OFFSET_CFG) << NE16_SHIFT_WEIGHT_OFFSET_CFG; + _ne16_cfg |= (0 & NE16_MASK_QUANT_RIGHT_SHIFT) << NE16_SHIFT_QUANT_RIGHT_SHIFT; + _ne16_cfg |= (${quant_bits} & NE16_MASK_QUANT_BITS) << NE16_SHIFT_QUANT_BITS; + _ne16_cfg |= (${quant_norect} & NE16_MASK_QUANT_NORECT) << NE16_SHIFT_QUANT_NORECT; + _ne16_cfg |= (1 & NE16_MASK_NORM_SHIFT) << NE16_SHIFT_NORM_SHIFT; + _ne16_cfg |= (1 & NE16_MASK_NORM_BIAS) << NE16_SHIFT_NORM_BIAS; + + NE16_Enable(); + NE16_SoftReset(); + + KerConv_NE16_T _ne16_arg = { + .In = (void *)${A}, + .Filter = (unsigned short *)${B}, + .Bias = (int *)${C}, + .Out = (void *)${data_out}, + .Scale = _ne16_ones, + .ScaleN = _ne16_zeros, + .Tile_InFeat = ${N}, + .TotalInFeatures = ${N}, + .Tile_InH = 1, + .Tile_InW = ${batch} * ${M}, + .Tile_OutFeat = ${O}, + .Tile_OutH = 1, + .Tile_OutW = ${batch} * ${M}, + .FilterSize = 1, + .Pad_Val = 0, + .Pad = (v4s){0, 0, 0, 0}, + .W_Offset = -128, + .Qw = 8, + .Mode16 = 0, + .FirstD0 = 1, + .LastD0 = 1, + .Default_NE16_Job_Cfg = _ne16_cfg, + .Fx = 1, + .Fy = 1, + .Sx = 1, + .Sy = 1, + .Dx = 1, + .Dy = 1, + .BuffOut = NULL, + .Infos = NULL, + .Extra = NULL, + }; + KerConv1x1_SmallHW_Stride1_NE16(&_ne16_arg); + + NE16_Disable(); +} +""") diff --git a/Deeploy/Targets/GAP9/TileConstraints/NE16GEMMTileConstraint.py b/Deeploy/Targets/GAP9/TileConstraints/NE16GEMMTileConstraint.py new file mode 100644 index 0000000000..6f490b9803 --- /dev/null +++ b/Deeploy/Targets/GAP9/TileConstraints/NE16GEMMTileConstraint.py @@ -0,0 +1,196 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +import math +from typing import Dict, List, Tuple + +from Deeploy.AbstractDataTypes import PointerClass +from Deeploy.CommonExtensions.DataTypes import uint8_t, uint16_t +from Deeploy.DeeployTypes import NetworkContext, OperatorRepresentation +from Deeploy.TilingExtension.MemoryConstraints import NodeMemoryConstraint +from Deeploy.TilingExtension.TileConstraint import TileConstraint +from Deeploy.TilingExtension.TilerModel import PerformanceHint, TilerModel +from Deeploy.TilingExtension.TilingCodegen import AbsoluteHyperRectangle, HyperRectangle, TilingSchedule, \ + VariableReplacementScheme + + +class NE16GEMMTileConstraint(TileConstraint): + """Tile constraint for NE16 GEMM with bitplane-packed weights stored as 2D [Ko, Ki].""" + + @staticmethod + def addGeometricalConstraint(tilerModel: TilerModel, parseDict: Dict, ctxt: NetworkContext) -> TilerModel: + + bufferA = ctxt.lookup(name = parseDict['A']) + bufferB = ctxt.lookup(name = parseDict['B']) + bufferC = ctxt.lookup(name = parseDict['C']) + outputBuffer = ctxt.lookup(name = parseDict['data_out']) + + bufferNames = [bufferA.name, bufferB.name, bufferC.name, outputBuffer.name] + hasMul = 'mul' in parseDict and isinstance(parseDict['mul'], str) + if hasMul: + mulBuffer = ctxt.lookup(name = parseDict['mul']) + bufferNames.append(mulBuffer.name) + hasScaleN = 'scale_n' in parseDict and isinstance(parseDict['scale_n'], str) + if hasScaleN: + scaleNBuffer = ctxt.lookup(name = parseDict['scale_n']) + bufferNames.append(scaleNBuffer.name) + + for bufferName in bufferNames: + tilerModel.addTensorDimToModel(ctxt, bufferName) + + dimOffsetA = len(bufferA.shape) - 2 + dimOffsetB = len(bufferB.shape) - 2 + dimOffsetOut = len(outputBuffer.shape) - 2 + + AFirstDimVar = tilerModel.getTensorDimVar(tensorName = bufferA.name, dimIdx = dimOffsetA + parseDict['transA']) + ASecondDimVar = tilerModel.getTensorDimVar(tensorName = bufferA.name, + dimIdx = dimOffsetA + 1 - parseDict['transA']) + BFirstDimVar = tilerModel.getTensorDimVar(tensorName = bufferB.name, dimIdx = dimOffsetB + parseDict['transB']) + BSecondDimVar = tilerModel.getTensorDimVar(tensorName = bufferB.name, + dimIdx = dimOffsetB + 1 - parseDict['transB']) + outputFirstDimVar = tilerModel.getTensorDimVar(tensorName = outputBuffer.name, dimIdx = dimOffsetOut) + outputSecondDimVar = tilerModel.getTensorDimVar(tensorName = outputBuffer.name, dimIdx = dimOffsetOut + 1) + + tilerModel.addConstraint(outputFirstDimVar == AFirstDimVar) + tilerModel.addConstraint(outputSecondDimVar == BSecondDimVar) + tilerModel.addConstraint(ASecondDimVar == BFirstDimVar) + + addDimVar = tilerModel.getTensorDimVar(tensorName = bufferC.name, dimIdx = 0) + tilerModel.addConstraint(outputSecondDimVar == addDimVar) + + if hasMul: + mulDimVar = tilerModel.getTensorDimVar(tensorName = mulBuffer.name, dimIdx = 0) + tilerModel.addConstraint(outputSecondDimVar == mulDimVar) + + if hasScaleN: + scaleNDimVar = tilerModel.getTensorDimVar(tensorName = scaleNBuffer.name, dimIdx = 0) + tilerModel.addConstraint(outputSecondDimVar == scaleNDimVar) + + return tilerModel + + @staticmethod + def addPolicyConstraint(tilerModel: TilerModel, parseDict: Dict, ctxt: NetworkContext) -> TilerModel: + + bufferA = ctxt.lookup(name = parseDict['A']) + bufferB = ctxt.lookup(name = parseDict['B']) + + dimOffsetA = len(bufferA.shape) - 2 + dimOffsetB = len(bufferB.shape) - 2 + + # Don't tile N (reduction dimension) — NE16 needs full input channels + ASecondDimVar = tilerModel.getTensorDimVar(tensorName = bufferA.name, + dimIdx = dimOffsetA + 1 - parseDict['transA']) + BFirstDimVar = tilerModel.getTensorDimVar(tensorName = bufferB.name, dimIdx = dimOffsetB + parseDict['transB']) + tilerModel.addConstraint(ASecondDimVar == parseDict['N']) + tilerModel.addConstraint(BFirstDimVar == parseDict['N']) + + # O (output channels) should be divisible by 32 (NE16 TP_OUT) + BSecondDimVar = tilerModel.getTensorDimVar(tensorName = bufferB.name, + dimIdx = dimOffsetB + 1 - parseDict['transB']) + if parseDict["O"] > 32: + tilerModel.addTileSizeDivisibleConstraint(parseDict, + 'O', + BSecondDimVar, + 32, + strategy = PerformanceHint(priority = 1)) + + return tilerModel + + @classmethod + def serializeTilingSolution( + cls, tilingSolution: NodeMemoryConstraint, absoluteOutputCubes: List[AbsoluteHyperRectangle], + targetMemLevel: str, ctxt: NetworkContext, + operatorRepresentation: OperatorRepresentation) -> Tuple[VariableReplacementScheme, TilingSchedule]: + outputCubes = [cube.rectangle for cube in absoluteOutputCubes] + + hasMul = 'mul' in operatorRepresentation and isinstance(operatorRepresentation['mul'], str) + hasScaleN = 'scale_n' in operatorRepresentation and isinstance(operatorRepresentation['scale_n'], str) + addrNames = ['A', 'B', 'C', 'data_out'] + if hasMul: + addrNames.insert(2, 'mul') + if hasScaleN: + addrNames.insert(-1, 'scale_n') + inputBaseOffsets, outputBaseOffsets = cls.extractBaseAddr(tilingSolution, targetMemLevel, + operatorRepresentation, addrNames) + transA = operatorRepresentation['transA'] + transB = operatorRepresentation['transB'] + + buffA = ctxt.lookup(operatorRepresentation['A']) + buffB = ctxt.lookup(operatorRepresentation['B']) + + NSize = buffA.shape[-1] + + inputACubes = [] + inputBCubes = [] + inputMulCubes = [] + inputAddCubes = [] + + replacements = {"M": [], "O": [], "batch": []} + + for cube in outputCubes: + MOffset, OOffset = cube.offset[-2:] + MSize, OSize = cube.dims[-2:] + + if len(cube.offset) > 2: + BatchSize = math.prod(cube.dims[:-2]) + else: + BatchSize = 1 + + replacements["M"].append(MSize) + replacements["O"].append(OSize) + replacements["batch"].append(BatchSize) + + if transA == 0: + AMatrixOffsets = (MOffset, 0) + AMatrixShape = (MSize, NSize) + else: + AMatrixOffsets = (0, MOffset) + AMatrixShape = (NSize, MSize) + + if len(buffA.shape) > 2: + batchDimCount = len(buffA.shape) - 2 + AMatrixOffsets = tuple(cube.offset[:-2][-batchDimCount:]) + AMatrixOffsets + AMatrixShape = tuple(cube.dims[:-2][-batchDimCount:]) + AMatrixShape + + inputACubes.append(HyperRectangle(AMatrixOffsets, AMatrixShape)) + + if transB == 0: + BMatrixOffsets = (0, OOffset) + BMatrixShape = (NSize, OSize) + else: + BMatrixOffsets = (OOffset, 0) + BMatrixShape = (OSize, NSize) + + inputBCubes.append(HyperRectangle(BMatrixOffsets, BMatrixShape)) + + RequantCube = HyperRectangle((OOffset,), (OSize,)) + inputMulCubes.append(RequantCube) + inputAddCubes.append(RequantCube) + + replacements["N"] = [NSize] * len(outputCubes) + + replacementTypes = { + "M": PointerClass(uint16_t), + "N": PointerClass(uint16_t), + "O": PointerClass(uint16_t), + "batch": PointerClass(uint8_t) + } + + inputLoadSchedule = [] + outputLoadSchedule = [] + + for idx, (a, b, c) in enumerate(zip(inputACubes, inputBCubes, inputAddCubes)): + load = {"A": a, "B": b, "C": c} + if hasMul: + load["mul"] = inputMulCubes[idx] + if hasScaleN: + load["scale_n"] = inputMulCubes[idx] # same per-channel slice as mul/C + inputLoadSchedule.append(load) + + for out in outputCubes: + outputLoadSchedule.append({"data_out": out}) + + schedule = TilingSchedule(inputBaseOffsets, outputBaseOffsets, inputLoadSchedule, outputLoadSchedule) + + return VariableReplacementScheme(replacements, replacementTypes), schedule diff --git a/Deeploy/Targets/GAP9/Tiler.py b/Deeploy/Targets/GAP9/Tiler.py index fefe12b6d7..b93aacb9db 100644 --- a/Deeploy/Targets/GAP9/Tiler.py +++ b/Deeploy/Targets/GAP9/Tiler.py @@ -10,14 +10,16 @@ import copy -from Deeploy.Targets.GAP9.Bindings import GAP9AddBindings, GAP9ConcatBindings, GAP9FloatConv2DBindings, \ - GAP9FloatDWConv2DBindings, GAP9FloatGELUBinding, GAP9FloatGEMMBindings, GAP9GatherBindings, \ - GAP9iHardswishBindings, GAP9iRMSNormBindings, GAP9iRQSGELUBindings, GAP9LayernormBinding, GAP9MatMulBindings, \ - GAP9MaxPool2DBindings, GAP9MulBindings, GAP9ReduceSumBindings, GAP9ReluBinding, GAP9ReshapeBindings, \ - GAP9RQAddBindings, GAP9RQSBindings, GAP9RQSConv2DBindings, GAP9RQSDWConv2DBindings, GAP9RQSGEMMBindings, \ - GAP9RQSiHardswishBindings, GAP9RQSMatrixVecBindings, GAP9RQSTallGEMMBindings, GAP9SGDBindings, \ - GAP9SoftmaxBindings, GAP9SoftmaxCrossEntropyLossBindings, GAP9SoftmaxCrossEntropyLossGradBindings, \ - GAP9SoftmaxGradBindings, GAP9TransposeBindings, GAP9UniformRQSBindings +from Deeploy.Targets.GAP9.Bindings import GAP9AddBindings, GAP9ConcatBindings, GAP9DequantBindings, \ + GAP9FloatConv2DBindings, GAP9FloatDWConv2DBindings, GAP9FloatGELUBinding, GAP9FloatGEMMBindings, \ + GAP9GatherBindings, GAP9iHardswishBindings, GAP9iRMSNormBindings, GAP9iRQSGELUBindings, GAP9LayernormBinding, \ + GAP9MatMulBindings, GAP9MaxPool2DBindings, GAP9MulBindings, GAP9NE16GEMMInt32Bindings, GAP9NE16RQSGEMMBindings, \ + GAP9QuantBindings, GAP9ReduceSumBindings, GAP9ReluBinding, GAP9ReshapeBindings, GAP9RQAddBindings, \ + GAP9RQSBindings, GAP9RQSConv2DBindings, GAP9RQSDWConv2DBindings, GAP9RQSGEMMBindings, GAP9RQSiHardswishBindings, \ + GAP9RQSMatrixVecBindings, GAP9RQSTallGEMMBindings, GAP9SGDBindings, GAP9SoftmaxBindings, \ + GAP9SoftmaxCrossEntropyLossBindings, GAP9SoftmaxCrossEntropyLossGradBindings, GAP9SoftmaxGradBindings, \ + GAP9TransposeBindings, GAP9UniformRQSBindings +from Deeploy.Targets.GAP9.TileConstraints.NE16GEMMTileConstraint import NE16GEMMTileConstraint from Deeploy.Targets.Generic.TileConstraints.AddTileConstraint import AddTileConstraint from Deeploy.Targets.Generic.TileConstraints.ConcatTileConstraint import ConcatTileConstraint from Deeploy.Targets.Generic.TileConstraints.iHardswishTileConstraint import iHardswishTileConstraint @@ -60,6 +62,12 @@ GAP9RQSGEMMTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = GAP9RQSGEMMBindings, tileConstraint = GEMMTileConstraint()) +GAP9NE16RQSGEMMTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = GAP9NE16RQSGEMMBindings, + tileConstraint = NE16GEMMTileConstraint()) + +GAP9NE16GEMMInt32TilingReadyBindings = TilingReadyNodeBindings(nodeBindings = GAP9NE16GEMMInt32Bindings, + tileConstraint = NE16GEMMTileConstraint()) + GAP9FPGEMMTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = GAP9FloatGEMMBindings, tileConstraint = FloatGEMMTileConstraint()) @@ -142,3 +150,9 @@ GAP9SGDTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = GAP9SGDBindings, tileConstraint = SGDTileConstraint()) + +QuantTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = GAP9QuantBindings, + tileConstraint = UnaryTileConstraint()) + +DeQuantTilingReadyBindings = TilingReadyNodeBindings(nodeBindings = GAP9DequantBindings, + tileConstraint = UnaryTileConstraint()) diff --git a/Deeploy/Targets/GAP9/TopologyOptimizationPasses/Passes.py b/Deeploy/Targets/GAP9/TopologyOptimizationPasses/Passes.py new file mode 100644 index 0000000000..d5c8df4f9a --- /dev/null +++ b/Deeploy/Targets/GAP9/TopologyOptimizationPasses/Passes.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 + +import math + +import numpy as np +import onnx_graphsurgeon as gs + +from Deeploy.CommonExtensions.OptimizationPasses.Matchers import Match, NonBranchingMatcher +from Deeploy.CommonExtensions.OptimizationPasses.PassClasses import ReplaceSequentialPatternPass, contextagnostic + + +def _compute_ne16_scale_shift(mul_values, log2D): + """Convert Deeploy's mul/log2D to NE16's per-channel scale/scale_n.""" + Ko = len(mul_values) + ne16_scale = np.zeros(Ko, dtype = np.uint8) + ne16_scale_n = np.zeros(Ko, dtype = np.uint8) + for ko in range(Ko): + sf = float(mul_values[ko]) / float(2**log2D) + if sf >= 1.0: + sn = 0 + sc = min(255, max(1, int(round(sf)))) + elif sf > 0: + sn = min(31, max(0, int(math.floor(math.log2(127.0 / sf))))) + sc = min(255, max(1, int(round(sf * (1 << sn))))) + else: + sn = 0 + sc = 0 + ne16_scale[ko] = sc + ne16_scale_n[ko] = sn + return ne16_scale, ne16_scale_n + + +def _ne16_adjust_gemm_weight_layout_fun(graph: gs.Graph, match: Match, name: str): + """Prepare GEMM node for NE16 execution. + + Handles transB normalization, scale/scale_n computation, and bias rescaling. + Weight bitplane packing and signed bias compensation are deferred to alignToContext + where input signedness is known from the type system. + """ + matched_nodes = list(match.nodes_map.values()) + node = matched_nodes[0] + + # Weight is input[1] for both Gemm and RequantizedGemm + weightTensor = node.inputs[1] + + if not isinstance(weightTensor, gs.Constant): + return graph + + values = weightTensor.values + + # Skip true float weights (Deeploy stores int8 weights as float32) + if not np.array_equal(values, np.round(values)): + return graph + + # Check shape is 2D + if len(values.shape) != 2: + return graph + + # Determine actual Ko, Ki based on transB + transB = node.attrs.get('transB', 0) + if transB: + Ko, Ki = values.shape + else: + Ki, Ko = values.shape + + # Check NE16 compatibility BEFORE modifying the node + if Ki % 16 != 0: + return graph + + # Transpose weight to [Ko, Ki] if needed — keep as int8 + if not transB: + transposed = values.T.astype(np.int8) + newWeightTensor = gs.Constant(f"{name}_{weightTensor.name}", transposed) + node.inputs[1] = newWeightTensor + node.attrs['transB'] = 1 + + # For RequantizedGemm: transform mul → ne16_scale, create scale_n, rescale bias + if node.op == 'RequantizedGemm' and len(node.inputs) >= 4: + mulTensor = node.inputs[3] + biasTensor = node.inputs[2] + + if isinstance(mulTensor, gs.Constant) and isinstance(biasTensor, gs.Constant): + mul_values = mulTensor.values.flatten().astype(np.int32) + log2D = int(np.log2(node.attrs['div'].values)) + + # Broadcast scalar mul to per-channel if needed + if len(mul_values) == 1: + mul_values = np.full(Ko, mul_values[0], dtype = np.int32) + + ne16_scale, ne16_scale_n = _compute_ne16_scale_shift(mul_values, log2D) + + # Rescale bias from mul/log2D domain to scale/scale_n domain + # bias_merged is already *= mul from PULPGEMMRequantMergePass + # NE16 needs: bias_ne16 = bias_merged * 2^(scale_n - log2D) + bias_values = biasTensor.values.flatten().astype(np.int64) + ne16_bias = np.zeros(Ko, dtype = np.int64) + for ko in range(Ko): + shift_diff = int(ne16_scale_n[ko]) - log2D + if shift_diff >= 0: + ne16_bias[ko] = bias_values[ko] << shift_diff + else: + ne16_bias[ko] = bias_values[ko] >> (-shift_diff) + + ne16_bias = ne16_bias.astype(np.int32) + + # Overwrite mul tensor with ne16_scale + mulTensor.values = ne16_scale + + # Overwrite bias tensor + biasTensor.values = ne16_bias + + # Append scale_n as new input[4] + scale_n_tensor = gs.Constant(f"{name}_scale_n", ne16_scale_n) + node.inputs.append(scale_n_tensor) + + return graph + + +@contextagnostic +class NE16AdjustGEMMWeightLayoutPass(ReplaceSequentialPatternPass): + + def __init__(self): + graph = gs.Graph() + _input = gs.Variable(name = 'input_1') + output = graph.layer(inputs = [_input], outputs = ['out'], op = 'RequantizedGemm|Gemm', name = 'node') + graph.outputs.append(output) + graph.inputs.append(_input) + + super().__init__(graph, _ne16_adjust_gemm_weight_layout_fun, "_NE16_ADJUST_GEMM_WEIGHT_LAYOUT_PASS", + NonBranchingMatcher(regex_op = True)) diff --git a/Deeploy/Targets/GAP9/TopologyOptimizationPasses/__init__.py b/Deeploy/Targets/GAP9/TopologyOptimizationPasses/__init__.py new file mode 100644 index 0000000000..4694b67df5 --- /dev/null +++ b/Deeploy/Targets/GAP9/TopologyOptimizationPasses/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna +# +# SPDX-License-Identifier: Apache-2.0 diff --git a/Deeploy/Targets/Generic/TopologyOptimizationPasses/Passes.py b/Deeploy/Targets/Generic/TopologyOptimizationPasses/Passes.py index 146bcf699e..e506db78ce 100644 --- a/Deeploy/Targets/Generic/TopologyOptimizationPasses/Passes.py +++ b/Deeploy/Targets/Generic/TopologyOptimizationPasses/Passes.py @@ -1177,3 +1177,85 @@ def __init__(self): name = "_RECOGNIZE_DEQUANT_PASS" super().__init__(graph, _recognize_dequant_fun, name) + + +def _merge_dequant_quant_fun(graph: gs.Graph, match: Match, name: str): + matched_nodes = [m for k, m in match.nodes_map.items()] + dequant_node = matched_nodes[0] + quant_node = matched_nodes[1] + + # Skip if dequant output has multiple consumers or is a graph output + dequant_out = dequant_node.outputs[0] + if len(dequant_out.outputs) > 1 or dequant_out in graph.outputs: + return graph + + # Extract Dequant parameters (stored as Python floats) + s_d = float(dequant_node.attrs['scale']) + zp_d = float(dequant_node.attrs['zero_point']) + + # Extract Quant parameters (stored as numpy arrays) + s_q = float(np.array(quant_node.attrs['scale']).item()) + zp_q = float(np.array(quant_node.attrs['zero_point']).item()) + + signed_val = int(np.array(quant_node.attrs['signed']).item()) if 'signed' in quant_node.attrs else 1 + signed = bool(signed_val) + bit_width = int(np.array(quant_node.attrs['bit_width']).item()) if 'bit_width' in quant_node.attrs else 8 + n_levels = 2**bit_width + + # Compute effective ratio: y_float = (x_int - zp_d) * s_d * s_q + zp_q + ratio = s_d * s_q + + # Identity case: ratio ~= 1.0, both zero points == 0 + EPSILON = 1e-6 + if abs(ratio - 1.0) < EPSILON and abs(zp_d) < EPSILON and abs(zp_q) < EPSILON and signed: + input_tensor = dequant_node.inputs[0] + output_tensor = quant_node.outputs[0] + for downstream_node in list(output_tensor.outputs): + for i, inp in enumerate(downstream_node.inputs): + if inp == output_tensor: + downstream_node.inputs[i] = input_tensor + dequant_node.inputs.clear() + dequant_node.outputs.clear() + quant_node.inputs.clear() + quant_node.outputs.clear() + graph.cleanup().toposort() + return graph + + # Requantization case: convert to RequantShift + shift = 16 + div_val = 2**shift + + mul_val = int(np.round(ratio * div_val)) + add_val = int(np.round((-zp_d * ratio + zp_q) * div_val)) + + mul_const = gs.Constant(name = f'{name}_mul', values = np.array([mul_val], dtype = np.int32)) + add_const = gs.Constant(name = f'{name}_add', values = np.array([add_val], dtype = np.int32)) + + rqs_attrs = { + 'div': gs.Constant(f'{name}_div', np.array(div_val)), + 'n_levels_out': gs.Constant(f'{name}_n_levels', np.array(n_levels)), + 'signed': gs.Constant(f'{name}_signed', np.array([signed_val])), + } + + _inputs = [dequant_node.inputs[0], mul_const, add_const] + _outputs = quant_node.outputs + + rqs_node = gs.Node(op = 'RequantShift', name = name, attrs = rqs_attrs) + graph.replaceInsertNode(_inputs, _outputs, rqs_node) + + return graph + + +@contextagnostic +class DequantQuantMergePass(ReplaceSequentialPatternPass): + + def __init__(self): + graph = gs.Graph() + _input = gs.Variable(name = 'input_1') + output = graph.layer(inputs = [_input], outputs = ['dequant_out'], op = 'Dequant', name = 'dequant') + output = graph.layer(inputs = output, outputs = ['quant_out'], op = 'Quant', name = 'quant') + graph.outputs.append(output) + graph.inputs.append(_input) + + name = "_MERGE_DEQUANT_QUANT_PASS" + super().__init__(graph, _merge_dequant_quant_fun, name) diff --git a/Deeploy/Targets/Generic/TypeCheckers.py b/Deeploy/Targets/Generic/TypeCheckers.py index c2c8d436f8..f1174ffebd 100644 --- a/Deeploy/Targets/Generic/TypeCheckers.py +++ b/Deeploy/Targets/Generic/TypeCheckers.py @@ -543,6 +543,15 @@ class QuantChecker(SignPropTypeChecker): def __init__(self, input_types: Sequence[Type[Pointer]], output_types: Sequence[Type[Pointer]]): super().__init__(input_types, output_types) + def checkOutputType(self, inputs: List[VariableBuffer], operatorRepresentation: OperatorRepresentation) -> bool: + outputTypeSigned = self.output_types[0].referencedType.typeMin < 0 + opSigned = bool(operatorRepresentation['signed']) + if opSigned and outputTypeSigned: + return True + if (not opSigned) and (not outputTypeSigned): + return True + return False + def _inferNumLevels(self, inputs: List[VariableBuffer], operatorRepresentation: OperatorRepresentation) -> List[int]: # Calculate number of levels based on bit_width diff --git a/DeeployTest/Platforms/GAP9/CMakeLists.txt b/DeeployTest/Platforms/GAP9/CMakeLists.txt index cbb6382329..c723bb02e9 100644 --- a/DeeployTest/Platforms/GAP9/CMakeLists.txt +++ b/DeeployTest/Platforms/GAP9/CMakeLists.txt @@ -32,6 +32,7 @@ target_compile_options(network PRIVATE -Wno-pointer-sign -Wno-unknown-pragmas -Wno-error + -O3 ) target_link_options(${ProjectId} PRIVATE diff --git a/DeeployTest/testUtils/platformMapping.py b/DeeployTest/testUtils/platformMapping.py index 4dc2bbf824..11ce25fbc6 100644 --- a/DeeployTest/testUtils/platformMapping.py +++ b/DeeployTest/testUtils/platformMapping.py @@ -15,7 +15,7 @@ from Deeploy.Targets.CortexM.Deployer import CMSISDeployer from Deeploy.Targets.CortexM.Platform import CMSISOptimizer, CMSISPlatform from Deeploy.Targets.GAP9.Deployer import GAP9Deployer -from Deeploy.Targets.GAP9.Platform import GAP9Platform, MemoryGAP9Platform, MemoryGAP9PlatformWrapper +from Deeploy.Targets.GAP9.Platform import GAP9Optimizer, GAP9Platform, MemoryGAP9Platform, MemoryGAP9PlatformWrapper from Deeploy.Targets.Generic.Deployer import GenericDeployer from Deeploy.Targets.Generic.Platform import GenericOptimizer, GenericPlatform from Deeploy.Targets.MemPool.Deployer import MemPoolDeployer @@ -234,7 +234,7 @@ def mapDeployer(platform: DeploymentPlatform, elif isinstance(platform, (GAP9Platform, MemoryGAP9Platform, MemoryGAP9PlatformWrapper)): if loweringOptimizer is None: - loweringOptimizer = PULPOptimizer + loweringOptimizer = GAP9Optimizer if default_channels_first is None: default_channels_first = False diff --git a/TargetLibraries/GAP9/CMakeLists.txt b/TargetLibraries/GAP9/CMakeLists.txt index 8051484dd0..8de757a61e 100644 --- a/TargetLibraries/GAP9/CMakeLists.txt +++ b/TargetLibraries/GAP9/CMakeLists.txt @@ -4,22 +4,61 @@ file(GLOB_RECURSE SOURCES "src/**" + "$ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries_fp32/CNN_Bias_Linear_Activation_fp32.c" + "$ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries_NE16/CNN_BasicKernels_NE16.c" + "$ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries/CNN_Copy.c" ) + +# Exclude dory_mem and dory_dma from SOURCES (they need different optimization) +list(FILTER SOURCES EXCLUDE REGEX ".*dory_(mem|dma).*") + # RW: Include PULPOpen sources but exclude dory_mem related files file(GLOB_RECURSE PULPOPEN_SOURCES "../PULPOpen/src/**") list(FILTER PULPOPEN_SOURCES EXCLUDE REGEX ".*dory_mem.*") list(APPEND SOURCES ${PULPOPEN_SOURCES}) +# Separate dory library compiled without -O3 +add_library(dory_lib STATIC + ${CMAKE_CURRENT_LIST_DIR}/src/dory_mem.c + ${CMAKE_CURRENT_LIST_DIR}/src/dory_dma.c +) +target_include_directories(dory_lib PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/inc + ${CMAKE_CURRENT_LIST_DIR}/../PULPOpen/inc +) +target_compile_options(dory_lib PRIVATE + -Wno-implicit-function-declaration + -Wno-sign-conversion + -Wno-sign-compare + -Wno-type-limits + -Wno-attributes + -Wno-incompatible-pointer-types + -Og +) +target_compile_definitions(dory_lib PUBLIC NUM_CORES=${NUM_CORES}) +target_link_libraries(dory_lib PUBLIC pmsis) + add_deeploy_library(deeploygap9 STATIC ${SOURCES}) target_include_directories(deeploygap9 PUBLIC ${CMAKE_CURRENT_LIST_DIR}/inc ${CMAKE_CURRENT_LIST_DIR}/../PULPOpen/inc + ${TILER_INC} + ${TILER_EMU_INC} + ${TILER_CNN_KERNEL_PATH_FP32} + ${TILER_CNN_KERNEL_PATH_FP16} + $ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries_NE16 + $ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries_SQ8 + $ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries + ${TILER_DSP_KERNEL_V2_PATH} + ${TILER_DSP_KERNEL_V2_PATH}/FastMathFunctions ) + target_compile_options(deeploygap9 PUBLIC -DNUM_CORES=${NUM_CORES} + -DSTD_FLOAT ) target_compile_options(deeploygap9 PRIVATE @@ -27,10 +66,10 @@ target_compile_options(deeploygap9 PRIVATE -Wno-sign-compare -Wno-type-limits -Wno-attributes + -Wno-incompatible-pointer-types + -O3 ) -target_link_libraries(deeploygap9 PUBLIC pmsis) - #RW: Link PULP-NN #RW: Set PULP-NN version and bitwidth for pulp-nn-mixed set(PULPNNVERSION XPULPV2) @@ -96,5 +135,6 @@ if(platform STREQUAL "GAP9_w_NE16") target_link_libraries(deeploygap9 PUBLIC pulp-nnx) endif() +target_link_libraries(deeploygap9 PUBLIC pmsis) target_link_libraries(deeploygap9 PUBLIC m) - +target_link_libraries(deeploygap9 PUBLIC dory_lib) diff --git a/TargetLibraries/GAP9/inc/ne16_utils.h b/TargetLibraries/GAP9/inc/ne16_utils.h new file mode 100644 index 0000000000..4d041c75dc --- /dev/null +++ b/TargetLibraries/GAP9/inc/ne16_utils.h @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna + * SPDX-License-Identifier: Apache-2.0 + * + * NE16 utility kernels for GAP9 + */ + +#ifndef __NE16_UTILS_GAP9__ +#define __NE16_UTILS_GAP9__ + +#include "CNN_BasicKernels_fp32.h" +#include "pmsis.h" + +typedef struct { + int8_t *In; + uint8_t *Out; + int size; +} ne16_int8_to_uint8_T; + +/* Multi-core SIMD int8 → uint8 conversion (+128 offset) */ +void ne16_int8_to_uint8(ne16_int8_to_uint8_T *Arg); + +#endif diff --git a/TargetLibraries/GAP9/src/ne16_utils.c b/TargetLibraries/GAP9/src/ne16_utils.c new file mode 100644 index 0000000000..c19c119b25 --- /dev/null +++ b/TargetLibraries/GAP9/src/ne16_utils.c @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2025 ETH Zurich and University of Bologna + * SPDX-License-Identifier: Apache-2.0 + * + * NE16 utility kernels for GAP9 + */ + +#include "ne16_utils.h" + +void ne16_int8_to_uint8(ne16_int8_to_uint8_T *Arg) { + int8_t *In = Arg->In; + uint8_t *Out = Arg->Out; + int size = Arg->size; + + unsigned int CoreId = gap_coreid(); + unsigned int NCore = gap_ncore(); + unsigned int total_quads = size / 4; + unsigned int Chunk = (total_quads + NCore - 1) / NCore; + unsigned int First = Chunk * CoreId; + unsigned int Last = First + Chunk; + if (Last > total_quads) + Last = total_quads; + + v4s offset = {-128, -128, -128, -128}; + for (unsigned int q = First; q < Last; q++) { + *((v4s *)&Out[q * 4]) = *((v4s *)&In[q * 4]) + offset; + } + + /* Handle remaining elements (size not multiple of 4) */ + if (CoreId == 0) { + for (int i = total_quads * 4; i < size; i++) { + Out[i] = (uint8_t)((int32_t)In[i] + 128); + } + } +} From 6c8ae2babe13aab705646b853ae8ba821ddcf4bc Mon Sep 17 00:00:00 2001 From: runwangdl Date: Tue, 14 Apr 2026 10:29:54 +0000 Subject: [PATCH 03/14] [NE16] integrate Pu DENG's NE16 Linear PR with NE16-w platform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TargetLibraries/GAP9/CMakeLists.txt: rename CNN_Libraries_NE16 → CNN_Libraries_HWPE (the actual gap9-sdk path); skip SDK CNN_BasicKernels_NE16.c source for GAP9_w_NE16 platform (it uses the pulp-nnx ne16 stack, so the SDK NE16 kernels are not needed). - Deeploy/Targets/NE16/Platform.py: instantiate the GAP9ClusterEngine with a trimmed includeList (no CNN_BasicKernels_NE16.h / ne16_utils.h / CNN_Copy.h) so the generated Network.c does not pull in the SDK NE16 header alongside pulp-nnx ne16_task_defs.h — the NE16_REG_* macros are defined in both and trigger -Werror redefs. --- .../ci-platform-gap9-w-ne16-tiled.yml | 10 ++++++-- Deeploy/Targets/GAP9/Platform.py | 4 ++-- Deeploy/Targets/NE16/Platform.py | 24 +++++++++++++++++-- TargetLibraries/GAP9/CMakeLists.txt | 11 +++++++-- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-platform-gap9-w-ne16-tiled.yml b/.github/workflows/ci-platform-gap9-w-ne16-tiled.yml index 5f45bbafeb..c7df408ffd 100644 --- a/.github/workflows/ci-platform-gap9-w-ne16-tiled.yml +++ b/.github/workflows/ci-platform-gap9-w-ne16-tiled.yml @@ -17,7 +17,7 @@ name: CI • GAP9 + NE16 (Tiled) docker_image_deeploy: description: "Deeploy Image to use" required: false - default: "ghcr.io/pulp-platform/deeploy-gap9:latest" + default: "ghcr.io/pulp-platform/deeploy-gap9:devel" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -25,9 +25,15 @@ concurrency: jobs: select-env: + # The GAP9 + NE16 image is hosted in pulp-platform's private ghcr.io + # registry; only upstream's self-hosted runners have credentials to + # pull it. On forks the docker pull always returns "denied", so skip + # the whole pipeline cleanly there. (Same constraint as the existing + # ci-platform-gap9{,-tiled}.yml jobs.) + if: github.repository == 'pulp-platform/Deeploy' uses: ./.github/workflows/_select-env.yml with: - docker_image_deeploy: ${{ github.event.inputs.docker_image_deeploy || github.repository == 'pulp-platform/Deeploy' && 'ghcr.io/pulp-platform/deeploy-gap9:latest'}} + docker_image_deeploy: ${{ github.event.inputs.docker_image_deeploy || 'ghcr.io/pulp-platform/deeploy-gap9:devel' }} gap9-w-ne16-kernels-tiled-singlebuffer-L2: needs: select-env diff --git a/Deeploy/Targets/GAP9/Platform.py b/Deeploy/Targets/GAP9/Platform.py index c482c14dab..9c74362ed2 100644 --- a/Deeploy/Targets/GAP9/Platform.py +++ b/Deeploy/Targets/GAP9/Platform.py @@ -46,8 +46,8 @@ IntegerDivRequantMergePass, MatMulAddMergePass, MergeConstAddAndRequantPass, MergeTrueIntegerDivRequantShiftPass, \ QuantPatternPass, RQSSplitPass, SkipEmptyConcatPass, SkipUnityRequantPass, iGELURequantMergePass, \ iHardswishRequantMergePass -from Deeploy.Targets.PULPOpen.Bindings import BasicDequantBindings, BasicQuantBindings, PULPConv1DBinding, \ - PULPDMASliceBindings, PULPDWConv1DBinding, PULPReduceMeanBindings, PULPRQSConv1DBindings, PULPSliceBindings +from Deeploy.Targets.PULPOpen.Bindings import PULPDMASliceBindings, PULPDWConv1DBinding, PULPReduceMeanBindings, \ + PULPRQSConv1DBindings, PULPSliceBindings from Deeploy.Targets.PULPOpen.Layers import PULPRQSConvLayer, PULPRQSGEMMLayer from Deeploy.Targets.PULPOpen.Parsers import PULPConv1DParser, PULPConv2DParser, PULPDWConv1DParser, \ PULPDWConv2DParser, PULPFPConv2DParser, PULPFPDWConv2DParser, PULPGEMMParser, PULPMatrixVecParser, \ diff --git a/Deeploy/Targets/NE16/Platform.py b/Deeploy/Targets/NE16/Platform.py index 2c6fddf8e5..0fa204c0f1 100644 --- a/Deeploy/Targets/NE16/Platform.py +++ b/Deeploy/Targets/NE16/Platform.py @@ -28,7 +28,17 @@ def __init__(self, structBuffer = GAP9StructBuffer, transientBuffer = GAP9TransientBuffer) -> None: if engines is None: - engines = [NE16Engine("NE16"), GAP9ClusterEngine("GAP9Cluster")] + # Drop SDK NE16 headers from the cluster engine include list so the + # generated Network.c does not pull in CNN_BasicKernels_NE16.h / + # ne16_utils.h alongside pulp-nnx's ne16_task_defs.h + # (NE16_REG_* macros are defined in both, causing -Werror redefs). + cluster = GAP9ClusterEngine( + "GAP9Cluster", + includeList = [ + "pmsis.h", "DeeployGAP9Math.h", "pulp_nn_kernels.h", "DeeployMchan.h", "CNN_BasicKernels_fp32.h" + ], + ) + engines = [NE16Engine("NE16"), cluster] super().__init__(engines, variableBuffer, constantBuffer, structBuffer, transientBuffer) @@ -44,7 +54,17 @@ def __init__(self, structBuffer = GAP9StructBuffer, transientBuffer = GAP9TransientBuffer) -> None: if engines is None: - engines = [NE16Engine("NE16"), GAP9ClusterEngine("GAP9Cluster")] + # Drop SDK NE16 headers from the cluster engine include list so the + # generated Network.c does not pull in CNN_BasicKernels_NE16.h / + # ne16_utils.h alongside pulp-nnx's ne16_task_defs.h + # (NE16_REG_* macros are defined in both, causing -Werror redefs). + cluster = GAP9ClusterEngine( + "GAP9Cluster", + includeList = [ + "pmsis.h", "DeeployGAP9Math.h", "pulp_nn_kernels.h", "DeeployMchan.h", "CNN_BasicKernels_fp32.h" + ], + ) + engines = [NE16Engine("NE16"), cluster] super().__init__(memoryHierarchy, defaultTargetMemoryLevel, engines, variableBuffer, constantBuffer, structBuffer, transientBuffer) self.weightMemoryLevel = weightMemoryLevel diff --git a/TargetLibraries/GAP9/CMakeLists.txt b/TargetLibraries/GAP9/CMakeLists.txt index 8de757a61e..b69e19fc6a 100644 --- a/TargetLibraries/GAP9/CMakeLists.txt +++ b/TargetLibraries/GAP9/CMakeLists.txt @@ -5,10 +5,18 @@ file(GLOB_RECURSE SOURCES "src/**" "$ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries_fp32/CNN_Bias_Linear_Activation_fp32.c" - "$ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries_NE16/CNN_BasicKernels_NE16.c" "$ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries/CNN_Copy.c" ) +# CNN_BasicKernels_NE16 from gap9-sdk redefines NE16_REG_* macros that +# pulp-nnx's ne16 hal also defines. For GAP9_w_NE16 we use the pulp-nnx +# NE16 stack; for plain GAP9 (Pu DENG's NE16-Linear path) we use the SDK's. +if(NOT platform STREQUAL "GAP9_w_NE16") + list(APPEND SOURCES + "$ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries_HWPE/CNN_BasicKernels_NE16.c" + ) +endif() + # Exclude dory_mem and dory_dma from SOURCES (they need different optimization) list(FILTER SOURCES EXCLUDE REGEX ".*dory_(mem|dma).*") @@ -48,7 +56,6 @@ target_include_directories(deeploygap9 ${TILER_EMU_INC} ${TILER_CNN_KERNEL_PATH_FP32} ${TILER_CNN_KERNEL_PATH_FP16} - $ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries_NE16 $ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries_SQ8 $ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries ${TILER_DSP_KERNEL_V2_PATH} From e46a09aa4e8bb3db9da52360df70594aa577de61 Mon Sep 17 00:00:00 2001 From: runwangdl Date: Tue, 14 Apr 2026 11:01:53 +0000 Subject: [PATCH 04/14] [CI] skip gap9 pipelines on forks (private docker image) ghcr.io/pulp-platform/deeploy-gap9:* is hosted in pulp-platform's private GitHub Container Registry. Only upstream's self-hosted runners have credentials to pull it; on fork CI runs (ubuntu-latest) the docker pull fails with 'Error response from daemon: denied' and the whole job is reported as failure. Guard the select-env entry of all three gap9 workflows (ci-platform-gap9.yml, -tiled.yml, -w-ne16-tiled.yml) so they SKIP cleanly on forks instead of FAILING. Upstream behaviour is unchanged. --- .github/workflows/ci-platform-gap9-tiled.yml | 3 +++ .github/workflows/ci-platform-gap9.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/ci-platform-gap9-tiled.yml b/.github/workflows/ci-platform-gap9-tiled.yml index 61cab4ea70..721cd5a365 100644 --- a/.github/workflows/ci-platform-gap9-tiled.yml +++ b/.github/workflows/ci-platform-gap9-tiled.yml @@ -25,6 +25,9 @@ concurrency: jobs: select-env: + # ghcr.io/pulp-platform/deeploy-gap9 is private; only upstream's + # self-hosted runners have credentials. Skip cleanly on forks. + if: github.repository == 'pulp-platform/Deeploy' uses: ./.github/workflows/_select-env.yml with: docker_image_deeploy: ${{ github.event.inputs.docker_image_deeploy || 'ghcr.io/pulp-platform/deeploy-gap9:devel' }} diff --git a/.github/workflows/ci-platform-gap9.yml b/.github/workflows/ci-platform-gap9.yml index 014828d6ce..597c0f40ef 100644 --- a/.github/workflows/ci-platform-gap9.yml +++ b/.github/workflows/ci-platform-gap9.yml @@ -26,6 +26,9 @@ concurrency: jobs: select-env: + # ghcr.io/pulp-platform/deeploy-gap9 is private; only upstream's + # self-hosted runners have credentials. Skip cleanly on forks. + if: github.repository == 'pulp-platform/Deeploy' uses: ./.github/workflows/_select-env.yml with: docker_image_deeploy: ${{ github.event.inputs.docker_image_deeploy || 'ghcr.io/pulp-platform/deeploy-gap9:devel' }} From ddde88f1d2efb0e2a40d05b558a7065e0bbafe8e Mon Sep 17 00:00:00 2001 From: runwangdl Date: Tue, 14 Apr 2026 13:05:50 +0000 Subject: [PATCH 05/14] [Quant] add uint8 output bindings to QuantChecker / DequantChecker QuantChecker.checkOutputType (added by the NE16-Linear PR) requires opSigned == outputTypeSigned. Existing Generic and PULPOpen bindings only registered the signed-int8 output variant, so any Quant pattern with signed=0 (e.g. 4-bit unsigned quantization in Models/Transformer_DeepQuant) had no candidate and parsing exhausted backtracking. Add uint8 output to BasicQuantBindings and uint8 input to BasicDequantBindings in both Targets/Generic/Bindings.py and Targets/PULPOpen/Bindings.py. Verified: Models/Transformer_DeepQuant network gen now succeeds for both Generic and Siracusa platforms. --- Deeploy/Targets/Generic/Bindings.py | 5 ++++- Deeploy/Targets/PULPOpen/Bindings.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Deeploy/Targets/Generic/Bindings.py b/Deeploy/Targets/Generic/Bindings.py index 308b179aef..363ed85541 100644 --- a/Deeploy/Targets/Generic/Bindings.py +++ b/Deeploy/Targets/Generic/Bindings.py @@ -291,12 +291,15 @@ BasicQuantBindings = [ NodeBinding(QuantChecker([PointerClass(float32_t)], [PointerClass(int8_t)]), QuantTemplate.referenceTemplate, BasicTransformer), + NodeBinding(QuantChecker([PointerClass(float32_t)], [PointerClass(uint8_t)]), QuantTemplate.referenceTemplate, + BasicTransformer), ] BasicDequantBindings = [ NodeBinding(DequantChecker([PointerClass(int8_t)], [PointerClass(float32_t)]), DequantTemplate.referenceTemplate, BasicTransformer), -] + [ + NodeBinding(DequantChecker([PointerClass(uint8_t)], [PointerClass(float32_t)]), DequantTemplate.referenceTemplate, + BasicTransformer), NodeBinding(DequantChecker([PointerClass(int32_t)], [PointerClass(float32_t)]), DequantTemplate.referenceTemplate, BasicTransformer), ] diff --git a/Deeploy/Targets/PULPOpen/Bindings.py b/Deeploy/Targets/PULPOpen/Bindings.py index 2c78978e23..2a68c3333c 100644 --- a/Deeploy/Targets/PULPOpen/Bindings.py +++ b/Deeploy/Targets/PULPOpen/Bindings.py @@ -453,12 +453,15 @@ BasicQuantBindings = [ NodeBinding(QuantChecker([PointerClass(float32_t)], [PointerClass(int8_t)]), QuantTemplate.referenceTemplate, ForkTransformer), + NodeBinding(QuantChecker([PointerClass(float32_t)], [PointerClass(uint8_t)]), QuantTemplate.referenceTemplate, + ForkTransformer), ] BasicDequantBindings = [ NodeBinding(DequantChecker([PointerClass(int8_t)], [PointerClass(float32_t)]), DequantTemplate.referenceTemplate, ForkTransformer), -] + [ + NodeBinding(DequantChecker([PointerClass(uint8_t)], [PointerClass(float32_t)]), DequantTemplate.referenceTemplate, + ForkTransformer), NodeBinding(DequantChecker([PointerClass(int32_t)], [PointerClass(float32_t)]), DequantTemplate.referenceTemplate, ForkTransformer), ] From 2373ff93fb8197694acf67a00edacabcbd4df6ad Mon Sep 17 00:00:00 2001 From: runwangdl Date: Tue, 14 Apr 2026 13:07:12 +0000 Subject: [PATCH 06/14] [CI] snitch-tiled: drop xdist parallelism 4 -> 2 The Snitch FP32 GEMM/TransB-5000 build OOMs the GitHub-hosted runner ('std::bad_alloc' from the C compiler driver) when 4 pytest-xdist workers compile in parallel. Two workers leave enough headroom on the standard 7-GB runner. (Pre-existing flake; surfaced as a hard fail in CI runs that happen to land both heavy FP32 GEMM compilations on adjacent workers.) --- .github/workflows/_runner-snitch-tiled-sequential.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/_runner-snitch-tiled-sequential.yml b/.github/workflows/_runner-snitch-tiled-sequential.yml index bcdd58a166..2fdd0ec839 100644 --- a/.github/workflows/_runner-snitch-tiled-sequential.yml +++ b/.github/workflows/_runner-snitch-tiled-sequential.yml @@ -33,10 +33,10 @@ jobs: - name: Build Deeploy shell: bash run: pip install -e . - - name: Run Test # VJUNG: Run tests with 4 parallel threads as GitHub action VM has 4 cores. + - name: Run Test # 2-way parallel: 4-way OOMs the GitHub runner on the FP32 GEMM/TransB build. run: | cd DeeployTest mkdir -p /app/.ccache export CCACHE_DIR=/app/.ccache - pytest test_platforms.py -v -n 4 -m "snitch_tiled and ${{ inputs.pytest-marker }}" + pytest test_platforms.py -v -n 2 -m "snitch_tiled and ${{ inputs.pytest-marker }}" shell: bash From a47ae48b0a8c41a2026f6ce437fff8b3407a3665 Mon Sep 17 00:00:00 2001 From: Run Wang Date: Sat, 18 Apr 2026 22:08:57 +0000 Subject: [PATCH 07/14] [NE16] add CNN_Libraries_HWPE include path for GAP9 SDK NE16 kernels The generated Network.c includes CNN_BasicKernels_NE16.h (from the GAP9 SDK autotiler CNN_Libraries_HWPE directory), but this path was missing from the cmake include directories, causing build failures on plain GAP9. --- TargetLibraries/GAP9/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/TargetLibraries/GAP9/CMakeLists.txt b/TargetLibraries/GAP9/CMakeLists.txt index b69e19fc6a..26d10d9b0c 100644 --- a/TargetLibraries/GAP9/CMakeLists.txt +++ b/TargetLibraries/GAP9/CMakeLists.txt @@ -58,6 +58,7 @@ target_include_directories(deeploygap9 ${TILER_CNN_KERNEL_PATH_FP16} $ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries_SQ8 $ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries + $ENV{GAP_SDK_HOME}/tools/autotiler_v3/CNN_Libraries_HWPE ${TILER_DSP_KERNEL_V2_PATH} ${TILER_DSP_KERNEL_V2_PATH}/FastMathFunctions ) From 6a3793a98faa538866e7e30e63103ad882fa2297 Mon Sep 17 00:00:00 2001 From: Run Wang Date: Sun, 19 Apr 2026 00:18:21 +0000 Subject: [PATCH 08/14] [NE16] fix v4s/v4u type mismatch in NE16 GEMM template KerConv_NE16_T.Pad is declared as v4u (unsigned) in the GAP9 SDK but the template was using (v4s){0,0,0,0} (signed), causing a compilation error on GCC with -Werror. --- Deeploy/Targets/GAP9/Templates/NE16GEMMTemplate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Deeploy/Targets/GAP9/Templates/NE16GEMMTemplate.py b/Deeploy/Targets/GAP9/Templates/NE16GEMMTemplate.py index 809d03d9c5..8acb27ce25 100644 --- a/Deeploy/Targets/GAP9/Templates/NE16GEMMTemplate.py +++ b/Deeploy/Targets/GAP9/Templates/NE16GEMMTemplate.py @@ -138,7 +138,7 @@ def alignToContext(self, ctxt: NetworkContext, .Tile_OutW = ${batch} * ${M}, .FilterSize = 1, .Pad_Val = 0, - .Pad = (v4s){0, 0, 0, 0}, + .Pad = (v4u){0, 0, 0, 0}, .W_Offset = -128, .Qw = 8, .Mode16 = 0, @@ -218,7 +218,7 @@ def alignToContext(self, ctxt: NetworkContext, .Tile_OutW = ${batch} * ${M}, .FilterSize = 1, .Pad_Val = 0, - .Pad = (v4s){0, 0, 0, 0}, + .Pad = (v4u){0, 0, 0, 0}, .W_Offset = -128, .Qw = 8, .Mode16 = 0, From e6355a9ae3639624a222428f529d049fe3cf7187 Mon Sep 17 00:00:00 2001 From: Run Wang Date: Sun, 19 Apr 2026 16:53:26 +0000 Subject: [PATCH 09/14] [CI] add NE16 profiling step to GAP9+NE16 tiled workflow Runs each NE16 conv kernel with --profileTiling after the normal test suite to collect cycle counts from gvsoc. --- .../workflows/_runner-gap9-w-ne16-tiled.yml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/_runner-gap9-w-ne16-tiled.yml b/.github/workflows/_runner-gap9-w-ne16-tiled.yml index bff85b1b95..086500e628 100644 --- a/.github/workflows/_runner-gap9-w-ne16-tiled.yml +++ b/.github/workflows/_runner-gap9-w-ne16-tiled.yml @@ -52,3 +52,31 @@ jobs: pytest test_platforms.py -v -m "${{ inputs.pytest-markers }}" deactivate shell: bash + - name: NE16 Profiling (cycle counts) + if: always() + run: | + source /app/install/gap9-sdk/.gap9-venv/bin/activate + source /app/install/gap9-sdk/configs/gap9_evk_audio.sh || true + export GVSOC_INSTALL_DIR=/app/install/gap9-sdk/install/workstation + export GAP_RISCV_GCC_TOOLCHAIN=/app/install/gcc/gap9 + mkdir -p /app/.ccache + export CCACHE_DIR=/app/.ccache + cd DeeployTest + for test in \ + "Tests/Kernels/Integer/Conv/PW_2D_RQ/Regular_RQ --l1 32000" \ + "Tests/Kernels/Integer/Conv/PW_2D --l1 32000" \ + "Tests/Kernels/Integer/Conv/DW_2D_RQ --l1 32000" \ + "Tests/Kernels/Integer/Conv/Dense_2D_RQ --l1 32000" \ + "Tests/Kernels/Integer/Conv/StriddedPadded_2D_RQ --l1 32000"; do + dir=$(echo $test | awk '{print $1}') + l1=$(echo $test | awk '{print $3}') + echo "========================================" + echo "PROFILING: $dir (L1=$l1)" + echo "========================================" + python3 deeployRunner_tiled_gap9_w_ne16.py \ + -t "$dir" --l1 "$l1" \ + --toolchain GCC --toolchain-install-dir /app/install/gcc/gap9 \ + --cores 8 --enable-3x3 --profileTiling -v 2>&1 || true + done + deactivate + shell: bash From 74fac924d5f0c60a348267a65b1ec8338993e9c1 Mon Sep 17 00:00:00 2001 From: Run Wang Date: Sun, 19 Apr 2026 17:28:13 +0000 Subject: [PATCH 10/14] [GAP9] fix profileTiling: add CycleCounter.h to include lists and cmake profileTiling generates code calling getCycles() in Network.c but the header declaring it was not included. Add CycleCounter.h to both GAP9 and NE16 platform include lists, and expose the GAP9 inc/ directory to the network target so the header is found at compile time. --- Deeploy/Targets/GAP9/Platform.py | 2 +- Deeploy/Targets/NE16/Platform.py | 6 ++++-- DeeployTest/Platforms/GAP9/CMakeLists.txt | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Deeploy/Targets/GAP9/Platform.py b/Deeploy/Targets/GAP9/Platform.py index 9c74362ed2..c262aeab1f 100644 --- a/Deeploy/Targets/GAP9/Platform.py +++ b/Deeploy/Targets/GAP9/Platform.py @@ -285,7 +285,7 @@ class GAP9StructBuffer(StructBuffer): _includeList = [ "pmsis.h", "DeeployGAP9Math.h", "pulp_nn_kernels.h", "DeeployMchan.h", "CNN_BasicKernels_fp32.h", - "CNN_BasicKernels_NE16.h", "CNN_Copy.h", "ne16_utils.h" + "CNN_BasicKernels_NE16.h", "CNN_Copy.h", "ne16_utils.h", "CycleCounter.h" ] diff --git a/Deeploy/Targets/NE16/Platform.py b/Deeploy/Targets/NE16/Platform.py index 0fa204c0f1..665146030a 100644 --- a/Deeploy/Targets/NE16/Platform.py +++ b/Deeploy/Targets/NE16/Platform.py @@ -35,7 +35,8 @@ def __init__(self, cluster = GAP9ClusterEngine( "GAP9Cluster", includeList = [ - "pmsis.h", "DeeployGAP9Math.h", "pulp_nn_kernels.h", "DeeployMchan.h", "CNN_BasicKernels_fp32.h" + "pmsis.h", "DeeployGAP9Math.h", "pulp_nn_kernels.h", "DeeployMchan.h", "CNN_BasicKernels_fp32.h", + "CycleCounter.h" ], ) engines = [NE16Engine("NE16"), cluster] @@ -61,7 +62,8 @@ def __init__(self, cluster = GAP9ClusterEngine( "GAP9Cluster", includeList = [ - "pmsis.h", "DeeployGAP9Math.h", "pulp_nn_kernels.h", "DeeployMchan.h", "CNN_BasicKernels_fp32.h" + "pmsis.h", "DeeployGAP9Math.h", "pulp_nn_kernels.h", "DeeployMchan.h", "CNN_BasicKernels_fp32.h", + "CycleCounter.h" ], ) engines = [NE16Engine("NE16"), cluster] diff --git a/DeeployTest/Platforms/GAP9/CMakeLists.txt b/DeeployTest/Platforms/GAP9/CMakeLists.txt index c723bb02e9..bc459d9c7d 100644 --- a/DeeployTest/Platforms/GAP9/CMakeLists.txt +++ b/DeeployTest/Platforms/GAP9/CMakeLists.txt @@ -17,6 +17,9 @@ add_deeploy_executable(${ProjectId} EXCLUDE_FROM_ALL ${SOURCES}) # add_executable(${ProjectId} ${SOURCES}) target_include_directories(${ProjectId} PRIVATE ${CMAKE_CURRENT_LIST_DIR}/inc) +# Network.c needs CycleCounter.h when --profileTiling is enabled +target_include_directories(network PRIVATE ${CMAKE_CURRENT_LIST_DIR}/inc) + target_link_libraries(${ProjectId} PRIVATE network deeploylib) target_compile_options(${ProjectId} INTERFACE network) add_gvsoc_emulation(${ProjectId} "gap9.evk") From 2d75f76adb3930491708879367680cfe1fdc4d72 Mon Sep 17 00:00:00 2001 From: Run Wang Date: Sun, 19 Apr 2026 17:41:33 +0000 Subject: [PATCH 11/14] [CI] fix NE16 profiling: disable LTO and clean build dir GCC 7.1.1 has LTO linking bugs with the GAP9 SDK PMSIS library. The profiling step needs a clean rebuild with LTO disabled to avoid conflicts with the cached LTO-enabled build from the test step. --- .github/workflows/_runner-gap9-w-ne16-tiled.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/_runner-gap9-w-ne16-tiled.yml b/.github/workflows/_runner-gap9-w-ne16-tiled.yml index 086500e628..33080003ee 100644 --- a/.github/workflows/_runner-gap9-w-ne16-tiled.yml +++ b/.github/workflows/_runner-gap9-w-ne16-tiled.yml @@ -62,6 +62,7 @@ jobs: mkdir -p /app/.ccache export CCACHE_DIR=/app/.ccache cd DeeployTest + rm -rf TEST_GAP9_W_NE16/build_master for test in \ "Tests/Kernels/Integer/Conv/PW_2D_RQ/Regular_RQ --l1 32000" \ "Tests/Kernels/Integer/Conv/PW_2D --l1 32000" \ @@ -76,7 +77,8 @@ jobs: python3 deeployRunner_tiled_gap9_w_ne16.py \ -t "$dir" --l1 "$l1" \ --toolchain GCC --toolchain-install-dir /app/install/gcc/gap9 \ - --cores 8 --enable-3x3 --profileTiling -v 2>&1 || true + --cores 8 --enable-3x3 --profileTiling -v \ + -D CMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF 2>&1 || true done deactivate shell: bash From ee2f1b99a54631c2a0160d9be9f4dfb5ec3a7b8a Mon Sep 17 00:00:00 2001 From: Run Wang Date: Sun, 19 Apr 2026 18:20:59 +0000 Subject: [PATCH 12/14] [NE16] wire --enable-3x3 flag in generateNetwork.py The --enable-3x3 flag was parsed by the runner script but never passed to generateNetwork.py, so NE16Engine.enable3x3 was always False. DW 3x3 and Dense 3x3 convolutions silently fell back to the PULP cluster instead of dispatching to NE16. Add the flag and set it on the engine. --- DeeployTest/generateNetwork.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/DeeployTest/generateNetwork.py b/DeeployTest/generateNetwork.py index 0b25bc6bbe..a7b678e701 100644 --- a/DeeployTest/generateNetwork.py +++ b/DeeployTest/generateNetwork.py @@ -84,6 +84,13 @@ def generateNetwork(args): platform, signProp = mapPlatform(args.platform) + # Enable NE16 3x3 convolutions (DW and Dense) if requested + if hasattr(args, 'enable_3x3') and args.enable_3x3: + from Deeploy.Targets.NE16.Engine import NE16Engine + for engine in platform.engines: + if isinstance(engine, NE16Engine): + engine.enable3x3 = True + clusters = [engine for engine in platform.engines if isinstance(engine, PULPClusterEngine)] for cluster in clusters: cluster.n_cores = args.cores @@ -192,6 +199,11 @@ def generateNetwork(args): help = '(Optional) mapping of input names to offsets. ' 'If not specified, offsets are set to 0. ' 'Example: --input-offset-map input_0=0 input_1=128 ...') + parser.add_argument('--enable-3x3', + action = 'store_true', + dest = 'enable_3x3', + default = False, + help = 'Enable NE16 3x3 convolutions (DW and Dense)\n') parser.add_argument('--shouldFail', action = 'store_true') parser.add_argument( "--cores", From ac3d1726c0910320b5caf3d22849030b21df3e86 Mon Sep 17 00:00:00 2001 From: Run Wang Date: Sun, 19 Apr 2026 18:48:42 +0000 Subject: [PATCH 13/14] [NE16] pass --enable-3x3 from deeployRunner to generateNetwork The deeployRunner parsed --enable-3x3 but never forwarded it to generateNetwork.py's gen_args, so NE16Engine.enable3x3 stayed False and DW/Dense 3x3 convs silently fell back to the cluster. --- DeeployTest/testUtils/deeployRunner.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DeeployTest/testUtils/deeployRunner.py b/DeeployTest/testUtils/deeployRunner.py index 71b056e9df..bbc313b9a6 100644 --- a/DeeployTest/testUtils/deeployRunner.py +++ b/DeeployTest/testUtils/deeployRunner.py @@ -239,6 +239,9 @@ def create_config_from_args(args: argparse.Namespace, if hasattr(args, 'plotMemAlloc') and args.plotMemAlloc: gen_args_list.append("--plotMemAlloc") + if getattr(args, 'enable_3x3', False): + gen_args_list.append("--enable-3x3") + if not tiling and getattr(args, 'profileUntiled', False): gen_args_list.append("--profileUntiled") From 822dd320388bdc71f2209bf10db49daa3680b744 Mon Sep 17 00:00:00 2001 From: Run Wang Date: Sun, 19 Apr 2026 19:14:35 +0000 Subject: [PATCH 14/14] [NE16] add large Dense 3x3 conv benchmark for NE16 utilization test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a 64×64×32×32 Dense 3x3 RQ Conv test case (75M ops) to properly benchmark NE16 throughput. The existing Dense_2D_RQ test (16×16×8×8) is too small — NE16 dispatch overhead dominates at only 12.8% utilization. Also wire --enable-3x3 through deeployRunner gen_args. --- .../workflows/_runner-gap9-w-ne16-tiled.yml | 6 ++---- .../Conv/Dense_2D_RQ_NE16Bench/inputs.npz | Bin 0 -> 262412 bytes .../Conv/Dense_2D_RQ_NE16Bench/network.onnx | Bin 0 -> 148684 bytes .../Conv/Dense_2D_RQ_NE16Bench/outputs.npz | Bin 0 -> 262410 bytes 4 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ_NE16Bench/inputs.npz create mode 100644 DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ_NE16Bench/network.onnx create mode 100644 DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ_NE16Bench/outputs.npz diff --git a/.github/workflows/_runner-gap9-w-ne16-tiled.yml b/.github/workflows/_runner-gap9-w-ne16-tiled.yml index 33080003ee..16bb81976f 100644 --- a/.github/workflows/_runner-gap9-w-ne16-tiled.yml +++ b/.github/workflows/_runner-gap9-w-ne16-tiled.yml @@ -64,11 +64,9 @@ jobs: cd DeeployTest rm -rf TEST_GAP9_W_NE16/build_master for test in \ + "Tests/Kernels/Integer/Conv/Dense_2D_RQ_NE16Bench --l1 32000" \ "Tests/Kernels/Integer/Conv/PW_2D_RQ/Regular_RQ --l1 32000" \ - "Tests/Kernels/Integer/Conv/PW_2D --l1 32000" \ - "Tests/Kernels/Integer/Conv/DW_2D_RQ --l1 32000" \ - "Tests/Kernels/Integer/Conv/Dense_2D_RQ --l1 32000" \ - "Tests/Kernels/Integer/Conv/StriddedPadded_2D_RQ --l1 32000"; do + "Tests/Kernels/Integer/Conv/Dense_2D_RQ --l1 32000"; do dir=$(echo $test | awk '{print $1}') l1=$(echo $test | awk '{print $3}') echo "========================================" diff --git a/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ_NE16Bench/inputs.npz b/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ_NE16Bench/inputs.npz new file mode 100644 index 0000000000000000000000000000000000000000..ffde610d3a0611e539030f459c116414decff7a5 GIT binary patch literal 262412 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 z

~?9lkkmA6@tzn{~C@dkS)}o5s1!zd-&WPJ0hI!`3$R@PF~V`*Geee(%co8vg)x zt`q()JV)0Sh@GGtZ~p&h!fc_FXV$hIfj}T45C}vB0)dFAKp>_p1fn7Wfj~qc5C{YU zQ4v{LSs7VbS@))>N@QhZC9?864u<=CHfx{jy#D6;n}OcX4)71`d>ya^&Tb9oKHxKt zHg~0-!k($HUEl%icfj-R&>Yr0&Ess%BXy7qd@sn>*5AYCew-l*F~56qjt^|+cjpNx zkq@B6zre3vWB);SrV)CMeFxn4CqC;8(A|f-wja^rdxGuV-5D>O_txuI{Oh5cP{Uia(S9WYlz+^>G{37n~=-V$HHXa5OY!7FU` z)8L;XZ-Mdd%0Be@7VN{lwAdezH(&_t;eu^3XGZiMY_1jZ6Y>s>k!xaFYI-NuD6!ew z6=cXk9bXE&n}k@;B-mPHb9>J73*;HNoh7D^-JS~AUC1?j#OD1Qr{5VC`20AVIm~x^ zgXr$U{)D`MPF(2vjB z&a(&3xSUfT>_AJLJ=jwU8e%u_6}H#SnLB)pXGZ8fbn|7Pq26r{{PsP-KLPf8fSt*G zm&oR{mpyEb2XI9GyT=;WAb+UiJE((SbbB>d0xsyXy|nPoSYZ9~Zr`G($O*PJvURLG zLGHj^5bi$TfjyXS2bSmmewV!G1vYp6fjmSu#|b=vXHapz1KXqbm%yHLo;~&kb}t#S zwe9Bt{ME&o`emaqi7I-gQlE!0*RgO~ZR2_LtZJG0vMI8|QnhKtLZ8 z>zwv#Y>nM`d+Wmsa7EVV-nzu+V2b?!+n;B)%GkzWtxUc;#QjaMCCHvV5IchHFT)~jec za^%>Qzp=aT95muFzE5l0OOF_9zoOfR{rI<8fA1WB!@LuFbEod%>3+tX&f({U_#<%{ zHtkdF7i`w^cirBcqkt#GjbQt?j=Qt2=j@?SevEgO66d{mZ^oM6KCZ|m2*4Swl^a9v zTiE@A?C?292Y9ZJZq5#@U3rLn1PwhU*aB=1?(peupWW>N-QApkJISbF{fBdW zBi4PppS6!$9sDz3KO?wL>_SBGX zs6LN7-owVfajwSKJ;ysM;T85P_UaK|!|bp-Ft>G_sX`wS>;78g+rLlM!GJjXz7UrI zXP*J*G4}$PI|F`{%`I;IH1Ymw;;nx>k1qkvRAS%4=BWdDiFrb{CwtM>k~77&I>%g@ ze$KhGfGtO!Ve_qa(4DuyXN zrw6zP`yCAQc)K6;J}|D~OoFeAo*|nrMSexLeu@4J5@y??7r-8FcY)1&@QihyVE6ce zTp&A(zb)U`T4J4}LhoUht?4Jg?$23od5Le3tt*>%KuigiAOY@j2Y2BOs6&h~-q~7m z{CgX|;TbXJPpIRrQ*c4I?f{>&TcgG{MOPl*+52*4cT(aXBm4W?g-dL2*xW&aY`!J3 zv)iXp`VbgQw4e;-u&*-oq3l-{0+W6x;w9s&7C8Ax1Y#eVkW@d z*wX`S{w+L(JHXk^`vV7H-vQlT4)|O0u93}MgPC^Ny?WOC+ADhO&f^|BkUb;LATEapDdg!0}q^gf!)o4*afhM75eon?!`D~wD&c3XSwYQeM7A0y(4Gc z!9(~2toMZeiC^8hpJ97`gm|%BL7tlw( zwZrj4_XUcP{g%@CMr$ydeKOvqaC(KfnR((Gw8pPx}R|(4XeJMvwaqrkob0ec7gD`#@_1XSqeegh3~<_TB;^(}tS^nfuxkFi#P?CcHb5L;2p zd*0#?l?DqbLyo29}b%)kA zr{}xK2jFgQ@v!^PVQts1c&>$O&+u8`cg6(&Cw6PvQvmj7Zfkk|_HTb}A@&vK@!nF{ z_!2&yi6zFfQ)Kt{3Ig&AER6erzrW&pw|0uZM7IZJeW`!27rW z=kYDM1MTLr@5MRZ(-ZuSuFV?yN7&3`Pao*^Vb8|62k+9H3Gxv83H^%h9+N(-gSt1lZ#Wo}B;Z zJHYuI-TmtuAY0#lf4~sACZ?uF2cI>6fNX4m{{&aW*4R9I1hTzYYYMyr`?eqTXW$+# zf^#kTb9_0vc}r|7*!Tju!SB6#A1O9x*`S-tyReqXz^5_bG^a1#guVQsJCk?l`G$PL zTIO*-ANVq$eUD9qNAhEnpV41ob5u>Ny_m;cTSLFTAzYK=0UiTqTEgaYclMFNTl55Z zHIHw)LS7;ZXUOpf@P_}@c=!|p&n5qCfy!rp%mTOfye65{Vx zhQE*g4x7t2Hzlrz&AW3)Pry6tf)N-1^JK)khY~i{m@%+d-?wA8`US@JK!Wc9pK=r8+v%cP-?&$BA;$gfvAcr?+aJ2MPGAeH>8wxq z{f+cY4xZ8N{{uOI+j-bMd}G(=-1g_}ew_b^KZJOv8)Rb+UvaLgiFakL!Ti2Inzz=$Yy{4}BXw4n|6>JXYRrh_=)UA-M|AD;$ z_C6ujm?fNZCLvdg&lxLXn>(yu;Y-jv$Xomcaqh=`?$OPkngiR-j(>st37gMaPhgC^ z0QP%h&gi=mXPzFgj(&5N_(#b0e1hN5OE|~Zh229H;+cT%URU_EdB+Lo+(!XQ?4I%C zo~FRw%sD{rfwqa?A>(K0##fJ=Kga&`+uIBN4z@MWf4KWoWAP`zyUVeE5^Kx=`2=cf zpnEqr#&7LExCO@hG0r#P9vb8=F-K&3eS#n08SFV@0z82ao+17wV_RVB1K-tp9)ANi za2?`aZ~f6rgKqEkdga{1Io{1PyaT@F3i~UZgA$uF70AZaa3F_$FXvI8!SC2TXG{Q| zA7b}ySfh_@z6KnTKi~zt108XW@4)!mZpd$}GX`+NukL*28KQeXzN; z1?LXPzP&NB_hdf_vN@gKS>!i-32|5W8(WKQjoya1kBayjUk2M_Am(-sejqNzR&%CA zFVWq{3?$giZC|o?ZvNewTAzF~7aIk3Z}O;7$^BW4zNNaDSfFR|0F=yZsihdp2eZK9HUNi0>Dl zd8}jX>5c^U%ow|OZ%hx=#0PkRttE%}BhFn|bBFH&JD0u`+1&5&3bwwxY0#a=U45c& z=EUN&$HN^@*sNK=2jZ-$&wdKH3s=}1&g*}|Ho<1jZ*2B|REMqAgt%L0PqCGt0&C87 zh`S)y;0nxFVsp3l+Hl5m0h?#!fIfrGpAk1i{y;Wwf<6WA#QDup1@huEPd<-l-Q5KL z2mTK1TtCP|*!ivH%m-!c32dL*Qsjo5FW6`3_PvLFTi#=9_U{|a_C^IbWg z60^eQ3iMq5c>|a z73cw&$2Y3}jn8?^Gr*p}_Wy%z2nX_6Go8n~`{i68xoyaU>`WQ{mbi*D6?SJ?++#{O z$1c0)1pYuzh;t8XU?1lB*YA*+B=A4j{+-Vqng4Y6P=3XHKe`1}}C0`r*D+1*t@PmnWZV7zx>+$VPD z@;WiG<6T5TW_66HxNz5<4 z4f-DJfO8vnA-)Z99?!fIcZ6%~6@KT+!2y)mJZnEo*gg}^x~J`&oaj$*z-GM!KAhv; zulUx;Ew*oDe+T>Mvw7^VZeoAdxquc}rvlDtKkmtQvO#z6?k~e;kMF>K-N7r+pM+S; zUbA_8Yn~aS@8O&{b8XR;?b#g{uzO1I+2aA-y3YB4Z44^n5@1iBZK(AI8y~=s7;C&> zv$qMdGYWT75%0cgc!$28$KE~L5L3eLYyvxHy{@v7wGmVUeQ0m!QV0@s|kX24&f|8k}=9!{_o=lFJJ_?&5m{(-NH9Dq5@Zx8Ona|7f* zZRc3uncdMET=4H<=Q8&YJOTS&!#~&$%5!ofPq5k76fBSfz8v`5mZHlK_@0ry1Nj76 z{pc+-nAbBM_y|hie!@J)nAg6o@dUQOdu=$oImcYiHbTDr{~XBHEQk>eP`B3t-vPXW zF}ibogC+h8?C$O56-@8(?#ejl@$J{hS8U$z0h{p$ z$|V@gD>oh>y6&ahvRD`n!`T^CG| z^>tx=Yh?3%ASd+nifn%e?Dn`p-hvUjwjr`Jny27Q%M45Go_WD;tr0NK8-91=jw<33 z(7_i#)x@0Io#Ti2fIOJT-0}kmz}`ILelF&MpTP@qZ_aK&0+juI3glUwlgiqzCs_vnLz&L{0}%G%hpQ40Kf0X z9hK;wtF*)3M@8)ey1BH?K#6Qk@2^B30`F*q%{yM8D^I}%ySucHC+t`JJGc+DS-%6; zprY1f9&>wkjO?t|SfJaFaTz)6y@PJfigO?6*;mZjA`h_#*z zfpFMduDKO6luCQm=16&bje^>YbzQNHrusX+GE#TrY z%GR{j1-%DX`21MYoh-o zp4HFMW{sPj8qS=+39P+oID3x!@;h#g137BoT+Wb!cVL|T+;ZWc;kP%>1dyVS&>MUu zdZSI98qTB5UZ=$Usy|W}xyC;u?g1Qtva@)`ow<(>Vu!Hv{>$l{?&uX?Kz<=!_Pl#C z#{k(q43O&(dr|KKcQgckhnz77&TZcl?B=m=S1%u-=t@q!|(qVn|%b%wCMI(U`yZ{Jw-NG3O=zd&{x3o z?qrH=UhVolTanAT^l#=de^;Fx>H)bz774LKbkEtV-*~=058?=XO&bepvIRD}t@7MZGbN@c$^ZwjH zj{QR1D_o&3f%&x^K#s1xMNY`=8SC`n3ujtz#aENNi>-wd{K7m#us+8-b{7q?YitE_ z$=M0AJxtL(>s;mzf!_ysJCD0h;W6-hL(CP}Q-)2>K^NKIiwYFT+DiBY4qtI6DK>ZC zJ9Ca2o?n81%{h#(vD;GvJ8O--4_iL~`?g1a6O4BTW89VRX8;RxTGzQRZ~~nD2YZX$ z!sd8Eo)FW+XJ6(Xg9QBuyN9ZYe^1r@tYCNe$60%9nwT#kXBT@DqTe~5(Gy@VJ9q}1 zVGC!ld-{OQVSTxT?K6NiamO#&Z@pe_Rj5N0<4>WClH!=XU>y@9X9VZJ;(1x7d-_J;QcH1>zLQ`?%#gg>j20J`Rw0VXS)#djJ`y^y%BWl z+lTeV0d%od*v7<_$mUDYGxP%}i1ADVf57fyLX5p6u<_ob`+X(G*a1FSTOEi4d*nV) zF61WiaUN$4oUO21(|j$kw*;B=1PsR=gkaeC(pdx`E|Q*1kU1w8Bhcn>*9s53&hc0f1B3i%iAoMY`8dkI>w z#^?Qec81?N1-2aSgBkJye!;KqzTK^MW1Z6;g!6RZ2QUORz5L)monzbqxdP5Qz-A94 z_yO3P_vkLo?fq)sA*-K&HJ;DRMcxZ}YWi;ABep%hKjeUHUkT`e16a-DuKKWb+*OHe zY)cOPIq^_UHIG5)W^c-HyV?P}@!?uR)Kfr-_{pK40_u!jL zu$#96_Wy$2yYOATBewxvAm8qf7U(k8x+_c0Thx0rggJ<`uRN@6G$O7iTNa@va(t z8M1vPcf0-!*l(XR#=58DJ#GgtK{=1J80%eKfV=TdD!7N=`o@ou-Gw{dBLDAvPxzlf z3TB`s&R8)*-^}CRnH6%l+dIg{+M|7qK_GXIKYgTsxMKdrJnqyQd*lx2Be(dR=?WU; z1X$mmt0umUfUU&l-2}KIXM*pIGX;1mdo=!4Cf>XHP$r)}c;20jkl*1P zUV#?By_ur{?lQqHrofnA>=*3jNY3%@CfMDt@57okK6C4LcS+z~AeZ+upTt<#kMn!x z3JPM)+aWeX&jor3AK<2mJ(|;e?%%k7eRi;?==M{=>NRHq9MGG<{AX;i^|#3BJietN z@`acaoWL7c+1&1VqMdWM&pgiRzTK~NJmV~572TP%Ics)~cjycy{(#;7tdXJ5;FdV^{sD7m zZ~PtVd_nvM=(DFOXIH>lYuFi5?CM?E{g=qjZf{M9{UvZ$8@?^vz!mx5zv3Ic z!uqtiEBp8c_T|47o;dS>{RjszMV^6*yd(6AIAa_1F}nVW^Y&_=-a&@mflolceJ|v5 zyvxA(0^OOsU+-oOkAVBN56|g40ONck?x~?}f?i^Gj?+2T&(J%_TkwIskKRLGf)w4J zci24lgREV@ckJ)~5ZT^yP!OL`=NF&5u?KD4bL1`hH$L+wz#KztWAKSQBhKDEpW{Ek z?%Ol=`+{u;o6pY`-5yq)3CJxlPWHZ*$T!O>p0QTR5-JyB?jL&gz8+2z+nt0|3eG1&onX2F~%P+JJir#JGb!oPsr|ALBbq;XqtNt`^z4L;L}5u?>{5+pjt7={5`c9^E}Q zoVSO=UG}WAmGk>~b!BV^;(VJ0T!C9GzM7aLyaOw6AjTQ()x0IRy$$qV>{t9x$Pd^n zWbe;C*1)>2a6--`#9k_RguRAO=UDsmEB@YiPZhQW{w1)k{aL4cj6459HqPEZz$@6laK7Y?wo&Qvv=McyL^;wxn5GN!@j*$a02IU<|01-ING zem{ywa^bU<`?bE@;xDkf181>c`|v*VbDT55mZ4{yvF-)9lLu_p_?IUkKF4>(*1@+x z|3$w2cOQ1orRetUebnf`U_`9#1PdjoFY2lzOTe=~DpGi2ZA4umG=Y02gHC4s+z zd1mMp+;TpF&E*?hf`H9_?a#ByCGh+ZuD;@L#t45vw`Sl>-NaqE^AmQ@X0Scqdgok) zENbL8;N9%dH(&|A(cOdl(Qn+xBlnJN0IXG^AFx-{vtNDYPeF~(dwM|bf-M+<4BLe> z39|3PT7jPZbdas-J-hEIatZh5_uruqXUoye9q?Jt{d(WN9q%i{?>x?6O?$jz51jvm zKhOjAA#tzhavx;a|G*SE0rkD!za9dyHy?bx$1cv)LZ87u*cO~Ke+PXB+?jBP?#Z}@ zyrX&S&wdJEj|Y4Y*mL9(c9xbi6YMGSHzwm=OweDE53v24SJ@u@H0Z-~JfD0;f70;Au^Ib`&7IZo1#FRzU5Rsg*zDc;r`QT~&srzNpI~!Ncj=iVFb8saj`g*r z=sA2Keu4cR-S{!K02nJ4@oGQJw$1b&09=ggk9Uw1aW%NO)3dWQcG`Gox)-lA`io!@+ceylk= z$9de3y{%w>^VayDk?q_5_vlA;WP8ier{E9S9twOr;EuI3-5i zSnsEOy!VQHz*fNDV6BYL-hB@vU|-H?AJ*AnQ`h(N80*?cpuT+`K!bcC#@g=4+BLIP z$SH6(dvWL1_gy{A$%#Az1E6f*_B5MEyS0@IV2%XX?*x4VKB;3bd*l+?UW|JH-f2Q# z4IE(aqy?|UW!MhD{rBea?O)N|Nr3IOKsJ}NSSKa!9Zui__*dVa^-W>#-#27W#%91e z&K&fi=jgSJtw8qd1i1&SYivSaEk67Egx{7n-uG=6$e+aN`!{=vZp|LPSNI8^aMl#AupNjI z34Ny66KsA$h=02pculP5+>Lqc<%GTkE&44VHfOWf68jXmhXF9}47AOCC&cv8Utnw7 zhkI@CC&asl0$U&1oqA{5%|8MsutayxHSEsReXjwVd4C^c|JoC5VIF@U?a!JuHuKn5 zgKV5T$vBr|yX}>@E;e`K9<61s34ZGsYp?F1{K5{7$o4aZN1#EjfcZSn&jX&V;OM$-)@pr?#9rXMh=RLrE z^Z>WS*t51VcqP_5w@-IafYUtYv9AmG=d*qnpZ)y;YuNJ@Otq6cL$(*?Bm9cKKgS*P z;T{-(%3kL&zvlwDgh zJ#FyYUmsY*zRXjC0(joJ)lcAdmh(^Z_%1SRo}U8q|A8jN+1=v<{tEUEdc=5F&Svcv zoA+a0=Ptny{&!=LjdPX_^1uD0=r#FMY~8!xy_?5-(q`^8Y(4LE0Cu3lZ+|W4jk(P= z|L-o`dx?LI%`?tAf!#@q?E85^?qPcd%e&9mhMaOuoO{cN3o?2`tqrpCS<`!ScJFqJ zuFd}JzafuugS?u@8O){a?J>UJ9Qy}0`!UxyasW@n$d2|x|=n2b7#m6IScd>-vU0u z{#MwR`K+B_bMGa(Z_v3OfVuU1M+tfCzX8A4D^P+laocn3#U8EEz$t9q0L=G;&6<9< z?B6*uY}N_no}H81$G%s{E9~a+otVek=C2;PTVM|_py8~$u!junz-S(I-;wb-uud1B zxlhQ+SKNcTck+hr*@`;t9R+&8-kTH8r$W90-@p|F{B6JicEoJZ-M=#)mEi;S6xsY4 zx*TBlXsy6m@56KM(z6HROYi`4V87O~SMTBlB%Cv@HV0=5p%4Csr}(mYJeOk|AiI~q zIoUbfPXiZc@`0sHKDi6*rw=<pIJTSfjH{=qJ+;d}Nj?CAwIr*&Swh(|VN zKaX##1Ah`@K6f&O3wR5s=lgFOyi>-uhC84kpD`=slK7vmsE^S7%-}a{2Vzp#Ud%B< z&wxAgE^^Sto?vs<-^Zvsqw(%-0Z)MS1NIBHfNt(1T#?&e+>g26@!#ek_6pv?@*eku ze`pN099ww2?;Z9|%)i0!4!gj(4(z!e>|EA$9{VzXK;B_1(UmXQN|0h(f(>?OTfuFJ zF(>5L$9Qgyyanz*pT}9o=xb!pS~JII-l2A&-5l0y$XUThba^5~g=e~B>-OC!z@P7bn`(A3$lFwauem18j ztiJ;0^;`=F;(m!MksInJ_jAsf5>sP;hWEhpId)KC!&u<=kI05JD!|ur)Zh(3Buy1mK?B1+z+)hR>;R|`X$RF?kJtyAS1~!*7ntzLY z1Rao^)iU0@jrnfFt7ggnLLki=dbAQ^$KtC zH_ncKf}CUXcVdigO=nnw4RY1Qcjf)gusM@+*uUpLkcV)K{|~u9J@zdKz;l6I z*72-;IhXU)^x`}3VP64rnPZ8skM8}J$N|=06EFMr4v(>Bh2OpGvD;??v|Tu}fbBuM zFsJn<*zCtMJJ1!_tT%vv;R7~ze?$-PtuFO5^ebnb!@94;mim!BTci6nlZIGiW32fE zr|47I+U~9)-w(1gSIE|#VNYQ5+w&ORKAr@!yRL|xAosxsvAMCxy?N{{pud4VdJfEM zjsuW)=a|=fDA2cH0nDA_^IT1h19}(P*`3pSFz*PxLC%mn=h)XL@&x+;jKLK>;LCvj zo=wo5+xqtO@)h4?3unOCzOleM#^3D3`W6f9_Fyh+8f#n^-h!5X?Yjd$IGYM&cW4hw zIDxGlh}|J4$i5|G-pJ+r2ka&K0|>}DwgTS@*>`+JmUHwP->Q;cSZB1HP+@TBCDBi*JFg zCdU=oy7ukv?Q=}r6hcEGlUr^>(@gLABTA=daK_6lzBt??az_u`op+Y0>?9-%v1 zM(hS#AK1I|xr>UL6+M;28t=T;uV8)V_WT(5w#=~v1N2w)_A$Ph8TuZ(@c|g)-1cH$ z?!mr&qlkD~}+yw=G@8=12*VZ+s z`_YzxcWmW6e!tz*5ZN6Bfls)nE4;$z$NoOh51qEv_O~Jh`Z6$^BLj0ga|a%Q zcjSb#)*GN(a|$Qeci6lGzvlrtV0*@AjTU*0+|kZ?;hh*;o#U>CurVL^bJq3T_gCDF z@kjh6e84`1TYN{@J}%hYk@sc}dq}9`uE)sc-vj$}f9|E_T!r5pzT-aZ8+ri?WM_N; zHF=ik?lXUk{sh|$TY&AU(0AAE^P&z4 zpu3v_bb-6IS7%C^XqzDK!2_uAZ(wWLvvEy```RKOfIH~TbV1dvhJpbL{TZy=KHZyKz@+=6XW!fD~H^3T)={Y{MC6a@H2x9`;?D zufXqI0hIVI*fKal&w(+{pMfMqzcCN)BeqEt`5AL$2<)&|7uvX zCg&KRwGa5*p>rqLHpm@#3O|8_So7&m;RJRUADkP)-s3CwJ^ZJiPZzs2{XVp?vA)|v zo8bL)J%{`ULc`o&@y^U`Kfc>W;M-yQMjpTud?WCK?%W4##(B=3W$oVozueYYVEctV z|0L%3dFZ$1y!GwNeZC=k7bWrm*_@uUhs#~R)x;k{{2tWE4YK!;BX8$%=l1W+-qjV@ z?~oYh?;W19FJ38Xa zusfr22ippEXXZG-Ct&Vx;y!@;YD4^9rPu?y^JK{G<6)lM-b@M?pdogAj_<1^R=Bec zK6h>o`)jeAe}n8y>v`PU3f+4*{)p_(yhpJ|H;1uTVm#ND0$U1Z$o4tF{>s@C@-xVh+Yn=&&Aj%f&;Fg|0Pny9 z_<4-qBmEuljc%@nGro(>Jo-!c)<3os`GdHII^i61*n_iI*hbj&Ip-htBW#`nvN_#_ zz5(n!O%vbB4BHa)L5}@|zafujyC6dk@DhH-?=Iyr`Wl$W9-oM@uDdaxvyR}~eZm)F z{9CX<_N?_<&bp5QDA3KlMRwLC{I|av*}OyWON|Nayem+VccP5Vz7mjwJ^CMP-YaaJ zbyGNL;`eomJ_PRHSss9S(l2@efx7P4d=1#(v*!(Q)@< z=Zie&IA4iwzCOBdA;;H6e!||n`;@Kcn`prS*&HRXA8jc%&jo7EVCUGwC2$_+UeDui zw7H(K?cfxCfbGM4-cv=tM|gaWJ?vojVDEuE##qPw^^gznl9)iu2Y5wye(PM(OXL~y z6EQ7vMgA>%h0k8CTfxpXfPEV^`7gva0>At6{1Y(+ctCbv*7*QybnChQH+1j9c|G5L z#qYBHJF`CfT>)pm5;KOa+r<`$_q&u~GgnK$zTF-^=dqV5z9C5H(^>3g0EctTYo7}+ zLw7FY*WlJOXOa-#gMBQ(1gz$mf&G}*UffHDzK5N&gp04ZbMK%9{dxQzO!4K&1-yeh za1Zv(11u`ex({=UhVvP^XRY}R*XVzb@h&^_*n@kn@SpHskln}5Gr;`r##vMBzCHJ5 zU+&PktAIa;=G$Fz6zhcy{IEEk&j>myptz55bq8Gctdv1mV7JNxP-GQ>@4=z zLpHu9#@GPAWBE~@(;X#JHWoDbM|z%S#zqLKF!}^Kfv~}oyR*czQsR;h5Ol@V~rgB z2+W_|*|YZLKD>t#dmq_3d~ePh$k&HQ=U6`@?i=45kV6yW*Tf7!0cOP7qccs>-DySK z1lf0?&;ADR9ytFJJweXl6*yo!kl$GEBSB8F2e`oI%x#FhIfK2sGwtrwyFOyKrnC62 ztnrKNyw;h*&d`!)f$W(vcKbKZnX7*w_rF>7xd-Ds7l5&yIWoDraEbpJ+XmUXw;8Z) zusK76T*7;N6=$BXkC11;eV1Se?A!gd;5Iw90yyLCZF6>q?rtjbdH&m2W@*v21!9tO ze0%j>?xVZ+<2>%kIsyFw-7{|abl2ipP!{Q2Ey9ebL9 zx}kS+yX^X|nyjbC9;h_Nqw zebtWa-d@n{^&k^t%^}EIWVs|jCC&k8)Wm?;|i|e3j4Qy;C$|8i_cjadM;tlc{fXa!2a#cdhW)#X25%J zFV<~|u{V8B^Z0i`zxS5lcQ+}>@Qtzez!bTq<`sKEObRyI@0j5Kz;}S{?cd$~)Bo_9 z|46*O&48Z*-902B_GoX;TQ#v(gX~`IeR793`>=QC&w(?0_KMFLOLTj7m)5o?d%hss zcLoB!8k_yB(NoxSJJ8V2zxVopzL>{-Hk?18+yB2_&8gkL%W~XGi#dhzsBiJfSNaZy)yB!YTR$nA2T(&N_b2uILB!3-S`%2{}jZATN<0 zz!A*A8}{m+vqUe@y>Ih6R~_OU?%lX4F$+-PvtMg|;PY$-+~EP;ec6k9%8;FV1uF72 zV0?}{PWU*7zk|g*?zC-U9{bw?YbST#2+q(4u=jjCa~4=HQPu~4e*IU=+!?ku#J`L7 z>A96QYF#_^`kTWI2Z?kGh!E(aC%5?+E2eD9#Eo!{I{{SiO#%muv% z-`)nYy_)}m-~N)jzmNR|n|ZvO32;|kcn6#7kJyjfb7Rw&gI<3 zyub-IW9@rST!lS^`1i7h{($cd+ZH(|#+j^rM0Yp(>L&IU@OSZ9N1wa3pB}nzU;>=e z_oe;cdF_X<*iSK!^ZD+ax5WO39EcgAzakG|^XO|q2i^BoA-jXkIsUfWe@$EtukgDs z^BQlx9QhSCro`qfdt&t+(Q{z$2{~8T8|+>987x2r+=Dru=EU(Skkz~B;W6f_(Lccz z-5M!!69|T#B{|36hXt~4N8SVD)IH-4?A;ljh!31k;RA6c_7fb?-A`W|@&LFK{Vm)_ zKfoEV?;5*1?tm%!4mNHJ>sKy82f2o4*jLC6vb`N1V=p?AF$1U=lAfx zyR=W|NU^M+U-tSlSa?o6Kd-vU=;bGm1DRne0hhI5%~w3doMwHj&bIzh&iCA$j(}k+nRlB>Qn5c zeo#EdK7AiKz5)#KAHg4T3o2j_&hqk@-1cCd4zPzFY@ah=%@1rNIGp4CX6PMceI;_B zzspy=s}1&BJZv8CrKOjO7~{vlw=g|Ny+Ge0H~5{sxZC&G2gvsK2{QC&^aZeQd#TA= zbI#q~bk6?(&pNYpO85Zw=NRuyec%pqY@Qv%IW-b&o=MTI?HOYlYK-S`*9YPb*p@(_ zvmN1zm>qm2hi}Ka#<)-W-k_V`vkkGnyMXNqcd)hC8vNdi`}O0!x|3J*39_^4H&+X% zpb7EqX3CHB1W(Sf|BSd-bk8~W1-7R(vftsVi96Y1d&l@CBl&)Evjz5r_k?9Sm__E^z#4G)cn)34~)X01Os5YtDtULl{8+xN(x z%-6`omB=-*7v#}AzNrE1eT~lXOsNdk=vU6Au=#FzIJ-i&Zb=SnwQvp}h+V+uX^{); zdvx=3;R?NJm8Tf+?ls45Uhm0!YpC~1Ob)vE+{d#@xK6NXHd6?3U-bj zvN@gA8XJ9&vA+a%XBGYt_8;^N-FZ&vHM(+%Jchjsdo$M?apqot@*Hj2oh<1qJ zu~|RhcYiH?yrNqtC&s)P_6f3jA0)&y;0d|JmjG*j0PQ*Y6rVjfZvn=0dWQROjo+Hu zbMz%}ANKlz&p6rst+#_aa7*rt80WLLZ)i5h4hMWm6aSv*J7TxK{ZElk$iKw-PP`ZI z%ibQ4^$)=wq{M7sXDDFLZ_lxQ<$3fPn7=?z;S#^SZh_xX_v}m~?9Ooo19b1UrM~ZA zfo}Z+z7)>jXV{z}#Q4rU_F#_}&L;Tm(;gaPtYJ^qwT`LGHpI z_^tZ_+uIYeXGd^F>=QO)|IM*RuBqkj0(srB{XU={@cnWoLH0Mq+MaEQOOf4)Z*GIl z+SdIh-g|C|@eb|7Iac@^bZ0#hXS}_MfZbkG@P_I zn@7L*q0Jrw=d}0EakqQq2A{p&Y9pt}3I3*u`>`K=;Va%xg}j`{{d#xq*Y{_yPxxAF zSKxW?BVhMqkNdkGLfnD54%i#wo%hx&x$IGUj@`Vz1O2x?&}(#e);96(eG}$>08{+x z-c^Bat}$G4#vaUb1m@4MtzdV2gcsPI%^2U?4tW4RkSDO`0=f0|k+;}9V@yLXa|E~w zamNWCWota0W4%AlrULzi*nfRx=-xw4to_)>H*ptio)2Joj^Bp|*!w8(=})Lpq082{ z7jwJc3wHOB$@rhKStr2uP|V}ssv~mZ8NRnQ?99%WBd_os&;#eq<#*HAA^sBk2KEgE z&_RBM?d@=m{q^A^_HS$(WWOJkF<->M_TpzxOpcsmpCUWc7@lCO0-!hF$gMAIDYAWB zfOlez9C?Osh1>=1YyrRHckUWa(9QD=?B5;qh#P|Xk-X^EEkPHX^I7WvJHP$fe+S)~ zBXoN*?+F~yOYjV?oZDgZJ8y0KG5063Z`gQi6zB~|fW83M`0T;j=J;2@vlJDVb#%buLO04;Kc&9~GcZUj!ix8%75zi(!YyaVp- zgwK7b|Dp%*0=xr-F8Wo*`2Xa~(>lzYhK{5~JQ5kus6 z;@tBOTmsL}h&uphG|wJ6e~fp%LKe=l!9M~M^oqEHzg#iSo6qp=9@(q!eu8fX0(SRf-5n@B3l_-3IW>v5_X>2u8k_MKVs16RVtx1b4nELR z*m)A*ywS*^QzKuI_07;cm~qt?ikIbHafbV?ApGU_JAh-#I;J&J1YRZ@wdJo|>A@ z>3b_4_Z-NdVAtoz`4=FBxDR_7%;Q{r*t>j!%QJQ0mN;{kpg?!NcjOd#d5+(k z3Vr|?w%@P#ds1`m8Jl;q1AYg>JnqPz?N?r6bEXEr^}VZv8s^MEh3@y*S`|41oP&TZ z1MYV`kH2@uCfNM6@C>`PbL{T-0FK!H^*cfC;tv5oF)L*Er2k)>{X4fgRSbIfY>-Y}r&AF`;u-n^+xC8nMUI6ny!`42|?{V~QkDg0G zj{O7v16}N|@Dk3xl6VLB?ag{UWP3``f6!0JIc)72vU#jyjve|H-yU5ofPJ6tY)poH zfctO_w_u{oJlgDS2b?Q~^tA2ppfxFYzhIkKK0^=-QUZ7wpcXy^n1Tn}3Sjfjw`(AHcaPc!+k(DW`va&L=5?T3O7mJ>knS1W%Z#{o&rTaYxSg(ccdkXBqSmQ>({vWlm zH{;jXGSDHrs{wM&oYwlpW-a9m+4J_lfm321k)5SPpTl>+^M5eSA4=&&FB1 z2ODe78u-RvfIG04DYlkgI^@GF)^I-aG_ddI2#<*~m*?uI_y!-)N5u6&fz3V=a(TW* zcm8W#^u;-T12wkOO)rV@zU0cY@-5D{;yw8G%u|QBrwO(KR3GuZhgs}z3L9T>?r%TN zW?yrBzDv*EBbTtVSR;W0+{fNf!}*LEU?0MB*xv}*y}beNY>7Q3*1dVgI@;~)M;*8` z{|J`v+DgRR{?#h%v4HM03Guzi@zzSPYt5}pgN^wG57;wo=G!9AL52Mhw!a$tSfB?mLOx<|iP<5S*gW@*&0PBK;0s(3 z<1F5Vb)C-{GB^RUe~)_D?DHP>?@I-@oT~-;06l;v#N9csK6!!N9t+@E|4vu4c;*4y z8rg5xofXLLz?v(3_Wg-Gfk)_TV4V3%Y#sTB$RWgD-Tlu~{5Pw`*J9seo1EjDPht0x z0{83u3I3Y6<|Fp+8}LjZwnX-h?7=$r_Yd~%-@!RJA@5-4Er_!R=RCm9=Wd;4if(T$ zwaj7u41JEh0y*+CHgnpSwabtAZZrHF>`OSq_5<{#Pce_RCZI*Pk1yD{%wL?5`CV={ zV;kfO+##Rf6xg$8oZos)h;LF0fgv1MV^qFMB@4KL=xC68yfo6BuI~ z0Q1OKKj%{P5qg**gYXpR_Wi5p@D~3&{Pq!h(C1sa0ObmHr~kkwdX8>vjXWerjjXSu z-W0uoOY~YG-_4@FME7p>x#y1aub>3(!aLf)1#q4%yaMJ-jKTH*teKG4zTBC^!7uX#-kKc6#7ua5aJqPN(BL}da#e5C2 zXA<-&@tzq0YufV=1pFn)v03LG`2x)G^Az`ehwPktxB%XbyH3yXjX2j4TLF8%1HKby zEJ2R_8F-&fi2F4@pl^WvdG-sNyICQ-vlQJK-1+r~ulR_$@3Aew1hmBX+u_%4y$sn| z?R^gSfE-WBR}yG;ORm z!Z*ZbZ+<%^d_Z?c-m&+v0}W^VmfU-S&m7hn%;Mdp$j-k&A0zL9cb^F48g4=L6npF$ z2d;IoJNptn5bNFaU~6~A!$)E!u=ikZ_UMfEKE)tmp#sz!@l>;=5fETVvOs zBQL;a7UxJ{cUHo$aE;G?&Fx!u=g#&B0yg($9|^uGTmbjuEX7Bh=Ysr-?5xE(&gP5> zJvfKIfWN}#%q{T^dU~^^v-n-+*nhEk=7elt_7KqPKoZz}JYXB1$$`y!Cu{@QJv*Cm z_M2e$-#PQt$o8wxT{x>}JJ`OR-@R^Odz-+{6tJ}dS>1h^JB0Y(6)SYlRm4w$z6)_f zYO> zkLd2Mf*;`#ToAWF{>GmW`;6ScId*qrUm3FYFZ8yJb%?(1(0b;400Fr?$Ncu%k*6j; z$CjYqfiZC3Q*0gV{MX&0JEt+$^Dc~YZrSthBnk1{9^Ie2*b~#i9cMgi?F`*~>TMh6 zbIy#|5}S4Pk6`bk0PaA$eR+TOI6)8a4xR&db`L&@?ZF*!?$G*gaF3WT_#2eS&p>-a z&g3J`Z~i32`&0HV8e%Gt0Q*XTXX}rcN58V~!P+h7U*X9)?#dmP==Qio_PjeQkspEc zIYWRC@Ckp0oO=cyfr~aUMsBg+&FBf(e}XMTw*CunPx{QAv~dT{>^n49jm_`O`?1~; zpK~S10h{^}_Kp1_mmop5zVX){%n{<>1beWy^;&H9^9+Wd2A;8x0Gqc1p0QW&@PQcj zl7v{>J{tV~?y+w`ADcZkA^!c4wN=h8p|i8g*2y8F4-#=j@dJ=klCuWsYso$YdqGk=HuM0^8}h;vu^kKh&PI}qo6 zj`3&c_Uk?~cnv2vTMparFBlWQ5cutP2@gSq&GV12GvsiI-?wMIPvonv4+L!X+0vh} z`kmFj+*OG`!yZDcVV}O!1pNqKz;y@MJ7T9Za^bV~H~Kq%XX&u@VCy8aFH=LmaS z=_f7$&SRYnyLrrK?g?-@FXGeID>- z$VrHEtdLJ1@vJfPbIjl3w^#3`h5OiR*xo$v4zw@fj+i0#XJl)+-vV|P>tFMtcjR>6 z-gAjQ1byIceYgKW1?o2TFh$Ps8DGKaES~v6_Z?Z+^8s||x$q2d-wpNL(*b`&PS5R- z1Gam3eU3ZXBOkHZt8-e%e4eqFB*ZtJ;!`e(Yhh()8w2;&aOM|S(|70&DsTr9P+;G{ z@AxjjzA|hp*fWDODY18L+~)%QIy<%k*;<~HN9gAAH-g_e{{zaw+Ie|zY|kN6AVS-FDk#krng&+cL0Ne^zY{~(*Ihi`@L z5%vyKFhozN8<1st@$ZxI>NWb5m@oXEwdOandwn>^J}1~J|6D9QHu0Gny-ht#tw!Q1`5} zzJY7Mx10ekv87;mj(25zKz47&o2LU4Z07Sm-Fd2x?EA8JcmD{yAN%)?OSpl}VT`*> z?3I2zdjdOb<~Ana3vh+acf5tI<9)k_>%7SF2D`E5-=N#`5dNnf*yAUDW9_K})~)bm zAi=lS2V1{`^S?#}GXTFzUdo6o$)JtL3d480*fz}{QHo_@r; z9wFBO8-DL@jBYQUKN90CJ8TW-?6bh;J&GkhXS#z&!1E2c?vZos?r?$Z{szeQJ49}& zwLrJe9DR$tfe&DXY%j|8zlYtSyRYDm{Pr=&?oMk#yuSl6M|>4FcjBDZdqZErO`tX~ zZzrGQ`D+i#^lxta8=$)bcQ=LSvsg1jZtyqcERjq24xc?(@9&*ld!mLt=fFJ#Vx7x7 zac7_!2mrV{giTwv5;n zx-)zcr#;Yb0#CKWb9|x@=zfb0FeYi^yKX-?CqnF{KZ}1O|910&uOhyNAIY-;=2#Gy zW3!ICSp#=xPmf>(GH|@v4#+8(p1Buz0-M|SY>yT2cSKJ?j()upWM>_ryY~~iwaohu zZbIx|n|(Oj3f|7*yZ<51H}Q98fA8Mjo%N2`Yc2H90%ALIzM)U>%RAVf?dKicT-K>! z@68(aI7N1r7N5KFjBh+4pL0Ke0tA_u0qmWeh!kR$(aHig|!ppO0uxoKmKHF_TyXFg-HbNp|J z2ka|kzZK{6UhJX7ey<&Ui@ZTLPI&^~!^Y*v7q}&-@6R`2UF$icGyP)sJ=yCBc7F4@ zQ{Rv_*_@AH58Rou_tb(+4wtc z^y9qho>M*`J6j3YA-+-TcgQEs>+d+%5#M6-taVHDkxWbWAmp$Fu0$BQJqx?7@5; z{5*>>2lY0-Gw;G$861H3y}0Qqy0M>6^oRZNW)H|M=k41awZQZ4*O}azZ{4#E{Dpsq zTwx!>_wWwrOE}|C-8?NeXIi8CmWRf{o~=X7t-r)}y%)~fLkfJ4zU7X%4e|EvJkIJ& z?qLJ$&3&El_2J1Z>J|1OdLVuZU+}lc?#G@zYajMwog7)1TOPo^4c~%wzhH3$DR3U& z$DX)?_z|37H)e;d-M8d@yPGMpb9jfI4cMR2?YTp*kk`n*uL}0>TaNsU>^|JB_u}~+ z{KM`}Wb3TdZ+7F%SK;5oKX8KY2<&624f${;C%yvR{@sK9r|9m@+zVigcKfeC;&t0}c8e(@BbL0XzV+o8~oa5g-d+4M4UY2lyzK1io1Ljs?!f){swo*Z`kawLbkqlU_V1(9&P`7m&gn3-o0~M-&|v4@7x~U z^+h}A!fg-uO6(anXV&g)_EMqmK#eVF<2x~zwyk`o2iQ0NLJry9SJ;Jb)ja02--bL5 z{uG$w7d~Q7@Yl%C*b-vg#|ir*c4Ix8Vf%uerz7SUxdSaou+{M99KR*cWnhF|zkU2e zKR(5L#wO$l@B{V*arbbJu3Tz|%`t}qY#$qB@6-Jmm%$at@HwM1f1^(n&5_ospS&W~eThIP;3=h)rZ7dGd!-WXeloPv%zH90bM>;c^z4Y)(Mu5ZG6=Cw!9+21?* z7QKW$zb58_`~Z75D}4T(t#0F+G*1%ZPICNn(8FeL_ParL-T(*Kdk@4XZQN;s%~=v) zY^x8O@z#+a&^KU?yoL+x&&b~O1laE!{=x2>bH)tL;6AuN$8+ZEBkQk`Q@Ep^^ZCsO zdCYwWMxaBN+cxejKQjXu zVyj_$&^JVG$vH+(IWvJvZ2rvILw8>HS0MNB`S*Q*oFRM8`$*b2r+aPCSJ=G=>$;B} zdIuXj!sfpHHde?b@*2JaV~~IfpMH_g(1Cl(z#h2?@ovnUo@0;3B>3I)9=sr5=VhN8 zd<8aVDxczc>txt{C!RC6^B9-FCA#$k?70g2|LzEThwTBI_vktI-#=xxTRNp z?pK?+-Ho*i5WqXWFMQvyzCJeZ=NW#*KEn2ZoWl+F6|%dYqFXBo%m}ZPXWSzkh^s+G zTtNTx+k-p&{D}LOQ*5F_Htr7p5BdSw8BW+e=MJ=6`<@u*@;%t!613>fQXr?m-z&Ot zNgMMGv75`h?sYti@jG}9M!-7GVa|$J?{5ow>YTZQFYq&Lk2?^E%VyE$dwoD|sp(wG z*0Z*@6}tE8tZj(%n#(*TxSz#4aLyXP^W@mBvvY2a9LOWwmA#v51fMvwM$X}D3~ast zQqE4{8eYKjb8`Lf?4>^9uHT3@@Q&_`8TzLu6;N5=zQ8I`2DTm2jCrQ%ZRf_^BsUaJC}V0`~|qf z?)=tE0y&YLr6G2K?0b15-rZU^#TJPB#a^SIz!0CdfNhHZ0o^(Ldu_}D{T)tWdu)mG zy_CSbKlt2%eJs)6uv_1=2lQ{)Svzd@XCFQ60aU>H8UBCgm^*`OaKTp7*C+7-whwjT zUC+<#6JOHCe#h9{i#hw?h4Tk=&zVb<=zU-x?#Ud^)&hID602jn~8&g|3N=9!%1p1TldurG6ax8}IOyII_MLX5Kcd+;wl zd$&&V6z^h@*lXCg=6!kASo_-|JEJ`spM*G@`y8EP{+e^%{q+ZT#8{)m zK8G7@?p3?}E`hc573cW<8M{YLu(ia=N6vNFogoEp=)H67>45%>>@Ix^1^5=|2khnx zu(Q1*n_~nr;NH5HI`Ed5gn9$m-4)oZouMnY#ObqsLY%qnDKV~%{kT*2c7&~4-h36Z zed@o5r@%b*Io`MOXQ=Ci)%U+@R) ziQLBT`{gO_ql5nuYu-6*{0?l9AAorh&MsyUP|x6o_zc!G`i1Sm^Ytyp{d>Ly`dZj~>fol0J7|!Nd({3E-`f<~8b6>W*FApExD)sN zjP6cLZ0^zX6WD!jL5-Z_b5G{#sO7uL@ipWVzM(mK0;g~Q+DdK2x+Coua`a*6wO4l) zfLsCJntO1rzyDuiiTs5v3G@UnXYp^nckly-=$}L^ybJ82L2v2B`Wb%bHAjwo zeY5=g=&XVK9kEBw8sm)Dw?bTs>=|oJVS6mGcjO(z&gYry%*YjTASRHrhTEsOrx)ZM zKKGu(yK~$}A9fDwc=vr|bJ^bn+4%-w0^HXIwB!!poilgn);Hf6WSlF|x5&cT&CwF? zjtA)f(BF|u&ih^;v8}Kj(MQPc`X1fBHlRTd^wf~Uy?9>#5;%`>2{Ao%;}+;UumuJF z0rmoX64Qq}*qolTr=MBcU^<>Blcj; zg!4IP%&~$4z6)><=KexX0(gqwhqWtg#(CET*b|$c;~tGopW=JYVCy+oMofoq2^;T@ z0(vkXF;_pb+;|ROhi)%RVi#Z!=D;1B;{tmh_K^_l9ks}7Y|fqIcQ*Ba9AN8Cfi};V z$j`|6jkmBfcfgsB#Mpy*zTr=F=Nh1E5A^v=oc;%}LhgYhK6|=HJ|U0s*@NGodCZ-` z1z6y--yLjDzl980{p;!WHxaoZU;iBI8Rr}Id`k}{oPs`a&QgO3a=^AnzC-Q--(w(8 z3zy)C{}&z^3o3MDtmEGq`&@vIGh^7^Ti_0~pJ3;APu8?2^XUHpbL2I4d$$LFIram# z40Iv(ar%hy6*0aSdzv6SPlJ2`1LQB*9d?{++t`!y*qbxhr+ZD%1F;FZb(f$)HeU@_ z`0PX3+;xb(-s89K9<;`s<9y!92Ki(xctu>S*%__rn{gg@`3Adl zdox#pzmL7d=3ak5!FhLJU!Gs!FOaS0eh28{4!CP$^*8VXdWLKd)^Qf=r^q>IknM4W z-B^FdG=ZMavA+en{sP_mwuZSg_^&T}s<8co18lGN$nL`W?*9e0UfcfX|3SZ-#l6-a z@g7=gyBFE>9rh*XJM<2_{q4~0#Xcs;Z*YyT0ttR|Ua$>ecjms`qi@q$5_5qmvb|=+ zx`!Iu5OyBt{K7Ya`{?%VdvFf(>UW2MGtPBH_8pAR*zq}ozZN#mx*7OJ-@++apACpJ z7_-J^yuF(92U~}KO57ZHHq{1K@F#3f?&%q}rxaU3tTQCYckmW8A$~KS^W7xq_t>qG z!vR}?Y){4~$kSPT$B%G&_tOUQ!|vOfOXLkafz`cN@4rH~U-z^|-XWXw#JP?fIl6rxiLH@WAj9{9 z{sSM-f02#vA)D_RcGrRP?qQD2nMT+Xpx<)?&=6A-que3e?;JT`+ryq&%wj(G(}E#3 z``QBc?=DN^Pvi!F3EQi0qrfKuxl?@i*p0IXcd-SY?SX_C`?9WkG}c}DKIXHS+dJ^= zg&f-2mfnyPY$f^vc0bk{pfAxq-w@-@-oOfb2Rm1azYp8v>r<@ZKFu3$_7l4KAHWv$ z!TKg=uyIq^K6dbRUUGe*kMTVN`{_db9<8t4Gc|T|%|VLooipzF6Kv57bbFOcVlHq( zp376rWj%LfANR9(&%O(5$Q@`u;!d2`JSS|{4IshhZx4H3+jXCTw}fmzEpiS!SAtw% z_xsR)ILCcd_zL7Z;O_hxV_)_^$3FoXXA)q&HpKg}&(S%)-2|KWbijWHzKrK*Pwu1% zG5-kP>Kywj@#%jB-sJ{+2~NbD``Q~m<0jZ2h_Qzw=*Z`;0$iP=ZHoSauY}W^Z4cMP zbS-(wv%#mngFDzhopY>COb!phvpRP3-Jjz==dgWPXAB?lC**iWb|!81xQ3n6J5#TW z1Mbzl&TD`6*>GOn+&y$>O<{A0q@@HlchNq@9v`sxfO)srJ91>mIsO*zV{5SG=Xm$_ zzrj|+H8J|{kUc-w&UZF|{cW&ch_SZ==!mI9%xj+owt&Au-rZu}(aq;CVDo$2st3*z z=XYW31ejMii?IIkOlTYL$2^bNJfj@Q;m(Y^0~vPxzO@8jcBTeK!pX^abz@oIs83+6T5|hR)ucYXpYCI>Ym=4|2Hw zKCowZ{SDjuCvqUy7}>M)bG*AZ{C#xo-g`=1g>8giySYo)GadE@m~RVOWM{UXJwG5j z$MqbtG4^1Mz4AHkuz-6Yp`XKy{P^AF7reaj;2d|8!S>)?O=Qkk>mJzeqs%#FV>};1 ztYdBMHF}PHe`W^s6>^Dhh3?%Pk*|HA??4I)?8f-6oHs-EeYBi+NA@znJ^}V%-s_Ih z-Af;vaTj2(fgaqUxy<>CJ%{J`cG%oogX3=(W#oNd4c*?P`60QOM;^VLsrF8f#m@6CFno31SYZAW}}H`$&uWWNdF zeYuO}>~^Q<&R43#|In>f!}j}^uR`{Gfv=Cxo%$V^N81Cu#QyHt5O*i%ANaBFXYuWp z_&tC1!Oz&8bqv0Vb#`}U-NzX+bw==)HrPGYAi;kCkD#H>-)xgvoW=e;WBvp9L{2$3 zJ;(gt=Cv@w&Gk^BfB3nO$tz%DfW07C6{oL%{ zz4?}$F@FloYrY4#58gQAd|ildsX^c3-{LE7XKQ>pHt*P-naeou+E{bggLy?q%=0XM zo5p+2_!ngR6Ur^;SHx|A=dvgIf)jkQ`@F~Zj_&@f>$%LB5Wl4qyKlgI>)@RD9e(F_ zA5-)@baN!=-k10CjO~D2zzw{@iP9{CG)7tSV}VTj*3 zD!7kt0o#jvwBH(3PciNUTdxc8?_Q4X9CL6a#|ey(?`H9yGyL{-kImlgdjWeV?$dMH zT9C~C`#$a2v&JNFLp^(2W49N5bL4=n{)ln*b;Ry&Gi(p&BhYa+zyt7!eGB`}oke|c zCLY+|H#YnA&dgV%|AHFd7yO3ZJk~URgKUpKx0n)p51adR7xs~W06dovJBFPhv~j0v zY#luo#8_tu%-4YJEPhA7_`Ii2Z1!i2JNldFg5Ue}-lzB)a+k2N3$O>ibL-c{1?nA; zci3Lg_0Q3d$lg_hzeAoNk7n^co!1%zIDH~N_7dKskAOS1AMH!9z~k-ZqRe&h8}`;(8G4Z=FH|dxAol5?xvfsCf4~YV(qI$H|H}5=-!v} zO+bRrzlk~g2D&!B3Fj`*?Wv;&ckUao?vQiJ#@8P)M?y@Ceuw`6%-4adofv04p^FUJ zU3ljMV(q2l{5Ll31-koj=l8(AC%`^S?B=$2@An^^Vsq#A74Rj*J|o*lLQD^Kzt(k! zuf!O;$G(M){lRAcff(f!+XH@O&+Dr}OTH1l?~jsd*%`OdLQsuJGK?vkv9;ZBMWnjIsPv=#a6<{kN@IZ zWOIAoTorJhd)U18YF+z#0KfJ*e(&8{?#a09=6A-LGktVx zWarqoz1pYq*06gRB477KjPq6K-fIuIgFbfqnxePx0S>p^&Q^izT@u%F{*xGM+@Zg~ z=Eymt&m8tSf}io*&ki=udFl`Lhp&fhU)D3XZ{Pu&XWU&wt_$`#a*8d(-_X+$PW9un zevQq3jP-l~tJzJ5gTP;a0X}CpzdKIASU+}i7GQyFuaC$}WMQoY-S0GMxi@6{S>tmS zb8TSnI|=bFYWy{^Z*T|Pi~e6??bmZ9`X{s2k1tVY|_t?IOsX>E2Mt2_LH^5vYZ0^CDo=>o~A)eb}_pSII=5UAI zJ(cM0ttIvW-92U6(Y3Y2IrAP{hx~$^pJNW=6KwtpZ1(NnW_Q<92fnBFBksh!TWl@< z46ZF$o4n{6Lfcwz)RTt)(`Lowy%_Ud?kd9;@U5}gpF610 zb9fFs@B6Lc$62h^an_&rXpN2>Bm7^)y@G4qHs%=J{Jvp(55RrSu}^_>{XE70zVOcM z-FtMu<8$1#@$T4tIY$#Bm$R5BC;lC~`G>@K_5rSmbKeJLY{e|T1@q|J!Oz%yTh{sq z>}829fE+kmP3-g>doXr_{SGwPJK|GNVz1yH_Wn8ko7H0TtySm=IHB8D57_&O_-EuA z4A71L#`Xfc1J4EYHSDguWBc-)b2&#KE(3Co{x`S#aF&i9y(iyPO^p31kBPN*ifxI_ zKDV&v0`eoi8a`lq&`+Fuw*G~f0{HVg%COn1c41xTF^{t)a1w~Y<{sU{BeM71+~R!) z6Link#HO&loy560=2iwrAMc zuZ(U?Va&~*V6%@6x^wvs8uHh`KHZfyDtzYaW4pjhIKcV?u~TB4X#}pjgD)TPZ?gSp z_v{hb_j-M^_#%a{{oxWnVi>p0omRkwBxVoX^;IL-8n|UTsbk;S-<%2TMq2~SVGZ}V z!he9B_lR!`+oQWZAouYJ@4dxtjvr71?c-T|TPf^W>%HNtV84a&nK~F0p0h%hnHZEUE4P?HGNq3 zQ5$-RY`^Ym1n$wpQ+!h|#v@l?fV~F_WY1c+M7H-7IfH9*YZvQt{P*sN-JI9F@C99) zHT@QL#JZylxStNYIUlk6`-eS6@1a}2MOM!~=nMGM{&&tDm0$s0i2DJ~n-lvCH|W>h z!|vK#ew%&^IcJ>3-ZyX!+qdxnzrEJ*9uAzd-vP2Y^_y!&ymxj7Cn4UA`G@HCbRlL8 z=jb~yN3MYOCHmLL?M-6418b1rI{|B$zeDyeJMhw4fg5|8vM@JL-xFL=dg8>5NEO18oRx_Bl|jn3JlQuz!@H3cbp@)oGsuK zy+h8<@$FgP*%It)*uF2sd56|dh&5M0u3-Cg2hQ)j`lr~Y`0d@Z`m~Kef^K|?>|Rl2&zmH^*F2dw#w zU%rE#x5a*d&G7|%9}nOQyL~&WX9K9PyRYsk%6;S4@*+bJj0>0eyt* zU6?y*V;y&%d;TMS|MsuX*;d4Dfp;>2&EpJ9blHA)$O&=}|FbfhDY|EUW9AuwHF&}A z8E5pI@#@~O-&F@ZW9~cRE9@z{@5Y^b#{HYin$KX0eh&w78tZ%|T)=beLtxAZc*dOG zn|0qoid=y)z8qZe`x_uHfPFO~#+%=>Z^U%itl@h#rT`h(&*Hole4UY){3F^tmteOq z&t|~BJYyf$Y{>hF>>lh(*?pO><9v(X`%Uoov90hI>cBm^3+vp$<}QIT=1zgSzA0FO z3HlBWZ~{u?Bj?@C0=1ADMeL%ssN2F4V#IER0meY5UcyR|3yob40a0Y1VN*xu~+yhgUh0Cs2By=Vg+ zwd~7#wf6T-*6vQ-Lqc5JMma@yR{L|u7hwJcNU`a!;Dqx5ImNyK4d*j>e~!BuA={_t zAF-`LiCkhIBOj47{MPke-TO1X6|jcqy#wblM+QoCbJ>?Y<;cc*7tUUzd!|88$lHO> zo9sKafAt6W{VAR`mp{*+uod_QV257gb8d0%3*H0!Hh&v#`#HzhF}??2ymcO7dvw-k zcmS+B#O|yK=L2~<{NBOj9RH^J#&hDFV~Ne3`39WXx?lJLFqd=fuou8O_LsvMcmeLO z3(SgragIHEE}+lRl~2U&kY&#gk^OtrLvE;d?N=M zy86l^{ zZDGHw0`9Qi!NwIgpEb2R=M?(}-Mr7p&bCK)F8K|90l#2~{{BG?bSD>iZT}==SRD zzCrK8eC}hTOg~3-`?LQ(>|F1_S?q5JGVCcRKtOLeS0Q_D2Ws>$&hO5#p97o&>l}bR zTF?Fyc!}O{#+hF^H%0G|y*GPs*S=Tt*>{V5kNpSv4NQ>x`0QEzbi)zeV>j0q7!&8+ z`F^eMY&*C-$G<_=$VJU`u<`Dx z0oEO2OMtV!<69$d&v9>_^Y225Zp;=W#JIDTvmIwHAPN8dH&%x3Y}(!T0oi=!u4eJ? zQh^?jjr+v@0NbB6jPZ>5AK@wb2)3s6`p6x=A#7g_=RdL6@B=abu+X z`~|eR&u}}VtpqK2#IAe~A7T5dKG+*_3n%AzXM5}?Y%BDCV2p0PC2&@AG{Cnz#-?nZ z9dX8}nJFLyU%N;T6GiQ-2d~Jw-H-4~Jpaky2`RvCW9dXX(*|v>m6XZK= zJ@A6RBF;U#i#y;9NAyqFUOk^7+k^9bA$R1m2j9j9n>#O&`#_trw(&Xc!Sjte_64xM z@1PIc=KxMZ+(}EE`@6@UV?QCU;T%07#@*ZF2%Gm+qUY$=a}MLp-(YKjx!uv0*k_Os zXV1?dV7D*hUy$vk`H1&oe)Ao$75MJpH*{w+#{vHwSU<)8h&=^g$nMo&9d0$yul0yO z;_uKaW!M?a>)((ua)NygJA2}Jut4vjkAU}ZA^s6L!DnAN`aQa{c%KC@pY!;$$2oEj z7}J6re>kJVf3+s~huGYkJvD(CV&9S7;V+or-xB9NERoGQ1`j~HeSG4xrvmvIn`a-8 z&C`Mk++lYoz5#n#$k+pDfcfuE(n_m~_NeSk={fe^U`x@>Yfby`yt#~7A~)pmZaZwr2X*-y=8h)V zrm#DigFEyEtlgZR4WQ=C5q*T+n7_LW$SJ-JoWnIS_TX9ZeX~7dvoB$OXK;TN{x59q zAxAgw5Z^0&!QRsM8?yg~dFGLrckBf$GEjjpVp7=J&gc0A-B^3;$Zu?h-@c5Wqx&X? zaE{G7#`Vw>*goBt`wgJRX1~sV58S8on9JVL5I z6TU6F{rw{Mu^D47cl8fAt9pt30r?xbCAa!?M!zBcd*eI-*?xxD#~?#q!S-9i?z-db z;>>K|@`N281N(Hw`&r!S9qbH`*b?LnXe&5l4u1i8iOpV8xDN5VF~*+NGvwZxn43Nz z&ieM*;;+FEG1>;evm@Ahybz;*h5m|u-ODW6Hn8(F*k53KONi~zbMzMe0>(I-cjE5) z#D$hve%_~bw(ukNzg;=A^XgZwi1`HtHuomq%jCPBLpJvwyZyAt&R1aHV9T*7o9p3* z1@eB_QPZF2%;WD39-xnbvF36=z9VzkySx5G59+XI8<2ty-Ml5fFpDwf zx7UdrC<6Z8i+8Why1Q9?SK5Bid+5%Zo#Xf9p53wiTg(1d=p}Lm0(t>2u_eS@&^`Nz z-nI0M&%Imw8M|@L^bd@I^_<-r8`z&cn^T)L&D)a0cT)l5U*Hj3f$$OEMFyL5G>bE> z;hZ@0T4w{yWe#`deVNBy?Z5!Pa(X*cqG!m-N33c8DOdvMK7tInn#CF=x^Mj+z5a+Z zw;{&wu-Ui!Si#24v6-s|fjG}RA}?m~F8nvcdl_Ogeu%w?+!1TtBk{h|1em)+ceVhx zu<->L0cWZ?w>{tf8%`Ur_648wyAOAMLa$)&?;W`S+IH9y*c|pIyOU?^#>)1$ghPn) zc<=wP=_}z^bbrp8k;{GhuD^i&syojRT(BL%BldgrIjnyTGGuqt+|K=e#J&4QUXbsI z*%Q-+xbrD;ADh1~{5|vs;7lXz?$`Vw#5d8QkFj5O)W&>s^yZe^Jyhr|zV3GBg0Ddy z&*HwE)!BT*&c6V6z z#;s-F_S7Jo8zFX(g zXRZX<9=yAEY%S;`>uc$0j(oj0@+@z%=ZDDla6ulNW8a<~A*=h%<@gfL+3#)^dsx5~ zc5~S4D_p_msi|j|! zx3kqD=1z%qM;U&1d_WHPI?w=n(r?f1qX6#2e9ozFC$O1cn{U#7owo;4?9ONZ+G_H8 z&U*`>!|t4ZH$U(m+b?ntPT=hM{5_5q&>SV|Tc59md3;O`p zZd^xwYgX92>ju3drbaH+ky~uz88&7q(MQ;gFFE%O*7)qj-CN7N%Gp!gpF6hS9(H~1 z)j0Q^pijXgwjAA@?rwrDgB#A;U!aG8-?#%l>lx!LkFY%qfxFnlyYuZGbJjZMaL+jy z;6I^TtHallyU>R{U^`(;s5?hb(Cwo_&VVt-n$Oyw#I9iH@@Icrt za;6Tkmb*2_!eIU}YTf_izTTd9Zt~hkXhB-N7SZp8-se-GMW0 zk-w0W5ci(oH_jd8_;$!W_^*a%oUaAHPqEK;cuh=7tY_`<5nY?L?RALVUyht&Gq>y> z+-U%9h;i=8p1d=6F#^`A&Llu~ZwYzSr(lT9+BGqA_<$|NZy%ojMRs>J`2#4ixr1kT z2?uOjIDub@voGJgxjgUwSJ+B;iCz%nePzi1u-D)LIm0f06X(AAz}nY;W616MHcy2u zhd00;jcsA;)$nzP#M_&5*}E~GA7D#heN$}qA_sJL*h5y|fr&Q!Jvir#{gv1&WPN_~ z_Hu{Kf1euUHL|;y!2X=k`f`Qc7()NlcsaGL?y*f8VmaefTz^Ddsxao+M~bANs0nmh~GeD3f8T#(1-_$}>V zeH-}mDfW}X59r2RkiEAA*;%~P?JT~-1TL{xa0)y3BYeGIY%RSN*!0cO55QiJ=46fv zyZh9agPQpCLwR$v6~X0gTu`N$dl4Y|LNr^wduK5EWe z;~jaAEx^BEi){~T{2S~;@C?EeJH`Jn!^a)C#|wG^>?u$~`MO&;2cAvwjo~qTdWvW4 z-#*u%caCq(yc6&U>|uV6D9Mx1i#zUsd7WcGOal7`x9|qt93y0B@x65PM13QpnbGODHkr%LU`QgUiMGc(24aDPfZ!NQS=;pTy1DRa&qg}dsbyFX|4YzXlV zH|W!I{JuK;6|(y<#yxbzxtqa^^XSgyJzi&n?cqlqyK`j3xChVpK1=i=y18Y0p8)$= zz}6_h2;Dq$g! z;=jcq#P}1saV7XA#vbg|pXWjwdvfRYof9*h-Tv;P|6tFs+1DFvoh5c=Ctk}-T~ic4cxoF{fu3p8|#d7biZ?Jk3mgu##i|C8+U>?_?$l{z5?Fc z960wgY~PQd<$M!jUFUY@d!U>X+u~D%?A0>`KKJ$Y!Jg1Fd`CEN?ibrHvgg*sylKaFje{51)SbsX?$JBThaEGPU4?C%x1!r#I7a7o-Zct&opxu*izUTa_;&+E5#1{&aZn1q<`aTfdW zO_s>X6FtHKe`w>31-=SwvDfD~zqXv%Pxu~tL+m^H3fUY}*nVsBOwX~ue>WcCGuBzZ zKH}Wob&mXky(5?XoWMpq@fmnQKfum<-7mfhG}K73`R2y>ov*bn^5Go!c*I|z57F(* zH|ssyi#z<=iE|v$wFhtqvUBt+mz+t+U4s?6^AF$(_Kwxp@DOy!Ur#YMoZ~*VJ5P&m zqm1AC@8eH^Gk4gGOQ>OQ##$?BZ}o_?#|doQFY(WChRyReaqgps-+j0Pb2j)oavD3u z_5io&{+zod<{o!G&vyTE)=lG7A5Z~V%-FWYH3A@V@ z`4b#wad#PViktzt8|8IkIov*dJi5zZ3Gc z?ngXhAH8#|_ejhD*pKzRQ+I7IA#m@=)~NB>o4w4zKVZ!z$e-da9nGyqO?inw#qLbja;_4*pqC)Qm%%H1z6twy0JZh7S-T}y zg6|I589HS9aAxs}%@}9>>%EWNw`gw(Y%OCa=Qzt2Zp_Q?$MXR^#rPao(3Z z>WKM5uJC<<3;3JA!0tOg0_*lUR})`jFFBjSC+vG<=bhuXpFqqSuHkp=)@k7iSa*LW z9uDY18GnMD5O0nGd4TPc7IpZU64+Z)K9-y1c zev>xtBFARE->2BGXIu0foUnQ33%=HX^)0b^ZbNJj*rz+I@a5!j%7fG2~q1K+DT@6oL>2G(ndb(S@9 z4xVSRNB6NrzTngEuf}H0F2tGKmv>*`cdmf_2%9&xzd(w;ZR5Le7fHB%x7^DXpL^X9 z@7pj}ANjgR*jfSD_q8Wt%=?HfgLl~XpulcF<}YCPd&2e#dv*kzwGZd`_n@VZ4qG73 zoaU&py&;$Qox?q*@GHJCascMBc1!Hw9P8xRopGU`Sl@N(?`V-CL1Yq-MK0~v6a z)}JF6z&T#f#XG)xZ0@Z z2(#Gd5LsJ>?(8Ko3A+7euzNos-@|+Gjo+L}h<#`)o|uup3HD!scTiyWXS}{8dI04N z{`1407utW&JK_^y@2|kz2mJ2A?>cE=f&KmFH~)h3=GW&O17!Eq5$AhPVDpWU{YI9s zdry^TF<%eenayP{H9mW}!zMg`!QQ~`!yMaL+--y1dtYGlYzxYdxbro(32_-V_nl#{ zLyYfWb2YH_y(jN$b+ZNJyIFh_=5RiDal)RV=h~3%=@I!EG{pF8L%aufSlr_5%^DBr zl{Vxa{Dr-1W8ZspYxO~by&;cv?Q@9ji!;W$lTYLwwm$MR@a`9|Z>`~+vk$-p-Tpj( zLN=dgyQesdcWLkP8aCG_K6^ic2H01Q-JSw)e)k_f;+yn-I`9qLi?AQ{DYCs-&t3Uu z-rxiFzrM^@;}7(D+Y-vZA#+aq!Te}TSsV7)v139tui zH`Gz@19Qkd_y?c;X7C(75uXrShugiOySovw1^y1-Jv>AY$Qir?&fRimJ&SMpjdQ}g zF0onLU0BoJ;T+aJ2j=U@=iS)X23rE0vqiV(3A(XIVl&`<1nkQ8;C@ zm?B>|pCB)QJ86h@Mti&)1MCh+l<-rK1pX(`7jxnEpiXsvo%|K_1%l=63--j++B&^ z`sS(VN3KC;@b-6@$GgekN9gWjj9wsz-~l{h2k-!#+jlZVcm8r7d-#Ua=le^a;@x-f zdEW!#LWntPq;)oM?xdv$YZ;etwn6ql3Rm8B?JjnW+#*+(ICF(u0DGxE;yu*lcZLP< zZoG>^KfG_X2dt}{5!1pyBjqae9Nyh0paY-4JEyhB*h_RjO^AD1!r7C#4wv|w<4i;J z5uA4#IIG{-Ah2KHJD~iCzatfA+(}PAxqT;lba(Ir8Zf}#T_WtwUhI1Y+);sahZ*Rh zujcU%&1sBv&G`@i5NYl}-EU-#T+0!cfD>Fw-0d9pJN5>D4!+QR^X8r4yP}(`#C`w+ zxD4Am&)9uz_v**k9o!?_nHbN95a0F&-FM(SveqML@YQh6^#cmH7Fpi)?uGNfo)^HK z6{rcQd72-V<=;qnZ~dNlsI>3z7&+` zE&1G;yg3tXq`BRn{ywn-P=}aP{(#+Z#y-RSOmgRPIKM*;a38(_dnxccvojvSFT7{J zkku3M)nIO)*3E1JUkXN_`x_7iCrX^oRe&cJT_wzrSdP=`hcc)`{{PsEp_I>M%oH;S}_YCj-bkX170x@T#we4+(e8BGvH;MiUZ@jj>*xM25 zey#I>^gh0hEx?=mN6O^yti5$E?{ zhTpwc@XqYM&)CNP1LpPj#<@H5I9rQe;VYhq$G?GhPR}Rg5kL5MbLutm>;w8j8(+>j zckOHmu#X&Rt^&B533wyMS_%Hn{J(t#Ip7RSc=Mdl6Z9Q85uXBkGVdOEr{?@e+;|@E z%X?YDeZ!r>4BolTbwxJd8ytx7ez)L(7~hb8Bb==ZXWcsRrqDO&?!}(ZzM;RCSd5$Lyu_v*d4zbi18cW6J}fiwQV-Fj`}K6-F9IT!fd zzvna1fZIK|XLxh?-F9Acx12XmN}PG8=;n5(0oi~S{+0)>2h1~`#~r&H=kabzP?68P z_S1pS(Hr_Qhu^CnwmC|qd#aIr;>@4Gx7d%^dr+b0aL(>Md6ygbHE;&sL>Ik)AA^QE zKX4g(1)L|)k3A0OaSuJ@D=~ima`bof74n2EFR}g=pY^O?qo+vE*^B+zgYP3ln)ghM zy*XPWsJX%Jg9N|%hUhK*^|94GZ-2&Ee~uPeHH3e?Nd-{IVuHCpUX^cLR)TYrf(UimPOdz&C<*aPGe9MR95v)&0g#NNS| z0#3UjX9qchv%duR-O%Tp_EBIr=;r#m``l5D{y^MOojiZGxjg%7tUhF>gp+?pcLtF? z#a=69z-Qeg#GO?5y$^eEo^PbReS-itaN5_WxR;kp{5|kaJo6X-NZb_oStE^4KojUk zH1Q31NBVjoy~O<+lfeJLxhMDLEGhOD+xyVhz@NYh`x#h22li<{_G+Cz`VX9Yv0u3! zb_Y8}KEgHBtk7?>BlmDea7Hhm;$7&kIeUPg0`K93Z#9oS)#$z-d$zA%;0_1a=5=pf z^aRM+pM8wctz)hX`GY*-3-shH@;SCSKC$iL4X(Jv*ez0+TiN3GzU;-C2{8ZTJmwlB zt?7JgaKLu95qf^N1GTSDvE~4Oi~Wi1OxCqd3GaM<%)J0@h132qk=Gxu4 zgxCt7oNxR=-zB+yO5_&X{PvL{t#3}}Utx4A-GyE5^-p}CjU%h|EVPBDtN@6_YE}b)j>jQgR z0q1d6=d*TAti73Y2NysOE(amR-Mtd`fWMLh*4knhz@9{oZhiOBa?U$i%;UW7YY7r~ zV|;f*WI2y|)5LlSoc#tc#uwoAIr9sgcbXy_&d-qM(YBu>@)`IpNAuX9vpCly(z|ds z74{o3z6)pcyu0#KA~)C}#CKH8(c!$OTmK(1);{|9j5X#Le}Z(LDO?xXMedMWWX8F+ zA(psjxE`E+$T{!dZW457njxJhBfbHhIq^v2>kxNr9`gm_%;nu~;9iiPU7_FJC(_yf zeb67M$uWd;c6;`_as}qA$*tes-=5;`PDuBY5~F{J?;YK_mmq<&meQPu3o7;kY|)+P zfF7RW&em}D;#*q7AHfx$G3IQ^lQeO66?zHR1$}Vl%r~+2*u(B!VxKvjdF<0W`614E z#^^0MG9_Fexthn{#FiZ4&N+`gxR)B4fEL(OMO-1rS$ANb6Zi(sZ%*g5mfwR8T#5dQ zzCm{%&R`ACo#8!`!sST!lp^=Q`zrCU;1ZY1kB^?&fJEW6o zzr;5`M$gcl)12xlXRNUW#xCIWo8yYl&jj5*OML%6$j$fNnhkmfpZjaD&7-{}E`&Hs zhEH98iZ39&d-J5kCHQ{OSNPmzfY15@n&JnB7taOMtyUU-kEkI32E0X%{LXYajv zj2k0U&N#bss=FiSdI0uYfD!n`cP8cmJpg0vy?}F0YmTs|=p|U9_kr`+-*FyykP+`p zHF6CX$Wx-*SB|ttYd6fX!T$(e;f!@p`ugbNi2j2wK~ISH?%j|5dxz>hq)^`xH^f&r zvF;n(Gn_RJpe4sUoOP^epDDaZ(A|eU$vf8`*z*D|-2E?b30MPjcvsHUL%Np+Jx5-# zUxB$oh`$44&eU+;^EcAIw9g>J*Ah2?OYs*-=gQ&jeF)s4J&iy`-V*7|C2{uWxgp#V zZj0Q2PdIm}ZawR|SLg6+}i=p_tpUK)EyQ0tuq5}@ZM8G+^rtIA$o?KB1?R}$2P?MIFq0Cg%JaZqdo6c~aU1k4y1Bel_h!HD zIYEEGXRLSh4||6`0LB)`Gtycu(jD5Hvkc)px5BQFQ(*r!G44fORP^>OQR@f5#T zk8lZ`cHq1@d-$)QMo;n2K#t#hC#18uhmsimp10>7*rS_&2XDR#+uZi($3FjIr%2yK z65?znTn5)7C&U)One9hjZUAz0?SymfonK!FBhH%c{`?g8(vc_5_xVPw^>TD;FYvvi z>kq)Zhk49t{|zXK@r=?w+@HG~A$Q0jaAx@qn1Vg9@4)#CpR@KZ@!uZq+FU!N`T*Ut zf%?XnJHhTE%~2pLVC)!~5Z6K4_W>D@)kpkoZ<|=tv(`25CqDOL@2}_&a9{YGGlY0o zC-^g1T;lJQvCh9CrbfDl6kmxDDv3+V*}#x9?x#65`Bfo&+55wZxq8d9FfF@ms5p{6RX$53xCZ@5|nQ zfjjWK=iA)C2hNu0=5iMOb%=MG!du6Fx0kr9HL}4!fV<*%u9CA$d17mP<~(^8-Lo?w z*F_rZ{bfjzz}Z*-LZ9dbTtN2V61WP~=oPut^Lc#7at&}N^#`#?=lBBN|CG2An5$~y zZ<#%7AHe7m<5x&`y9XWMJocL+Go(Gc7i*u$o5`bF?~475z5s##hRDr4)^;{`Z|y14 zeGHHFev%+tjbG?G8{=b6d}R&VB%%e?*?~ zw@-0K_v6lM&?Pn@#+q;N?xPQsJ?zOmzQLT>4Z1zA?>=j#aLU`x;jQ(~xjlM8Y=NC1 zLx{be(Q9}qJY6b?E_(tu$C7&Pr^t;?e zx|c0-46f)I(zjuM`p#gA{_lxdklHo6JzLNG_Nwn2pLN~0a}{tWd<6*jd>18hfiz}_ zw67jAna8vCb$w!wff)29z5vV>;5$gqHcix>(cbLKTK4XFXEUZt{05Y8&TY+a^cSRi zY=GbWJ-!FHgfl5zj$NTU>odBueS$GD=6D3oWe(5xvE89PdlyZJxrT6oJn!%$Vt$a$ zVGU!|e}KK(e+wqWrJSqL?ZMuiqY8H(PX7VUm=XE{IHxtdk1ckA93b6^eH0(@P0TLw z9xD9L=)yS9xdZbR-~a+Rac*}(2M^d?a3$89HNHeiPxg?)d(Sn!K4Pnv$R%j)n|OC+ zZNIa|d*{A`2Hm?^5xWH`aSeIgQxAI$jJ3A@Ph>*QfP4qmSzow2`452fE}ZEIGPsjI zJ}ojM-W~d}ws|Mm&e=5gcNwDn0@nfh-IX&tPxy%U?~bhTh0O8g=oS7w`kB1;VV}O$ z1YZZ<*dwxsy+ayrjSYANHU12_2JXbVQ}jKsS9j2a_?FMa+LQ6-Gtccjan|+h1O(u0 zL*(;?Je(_l`FGe;q&xOI=58LbowI?n#{xKC@f7cLhVEW7Y-3)*FM3U1<~)GGCB7m1 z@a!17hrU8Ob3*J9-W(~MA9IY*!$<64gg=2Ru`76OdpD17XN;W!-%p9(y@&8$4;glg zUE=o>u${qov%JK-J2+={_A12rtYO{`aEJEge%t$5cjdVUxDnW(JBvA;$NM@0cj)}i zYrbbN0b6|T%{kBb8|oJDo^_Yz*@G6o0cSY#9p>@-R$$9p)0vxwvr2etbuW>BMBa2{ zj?dgJWdcoTbKgpD*|-c>65ibGYw&&KaEg2F^aM-$$RI zJNE+p4X)*U`w`!WT!rmDrk6NJ51EU3{G01}=WzympMo*)-8hf+wwHJ(OZ4}T_&37y zUC=b~ow@gv_<(&N-Z_oA!ln3*=;6JgAIb%DYyBmQMynFJ~pnE6Yc@O>37-IeO&;y+F*suK= z=Np}&4}pDrVcUax3b#Z4A+`5>n+gTO$Lnj z4*Z_$H_r(>gCC)%;0NBGTKcSj@rQZbgE z!}R{a8rEFXYPixE94aX!?#6lz?E||q{!#7zlt+8@h$LU|3iE|P&e_cd0*h& z{Tlumc)#woAis6|ZODwl-r~1+{eCyz&|Ba+_f>`XR))lz_x6_X{Sq6fbtcB0J-{vT zyQgQQbDFOJ0lSa?3U>k(+#|L<*uV8h==y%ZCw}|1U;RJW?$H_vyqvqXZU)}K>l1lF zOTRn(?s7w{Gg@~5=bqm`V%{~nG4^J@PxKt5+TcW-dIRTf#Q^&ZpTXImyDrg<)0gA3 zk1hIgjt{$wUV%M!hJ8do6SGI^v#y^5{vNiuoZlV-cmP$yOv(^*xtH#o^4Q+T1U&~= z_y?r9)a}uJ%qRDP{f%ut`|(b+8}ba$1A2mV9|f>KVZZf9yd!t^1$x*gq`3mIIdLVj zy!)2e8?Xj%aPILLeFW4Cq2bA__e)s7+^?lou zyV~OmA@1{t-!mDU^-^Sobcg*rmw&{1?(-M@c}^Ti$Z>@8t(!-FiEWJ?a)DhUZ{K4$ zcW!U)xWMP0{jAWPBLI6gzKtJtyyZ8nzF^~8k;8q~vtUhbx@FmjPdvrP9 z!r%M}oHf03d(r+OW{T`yqO|5WKF>Il{kvOdnt~?8x04fRt{V86&0|jGNj}6~{-T@1 zoJV{M^Z|GT0nR(LXYZy5oXOnIS2SB1 zF7dl)tb5*Jm%yGpmxI$syq`02i|>Yc{9RaJTXTrC?=HMEHpGt5yKv_7%p(N}Wbb2)x@)kAiW)-KT9z2_gX1G+Wkr@)<6oGsyZ z_}8F7_g>tK^(Ju7*agzr3t+z`T#oD!(?>S&_S=J3*7RuY79{vnu)sG)I)mrz+qwa` z2lu}rCc$TK0lz!9UJ~%b3;Xwc0QFP6cYAn4e}`M*_jk*gtv3V@a6M!W|2O*xZVQa9 z@Y&A-nF4F~@x4AVD>wmt6@6UMC-`b~d-?r{G4^ANIrB5pX3F#)?F_KZwIocoHu1n#PVTN69H z#J;R!ZtvST<0ZE-S3J$Ij8mQXEG-iyrY{>+nwx?8K^Gt zt-r!OVw=<4+By0eSpfHEPY?L4Ss)vvJ+IJPWbY~Fuor74z@9tcwjbVO{}OlUUAfZ@ zzB;9LP|-5;bo znxdB=*S^FvSM&tk+UDkHY!z2oyf+?)A|OFZ+1Zx0ucIh=E1yx;vvb8MorExwtHVJzJY`o-;w=)A%C##qlc99ZqDclQhyub{N^h`4)!1e&fvakkb`m_?>kWc?`}45 z_T}E)k3Co`Mf#3ByTDg~5^g|@ws-e{w4QbM^Y|7{#9zVVo%24d@q+XXc9C1)PT#Su z6Oi_*G{*eCOZ%L_Y0>zMz@a6Ii zGGI&&?^)3TXZFsQ_;7!7B*Z&g3lG?)rHiXL!U{k?$3*K+ljrV1nOy z?b{j+(i|Oh?<{7~UumLHyb$iI182RT* z+>LYD*I%qTe&A{_A;unNNN4ZO?|;9LrDyP+iTech=T5C-jy3Uq7TCXc{`ACKf^R~+ z3;phCCx_3OTBJFZ6?}nQBKzR`BhKZ_zwquRAaD1Deg$8kAa)AegSqYfwo7bhwa*%O zE}i3(LuSagr#Ql5YGU8+!P6Xq~y7rljZ#uc_Z2_ec3K6?njk8{*d z@jKfzafTr=6QIwW1<0}O&mDOOJM2KbaV2~Tdhp(H2G_x^u)nbn*zVQ7_5aoH6Laek zz6YNZGlDD7<5@VhjsL=m@C+*`17vQWnLY7GPJ%sOJ z=g1P+XG_oS%X8*6W~uK&PX4WAfBH*g59ut?uvQr(X-Ax zLa*RY*bUM??6JW24&19dFkgbdi#?vlH*0QlJ%9;%f!rWl;;zJ-%lS4)u|q$@xB9@( zM~rQW_fGBGew@vkEnGl1cTT+Xb%-~Sw%03M55K+YYv|MZ-r+CO8Vwj=dw!4fZsaRqZFjqXdjam#TqV8}dV*e~ z2c)qXoG3sJ%%|Nx#Wxk2c+aoc3A}e}t~yZf{%?mdc|=R>mh;|?cj+AVWsVH{%-MG^ z!e0XG2l6OKz`G5^_mKY9)x^3(`P&D*Ku_^~f+WxbF-thlbuMxC67Gy`Kh89UH>QiM zIkQ1;kTaxQOUx_qd=3ADZv6}dYNb9+m!zF%So47tcKW&J4+_C+1@XzqQVLPYy zp}vF9!2r&`2@OcV5Y9Jd?i$gzQr*)mzdlx^zJf6!ya^XC(tBu}A-tzG~R8do|A%{EN;X5HaaQ0}w9k@qOz`Fy_ zTHkwKBYj^9NU?8u$!VSjJ4Jeae~I%{$Q)j7h+ZIX=c%*Ct~u*H&d^Jw^Xh*E<~H7) zE|4{`33`stoB__B{e8{Rx9DTw9+tqf<_VlL-n=JdjcqSV?=J;s_zF8|Vt(I%vzliJ zZv2Iu#Mzg5zV9fJJ>U#eFaZz1yfYBU>Dvu~JfH{X{wvR^<3C>F3|BaJ*AS=QxFxon zHCOYP$9>vg4L3oKfc1?D=mFVB4&dxJN3W@~Lxv}SZrv$%g|vV7>f17h@5+64fS(bv z2gdW*%MWn@+xQgug>)b0YC?RITVlOi^DfX!{MPY|K0o%@z?sAE?+$5<=ep=2#P@o| zwx2UHL8i!bPOnPf{>Gpp-q{1bTTQq}5J1~-7QZv--+>m}dY{1D;^h+G&H#Ugp5q%K z&HDwW=q2)i|B9^1Ypv^3ybHO8ya{pctOsxJ88U%?M)wY~d3-0{Q4OzrfHUt56xiO| z0V#&)L*$wKGvorKaL(-xN?<*EY??Tu_h`S)(FgWzUyFI1`I9ploc+1~1bd6myELc0 zPqFv-m&iV_Zh_ykey6Jz6HE}TK^3$!0CHN zCU7PG9{L-4!}%6z?E{=M1vt;xi?ja&&e4T)U(Vrs3i3_-ZTpjt#CXR3+`T(bke->K z*MZvjt!Xa>zM35FuO?(e1Z)#}_gLY9p;{-}YqQ6M74bZIA=}-i5U?xHk|u z+k^jxH-Ccs%T=RK@hQhh@7Oc$)j11ffi!=Z$M2RqFkb0SyZG%VL4Q$)|9y%v0lN>5 z_&lS%M0cO=!ToKK9i%-~poeY`kMKX7aYrrjo|Ch#HCyB-IG`6#u@~cq*y%j}h8dUQ zv(5;8hfImF))^FVBRJ=tA#eGS1u+BU>_Sh0c=T7|_s9fpg8U*TN3W0xzE2R)E4VeV zo_gT?9%&EWrSoloc7^ZCna(BVc|cC!o#z=npijYx82eko+1nW1ern*|cvseMKH}Yb z*4pyUUxJ7E-A2kCkw3^=|BT>S?=7LH1HNaV?R%==666`!zw_nzy!Ttb@CkO^kcU|7 z^@%Z$_6pq@cVGbWIWe5^9hldC<-a-S+Z&?qh_RM;yhOg?GlzY?Al*kx?h$gY4HB@% z)}P>W-}br0b{Fc_9M0qZjCIG}!4!W5oTEUx;{x7$9-z1I=J|*0W7~)QdN)J(47q`? zscXIfr*04CyCS`VfA|jA0p9|7hJS=t_OQ*X&o^nGTi_0~NBBLfT@%y7t+1VIF^7Yn zA+2NF7?nzQCELI1*eVC^HeIoyT0E8=rF=W;&tW?%rk`y8%^en#HrxWwOc z>lj~?V~@X~UKcq6)rDS=E&dApfD}6+M%_IZ#OU{Y59vAQYVfV`_0@?N_IA9Gi&%R% z?uWP%UD#_6`xV`N_&u>#>+G;kNM}EP&;oF8`rNg<_v{jR04unhn3}rQbVn=rf*AX? z#t!?6^xpkjniHpV7Jcrx!QX*5SBXr>cZ=nWG2V?nV<(_O?_J{E`5tTH7w{e63=O$c z_$l@a_7Eg+;qH5XiaS|g+p~99ng_l=kMYKu)Au<-K7$Zq+!=0-?R>{O@4e10@qCJG ziG4&C#3&!<|J7>nCGgg;*96<|^Ah_9*`3E8%)6)@Kf-w0VEfAM_-@39BY#6Q5hqrY5N{O-$oZ}(Vpq}VkxB)}MV zV_x@pKqlC&espI_(CzC5+xzaiT;ZIgD*vjgtK)M-!9heUdP0`zdiBJ z`i<_L`i6FJ6?Ox^KzIHExPN;vXTZ*p)_O#rz#E%1vF{eX4e?Igxw>cnq0f-op3g7w z`{#Q#r~MC+?!X@0ZG+q+^_TOQ$KKtUxyz3j-^Jgp0KSuCayyEkO{nR!1pX~U1xHh z16TrUBq6@#KDK9$aK4!(@)6GS?x}-*!vBn3amJV$>FzUlYuShAo!L6})WEw(<8pZS zVDG=k0RDbIyi<2k6L&`1%YgVJXYC=!_S`qHA7^s*7UUnXzX0cb8{@pn0^jx$ck27@ zfLC~XE0Gy?2rLINe;NC%wZNHvxu-(TD-G0pLyZ(f8*IRjN{MB~8 z4*cz|h?PGh_sAK%@70-vdvb?;d=>lz>5fa{++A~z$&gdHDL!?3(eF%;@b))=v*#u9 z2U*}V?uzb=`kybc2m3jJ63+APQvZ#^PC1h!?azG}6PU3(kF%cP{dgbt=$qB2^qhGo z$R0SN7s%^V?B5wfh&=`PHFnE6cbdQ-;oR*4xdEOVBHf?4ez4tXORPQnvEIuie$R}r zId6~7>kK8FA8YL4+>iaaFYn6y+j+dte{jy9KE+!0r(I(wNcZd8DUiPV>cX7}>?tzA zXAeE}+n(Uj}TH6|%+W-U9v}_V^y(qQAr2kKdbL?3^5$jG5msqzYZjC;HvoB|Gr{+qL#!i9$K6VZ78FL2gDxhP# zm$r%dkMINR6utt^=B%DIPZz&)-Oj*yegM}2&apwJ$Q>|eAlAFHo@bBn&TS8mK<<;6 z4f+DzePqPj$1|L{mgpJ0bv&cboO|>dKA}z*-S5{DdBxW!<`Wak+;IL4JHhYl#@M5uI>hhM?RN*g1@0mv&ip0l5&HoA9yx2^{9j)w z{Fc~U7=z!LTJoIH_i!CB(g$X9-U$8!(z(BZz0|}z%g+nO0M-*C>* z!JZ+9ETsy2Sn~d_8n? zw4iC?Of}~+FeO&*jGhDE$@wF`Y45ZL=U(hT0R30E6t2a7A-Ct6fDdVXXWzm1KBDdp zEBra!6}?Z4vF>*UQt+&tQ$s(xcki6$Nst9TXV@W&C(gtBR>w&1GDElD5?_gQcb>KO z7P%s(i!^qj1kN(Sc1H7W;JiC?SnGt;Z>|da4LqQ~D&af8J_pE}`4Z#|TsgPC#5ZgY z1+d=|G{kwQ_B))%do{-vR85@8I*<6wu|`gi1CZR$R>&203UXq9$mf0D&dlR|e%*6S zjlrkv!S(P@f%A`%IcVU$vxdA$LmxK*!I$*r(lD=pT`~l`-pR!^9|n@XvlTIAIR%0?$Num2Wytt zpP&nOhFc&#XWkw93Ymky{!4s)Y`;g&mlER+ywee|_ba}?+}85G_5%F@6!;g|^*t_w zb1y$1G3Fn-7!y;`cY(CGN95n!<9VF9#cw@-3ye3XZ^Qd^_JCjRFpu+ZkY{{rVjj^~ zAYgmPw^`AxJ;QGAx!s#{dGFaJ*6pF&uRFMcK+F=K=ggfWJ79-y?KLoudrQcxbe8JE zUBkQA_mB8}*W;*v_dhfis_b@m$-)UbonNAb)y_=j}IO{}Sh{`pmmPms=r?9l`w|oudx% zduD%yHqzJ$F%3TJAHV{%*x5YJX|LuliS=H-kxOk5(5LfwN5xaz&kOt+-P-QjJp}UU zcdzbq51+twu+5hteIGTPZ)=CX2E_#*=e&2%ne!PwLC@g>y8AQc3VL7+XAeKe|0A4*0S&FgFK#xbDr5H_E3lT zF0}36-2T3|)C_Pne)nU4&SOs{e2yF=EBw|uf-QEyc9*leU6aSWT{wGoPZcpavWs0H zAHff~?bY8G`#-{2Z-{g+?s%ZH`xxMRL9Rhdyyt&G z?}IpeBYE^5(s{jSYd!<>-Pvk-ud+drl zUHtY^gA@ES_AB~dohi1p&F{SSw!`*p0!r)yws-Gb&Yz(h>s-d@Prwz<9mu(p0)3_A z%o$A3-QxuQo7f&)AH9eE0PkDtVcW~!+)MOt;%agZ(Z~AdF~6~I_&l4CBM{R7_cnub z&L8xDAR)F1ac5ifcYF;n)>+)$kuwWW6Sqe?M}@D&c7N{vgzVsR)-lq4axg--W`G|c z-Bryw`^wN0bZbp7@r@a8zcrk7yw4%{+p{yduM}9lX9)KX zyF&VLzs8=C_HsZM=3HLlcgLK!c{dM z=k2`?F*YI2dFnfFolk7{zw`{Ki8rRryPt&|xx7nt^O~*rM@~LF^qey*WZT4=4RVD2PHYG1 z8TaL07V_xsNxh3)BQs*0;TPW*c!qc86MBK15K|(}`HWtHfX_ah-#GjAY(gIO>IoIw zJFAg1IOoo>okzGo>)PWNc-1CmNX!FLuEJlU@4y~xfM*hPQ(T7N9HLsrxdk0eBnFOD) z#?|O`Lk{--L%erpkM{1og@AKs@;hv69MP}vA;j;=H+pvGoz1i69{}gC!6W(`G9bMN z`*8jqw*BodanCu@c?!^n|0S-A49I`La~0>BrrIte)HXCpU1bi#dZ$;Q@B3TGa0bY7I+`dZS4bc zOiYe$ZSy&64ZP1T_6&4D3-<=>sXveK`{lmmbO5Xv!MLLh?bIv;B7+J%a z*E}UL&Snqt0e(YFh0Nf0=+0-K&i4pB=PVOY5Z7>S0Kdm>(JNqI>$^WgHsRhU`U;fz z-@zGdE>SO$p1*#?@BBADiFiE&+XE>v zW9$jKwGwRSnqqgsGx&smM7qoVCC=<4UT zG(Nz+!#Up(+cR=Ibnh=9OOU{YdF(@)*kIE3GrNo^vvxnz8P>Pb69hTJpktO zj=Y1wnH9D(HrU3OyVP7Cxta3N-ufQw1JJ2`9 zegrkSD>!?x=YVefE4KR_0e7)RTF2bSd7N>GZI2E82H?GvaPlY5`(L7y3mS6GF88+vE3a0l&nb;rqn4W`o?rS+ga61vdir_5!*fz&oS&m|>?NL2u#R z&2%2`+&VS<9=jrbhp&TvhO?gmwmIF4`Tr4foX4E*sRJh(^4gC(48WS&zr?uXzt20q zB*Z)L4J_wzo}8F9`Vr|oV`NR7=biTvdkfzM?l3_naL$$E56A-fEQj^J5RjHi3Bj70B@YB3n>@#5`SMQtTBnz&U@* znFr(nobcJ#41Eo9bl=MWxd1!h4ql0KmV~p#CEk(eE8y83>{FBGlM(KbneZ@+JO?_!Rk$&m1{$ zxBou=d&@b#E12O6#9McV{zZ&&6`Z}D(LHZ`1Lq#yr+e=JXL&(3aAWi-dP{8EMEkZE zd=-4f`3&7N-mN`8q8CWt{Rvc@%ZVAn@8B2Wu8+~3+qdpq?$X}u$$m2A8=P-OUTH7S z=+^YSbNbPKLvKPn*TLugSxY?ux7m>^IDG|rXz+c+@8~Pim?Y3Q{tj`TwVr*ZNayn{ zRPde)*dy={f5kcHwD0jZ*?pgcOm!jLheSN~2 zGlloe7q)TU&+AisWA37a^FH4|OWq^#2V#5|Ia~_5*nfLnx3A(7YyN{z5~zpiLv$+egP)n8-9j#e}S62d3;Zvb*>8JPw~BVi~&7joWuGRKKYuw zA~znN?_hwmSKpfZ6xL7RMsO7}L3TmV=J(TnZ})_Bk9CN1m}7K_-;)D+fcI_+^oCgX z@O+6o8zY@70ru%ma=5CwfA@%&9|Loq@jKHB*qigpyC3IS!vFcLBhJJe@VmPV{S)-i z_1T}Ztgzir7unEnhV&EW@h;qrceX=50_)%Q2-k+ViwF2V*y4Kx=5;>_{tUYye=ru# z+8yBD{^qaoS*OD9{;x=DS?3LVi*7$H`dFR#2Hy$Wnf7q*&fR`~us380?{}xbb`PHG zfCk(5>TbV~3AtA2EwYa+z!2n|kxP*y>>cooXT6K$66bPGWA)ifg6;gm{=J))JoX=u zUF0iqgG-!YhI|GIoOAmo)qCg%czbfUz7O~24%8EJWk~n9z&}C$VB7B)-I={pV=H2v zafhu>EP%Ulx90yww_oj*+?*QNJ>==0!x{hjdPe_#if=2&ABe4xw_RM~o2ddh;eLp- zzm}K|K7T`&*w)Iy4Bh_J?c)ubL0T&`7dLTD_}1@Fa*xD zL@wu~MgIg9P)`D9;qAr!|M~1|gzes)HTn4OjpT6VJRr?6AudJtO-;c+IOqH3OoIK2 zt-ghGh78%jxj*+|U-tiqbl+Q~C-Ww_72Q| z-!1$6B)$o;mb3XDoTYq z2kdb$zkd&iQ8#9Q&)%Kem?3CPK+I#Y#oFSmJ-p0Vy8`2cGCOKjtud8m#w=McZryR{$Vt@8jHYiQ04E}1ia2@P5G47xVF^}`+^Vr8T`V^EP!S7kWBhGBC9{N}x`U}#Yd|%$z3c1G) zP5cetU?0JWGb?!Sut0jo8acdu*i((ZL(kAX@0&KSJ9Q6T^lxJ9tHx(fXXFg~h`)zE zK)1&nS%3h3;74%Yy*1vk-;o8#;ZN8t^#Z!mI@Yk4mKb}t--bBP8>c@(CdB&=jIl-v zKStV53Fp2&;bqN@1%gAfM@hIctO7szd+i@8sx+}=Mp63 zNbomJ+=clQ^zNK_@Hg1@F~i<~481iT*tkOoKzw>rLjh)RYh4hXB+!1>T zQfza3&U<^uwwDC`=0|?PxetF6eS;(8K^5X#F<18z`}iSN+g+HaKt2L%{&3bDCDJ+U z-(C1wVY?&yDUe|v-?4qzv;8OV-lymG=&wlgl%NT5x7MuSTco|VAMu@@&}(#Sm-D#W z0p|)h_v+jeWJ8|m-LA>eVi%k-pJ%nbn_u)J(puV+OXQ5(pxf^ODW4Pb1-z#juQR$w;%qBo`jgAgB^hNjPIg1P0ZohY##4p41e+*T!VD~72FrHhnx^+ z{Sv=Bc(~gY{uH$EYq*Ns`%C=0c!oFDdoYLdo1+8VfqiG3YXkBU(ZV~cdkAnlVw^2t zdrt+rvsm|tuR!;cxj6DE#i1~Zy_K@SRF7W93AJKgi?zbiO2bqEQkN6fo(M#Z4 z_q=a!M11`b@3G;GdtLzd;NB8^o}Xc_;BUPnozorcfVGVc#1+{8koIr|=6lBf2Iu({ z&ONvCWtB4Hj_L zw_m?IEoa@s2K$E`p6`JpD2el4QgrvxL7Lk>T5NmY!hNB8U;51N4hQfRaeHKfe1Kbn z0^54uoY8hR>(1oiQsA!tfdky0m@#k{&NG3l19PE|&;z;NIkUxf$NIhF1=4-o49TV7-!j zYh;E@fjcQc2G?+|ZQ^f}^WWYGF$a9s>Bte=8i(A%o6o-uJ#1%h@M+iBXY3ykpAxn-F|X#L|*}SRN=dQU(r3YLmF?Kv-pne#oj#M0|EbG9(NR|Szuc;5H~}5 zAD?g?^cugloKw9bUwMgnUy%BgJ>)a8CdOIL0E~cl)(~U7b>02rJf1nC zkJ0@u>D!4C`?KGGTw;5F?NiL< z&i?Ez{3CJh`kUAk+cS^IE@(r1&(?7EgtI&3H_|!(5&MPgAxFgLph7=`EphgcpuZ^j zygl(BVw~X}-u~^&*aUrm?+BVe9_$K!2r{@5>D(o{c?Vz(?4^YNMK+vyfU7v)!dbUJ zkNFZl&)8ESZ+-jB&}*dUom;=WIe*~P?Rf;O)zN_L&0DIp_5*Q}hkI z^Bv}KmJqPfAK|SrK{r<)+rG?y#P(i3u> z?zng7?bRCY%HO&L{08TNsg8tJ~q=sEG`>i~E9 z1s3q;JfUakD}37Ck#-8KSCQ9q=KaBU1r2ad=DPWbwVyYzL0=PZ-p{8P=l983x4?4= zG0x=qE;7enffnBq+gR^%2b{rAd5OPQ&hl#B|CiHqSL`)0zsQUj^Zm^^!nV%_IG?o( zFmJe9~Yji+O&ec&`G%Ms@Es-Oc)*u}Gk$lGq1)FHX#e{l9^D)zw);1S@5nnUu|JTzcYK}+ z=;le#Ux0I(zYk{~=P0n3=v9bsXp3#Xdt{5W?A{brde?$kRVnf5S1raRyHUMVOxbfS-JlJ$w?f1KKt$x zwwcb4_nmp)nR!hM>VTKq*fa3BieLN^#-qc!RM=;hB=li+AuXk}O_k8-=;N>v$46JvU2Ejn6sFmGsO3{M6Zf8Uvd4s$ZN@S&kqW|Jg(70@XjY9m&k&1`x~L%t@XTPj_u5H zZ1*tR`R2>rxmn+wz+E9GKHgswUn7U$4iNjviF>cStl8rG*6n*rJm7nuJ+84cFwRt7 z8|0SQ3I2C%-@$Q_ax!dpX21>RtZos`^x(KY40s Mz|U*+4-!K72XJnQ1ONa4 literal 0 HcmV?d00001 diff --git a/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ_NE16Bench/network.onnx b/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ_NE16Bench/network.onnx new file mode 100644 index 0000000000000000000000000000000000000000..8ef6c9a35fe01031b8fe52d0f9c54435354d64c4 GIT binary patch literal 148684 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�u_V*msHj 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 Z;bnC;+~lQSH!IIU6TH>{{zZH-O>O6 literal 0 HcmV?d00001 diff --git a/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ_NE16Bench/outputs.npz b/DeeployTest/Tests/Kernels/Integer/Conv/Dense_2D_RQ_NE16Bench/outputs.npz new file mode 100644 index 0000000000000000000000000000000000000000..adada12411a8144c4597818f08958c4c194c60ff GIT binary patch literal 262410 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

@kDg%f0P2QUlg;Zr8 zch1oKr#Ukret>-i#=%R_W1Y6Wd;sPqu$uM!Hs0aegDBtZHu@U6`NDJBk2&RAQ18V> z9KDhkHoE6>RvolG;Can=R@NBQgb#_`Lz{-L=_kehi9KHUw_)r2K!1z<3tom}*gF1> zTC1$*xgHTusihXP?<&6UYy(V!GGAWELmg|0K3cQTjNbwFXskyqHQ&NpSbG;ZW9_Lt zd^2(l*o*h>8OE@mz~&kI2i6w;{pz36KPTQp_pFP+Sr}UYJ@(d~0QS_xm*3@qjp>Ur z=PTMX(2Ij^A6wub;LOb}_Z~31zsK6^*n@i5{;{U|j()LExwf5$*bDd>SS@n;MbGBB zW*F`h_pQCa_6$q-#^&HjG;Q-em%ka#%Xd-k*~a%i&%r%#3CuqNE8uS7r*Otuyn|QZ zxX}N%2ig{zZ`<|%>k<7MeiQsIbib$acgVcuLf-qzxViz z@0xN>)_;^Um%s8}V|UOlKvd|~wNCj>Y4_rwJx;+OY?f7qqpEBbQn9C|Mfzx4;?d2Va0xTA zrC4`|SPCu$>-xm}-LkH=TvOI<6YCID^ZZA|jX9q!SiMzh@FDsWcmiBvufpah*u5FJpB9i37Y$raBaDN8voG; zYl@int3Q#4-7V{(U+3YDKA~Mx>-a{v zd(YS+<{ICd>ym=CMH+|r^E~*TeHl1Ee=nje#H|stR<_@MVZSm5&A0a(d+=|%_|KXS zns@B?;eI2)-;Wfn4Lc9@Bn!Xqm*}o}jkX0FbH5DReHz$bu)FA2uyOT$Y~TAHzM6A9 zC~SSdxeWNNw7^~cIJ|+yJp4O*6RoVbN3AA$N=_fUPi(h{+oOAS@NX8HYyDPTe+s;- z!M=rN?ke^O=z@g0#=U=Qxu5<5kcD?y+UAU+c`x3T|1Df)O}YLQ%|G_$cRgFjVNDDl zfpc{IjM!W3pJ>xy3%kQU{tmg<7udYP{`k&byM=xMy7<21q|o1?#ifmIZ`~W=?=Ams zj)CYuc+fw2-j(+oVY_}7G{7z3``oFAnpdnVd%r`> zx}JBDxcWLg1iVjs@XbBS;bNV8n>SqW5Nw|3*?>P3y4nel{oNRa?L8*1l7||XXc^iM z*qUv0^M!tkn7oR=LM&x%Ir}aAGWRp_U-$>G`Tjj{Os;lWt6RjJeX02!e=hvpBJSCn zXh(%_4RMZk56!hDZ=-qUN8q~HuYp$fRqmhurRC&$E_24v%Wq&0t;{#(Tl)>PtHhG{ z&%aU1p3*F=`Inp>a=jz#J;rvOc`vcO;|8&^zPwb#%e*Oa%DVE%U-4C9WnP6|O1sQ2 z{aO(#edA5`oh;USCoSq4KQ6RGY~QNd$AVq=1z+tg&@bntR-%)K*hG=ji^Cf;-@6%n z@7QI$jpkb8qKU2V9d`4e`9AGA!~crj0OcCLIp<<-3)>p+{))TbCQx@xa`tC!n|w9T zZM+5R%W09fj=xdpiv{=5=0Q1+Tj;JY_b7Y&zid3FuJ^A!M-6lC;(Oi%?!jteAK$!D zY`MH=_iLk>^BR=jmGixeZ?3q+mw&+KUa>^*M)?Khyp3|;pV+ixer_P@JoMUHv@f)ik^@?BbIt}->wN~++?IYkf>#VQQ%zFa7M^V-(d6L*;um?&_cHIU0C)6tS z|Bp`d2aRg$!u}~zq)0JEiWDiPNRcAN6e&_z9ETJsQcRH|MT#jbQlyx|!op&ZAVEY1 z2@-6#MU1L->UzPS!*?F3&anx zKY~T+1i5?HaXb!594Vx#m4W8}w9$dzI7M`Tao^V9%qd3+Y-Ye_xVh;oDVQ|WNw1s1hxrTwC zhrO*6TL5)(J2Cbezo3Wju(^Ie`Mz;~tTTc3t_m?VbM}ET?R9PRMcDU7>f4y#gSTMq z1mm9NWEHf>k&WMtb_-kI+_R`1MgKVY%xQcn`tOO4f*$@WU@Xm9ple?NBj;k~EJWY2 zDL>^J7x!Y!9DVo9_lI`+?%jeX{MG939?fB^iDT-llGBKJ zIV@g)nJS#^4Qy+gFVBMttX2QLmOfgBX5LTiD%$$?vI%B@zGq{tgjCz!(p=u3x?`f| z8uI+V$E0zK`80mnU$#3pcR~<~rZEXE7L8vPZbt3y5 zIsO<;c8qM_=7~DFYJ+&xlP?d&xVf%L?Gc*3_sSl%XJbseM60P)A=brKFTg)hbFG6p z@lUbqjLGw$rTucud6=Jk$DH3w-;c%7a+eF;^y?R)qPL(f%Zt` z!-`yBp1(DXdFQ>iYVPwQzWcWUvYb}Xx@EYNKd~2xH(?RV72do3GMt@b&fgXHVbBwEdIyu8INck6i@BaJ zjIjwJpY^L*c4Aa%WtFJX72O4w*AWoF-P0?ws)-` zYfm-9ca^;wZ@>?#V4ttiW!z8n)b~^DWpYzb|I0_jW@6kr_9>@4ZTC~(J%0_nAL{O* zx@&$6YkN=9*mM{5ix_vDSfl226>RtM3h~eI8W;f?{sQdSG-mMZG42go|9SX(*%o#k zJ0n+nke`QlnD==VI6n3Cp1iAQ_h8TBH*Cy){XJLb z*@(IB<40mU_&4D+zDv$0e8;OTfY}&NHTTf8_u(hNypt;Y=63&mW>Yox+Lq3$`<@eA0o zE%IddpveE$_+8O2S_3w&FTVuVS%dW(piAFscd@OlExgaNc^h$_oZR&HNcBN3V=9n* zc^P=`(s(uTtqR%{ypHeQyGMh$$R9^@jx{v<83ltm=%W$qxi|a3^Yy#>8Mz(&RQvp# zuD=hwpUyvu{S|Cs`|f#+o$|exbu|4l|GS0M?l||L7j5G`^3+|=A~Ef= zsGSlYMeD#{K{pTkaZ0=%bGFePvt5O~cmi9upxz9&v6ry*jrHIu;_6E=K8rp-*TDB4>_+_<+xQXw7}#otarW-`bRHdIOZeu##=eAIvW^CP9CdSy z`yQQ+_60cgX6zv`*=I`r6n*dVN3aakdSD*Ag!{~64f~sc&)2htP}?G&{M#`v-Gd?z z?~Hp<|2O5L+yAws!`4_NzK%Twe*@P4&pO$&>-e|OC*f7tduYxF@D1qOdxzW>FqV4r zyXO}Ea24*J=lKr!UK&Mvi(O!MfzMRG4Bue~=sU#kfefvihkvsjp?^km+%fzbIM=(# zzH?WIwSjlldEINz!oCt5bKd?rxpPRenq%q6bUqAuQ>*fm2*xbiJ z{s?YVVUJP`+1$_YGPn;Gqvk%gqHQmeRj6aWb%wzY^z*f})BDlin_X(8xOXJ=)+1Ii zUO&aRW39{Zofz{sLysD7V$5g4eD^6Y!@pg}t5B!RL+qG%2JV0_+Hnp!-EHTeszUw+ za&pEtU~4Z_;km3~d*80&KZM<%O)!J+_fe|5i*^^hLcb0iZ!EosRT`V(l+fHC!wd~NX*{HXr^c2%>V4N!s9gL(tV=5K-p>=eI(?^$`* zR?+?5euDNH+cDRG&w%-!$1h^;vw9cZ+(YzLcu*hR{q>pk9_-*db`*Y#_8!jRA`iO1 z$F|UXHl5FXkv*q=v@>!Rv44Uc>^D*W&qVs~P#`6}eziTPv?&d&3+MxDG(*xC*79b{;I;_uMS*}?Zb zwA1^~=iRwZiOs?{t6(2vuLFDXykz&-THeng*mL)OxL?Ls(KdkJy=5M>K_9M*_ej6` z+lxIUOrSene+smytN$8ZtxmmZcmrQO!S{f1eb?>!^&Ow?-2?oeU=}QbZEyxUz%yR1 z!uS^cG}>})PnWpb1>hQw;12oQRd~ns@C$O*@Vnq6{vp@~juX~-SOt46 zYIXb;cFEYgaE+QNKaDAvZ=Jkl;_k^{4Gp;1xwWtYe z$#>Cyf~*X8(mT9{@0{b{3BI*HV*k>HuYix()4*qc683DYcNFV7#{2FwuP%1*8}v~F zejcM$U>4+gcqW&t_#FmYV~&_(z01Dmd|tm0Q@aUngTeY3pK>$ob@ZYN_qQm+89L@V zT0tFayC>c&`=3OA2Rn~>-|^iW^UY1`bN%{P;kQj{}URPn9eX332%g4a;UI6aLBK|k*W!UwsVBf(u=JVm)!m%U7 zhTuA}RMXfP+9-GkynDt(i#+4$UTU9IykCg9whrvwy^H1@c?ZndgZs^}7d2STJy`?R zy;t!LVf+T%29AFM%ySKC{!O&9$^D3Z_)LAnH{L-z#kcpAe}Mfi`rZ@kiEbY1-XPYu zmOi%Q>d!;1dF(uD?$J<1eR6+e&tkV=YweMLYz^!hdKddXJWSl@*_x}s_!_yg<8P|L z`sTQQbMOdwh3;H)@EZ64mYdWj=PkBl66*LpV2vgC8Ys!DGkyil_rnZ44ZE*DK?}G} z>%0Db39!e(wPx6R&%~NHNkr%;x;Jd-x%h*4v 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%0DpgSk=0PZq)g}#bh08ha-xo?qg+v)w@ zKsWax_91ctxeRB|zoNUYIrisMB4>cJE6enLzr%LTvv6Hv?cd%H(c|}KKmC6{TJX+# z4!FK|&b~_GI@p)dJ0QTDZ+&~ZDUWOc*KyVxUFZ|~zrK8C*wO6(DS`*bglf&IJw4)OrMHP*oca1QL^n@2tZKJKFd zcdp`|!Z|zN!w}MX2l!^;-r;k-0d@m9L^=m^Jrgkvdf4vQJsvB8-alh_~O&Dlme8~wN1X%Eh8mm0<&;+p~2(OYotM?R42Ud($9=Wpi{ynEGeZ_nWS z*adKn+rU{{Pkjj-VcX+6@VtI#r{4|N8iE1Hd!bVN=6XN2hz-C$uiQf30j~27=^eB$ z;dknncKzpmsA=p?a2d|{ztHEc2N;3_5PL<&GeusgIp=CU=lT=+6w>}0)NpQ|^Aq?E zvVn|!;U9%GAA2>=-qc;sJ-Xgc*dM_F&OT>>`Nplm4T-bgzkstbXA55wEW%mSeK`a3 z^?yNr1)j|kcm+NKYi@&nNO6zQe+3QTIwSD1qBgRC^D)Qw=?c8H-MiBJu!nB!<=Q7d z3@JywkKcP_4RJ{9lPcY#tJq)BTh!?y=dgcAy8lBs=kgR=eFxoHTjK?=w{KPYeQO{G z*z?G*#9TrDfF9`MFXRPmd+|Lp{|0Cg=Y8~D(YJ{H1ip!U1M-|7z}>_4chxmtf@j$7 zxkv6fY-{Rw#-70}u;y0H8)r{__@8UvG4b~L9DW6^1-jVgg~0if^B(RsIBKWwy}l#( zypJV#XVn8QDrP{wHMh~%;M~gxFoZLA0xsW&cj(^R1^o8^2h!ZP=oirC8^An!y^THt ztmnGkH+@3e8t$WUK7GeK$YGgkzJb@@1jcV8CxJ8T0e#N+DsV6E&;5J;2f&KeC-z6-XAc?xa-ds99E z8(P(5{u=1^zEma7GhD;Acl&Y1yPyMK zP|v(kNHc80-N&~7L!`cgT258Tw(cI>Qzh~CY<*`w37Wu}ELN%aH|X9u*Y6|e!Dl#g zena1;#t7Xx+3OMTowx$$`~MJlKO5)M**(Ix_I1!uZ>QQ9(XBfJtmhslf%}~X8|1s@ z61rz`fc^~>?0pmIT-~qliM|f{~kO$P?s`)-^c&5K&d%t(^d!F}zIrjOXN^|i1-Rm*=E%*+419+y($ZP8O%yZ2@ zkh3*sk5j;&+{b*C;=L>R_sl%c!h1CWhs4NTK{``w&!N9We}KG*G^P)XbIzN!?)pE# z&0vp!bJ_z#^lRV*{Z>2edm3rJy_oj}zvtM+wpVRu)$|2t@A};;{kioW@;=;;$h*h^z8}yx z(O&^)t?izdk@jX!)(HWd_&?x(#(s{>z9H)wlWpILx7fSL%z`J%2 zpK%-LufYn)HA{47X>N)1{!AbXFrrQi`1FwO_dWQipL*R|?;x*%ewp6S4QkwJvp#Zw zw6=G~TI##t8(0R~D^>bdUPiaBah^%+6WIk<@ZBM`?C)bkEHANpKTe?lCyTK4cAfw2Xx=l+h7q4$+y3adE^do-#hIz z-y>uXZbZ%-q%p2>3u!Mefzoq}?+m%hO>Ex-Yt5p2pC2IC+9`h<-n|>Q3+}>C1Haim z-pwdO&6UV;9lx>0DFeQOm?1Ee$UKKAv0Z0~w5RcUeYgnw^C?029uwoOFAw6{`i1tt7V!z{0kuXzW*=;d z*T-{O!`!mAb?}__R@aUDLqE;=j}&K~&$vJ9u@~ZUt=LN-xCB0-hI)QN9q+m&I(B@@ z?{#ARx~?_jQ&KLZm_($1#xWu*4eb&qh&n!>yvJyi{4=Dy zY@0KJi#)pdf3K0(F*f>KuLjKbxxi)HoYN=kC9&wj$39PGNbh1|>YDoDea6?5JKe9& zvaW>;0Xsk8J8!;^d9EMpsl#W^{pGsy*3ur76#lt0wAPHkIDzw*?l%@fxsqxyaS+8Lpafr>Sd`N_Gi6`xSYAk81=W2}C7`we5d zanVM`eJICtYsGvedFIC)Y~zPY;^j|)8g(U4>^(#^^8Zw?O!+aU)&sGzhw(n@bj9g< zWlbu>cRlYT_AXxnWsULLIWKbTCqFqRjOE0>wa1>=OPPAg`-&A}vYyJ2j<;VADPOfcCt`U2!eRAWE z`Po12%ek(1+JlM2=O@NvM;Q{EdyN*p*q;XaY5nlM@3;>8i2E9gvCi;scCM9kwAF_p z?Lpg|-0$Dli#+kMM{<1BbKOXk@t>z>T42ZZvLrtCiikY*%(qS-=^ELu9&5{kxGwJO zkNpJ+o)G_;=38Q7$6V-t>X-FmEHQ=PF*e4O-}7Q`_UcpOCr_OA5fYzkSkpSOwgNrZ zhys9ar1>PxH`?iMIX{{a^G)*0e=zkja}n`Wz0`j7OO6ZE5GOKk9{kNSC=b^ z8Q+_Fj*W4|`oukzQH}r5SA?-zWr~rDz_yM(DdXBj9bd1XYZ!mpTiq9-i7Qi1w8wiP zF0Nx-t|!McETf2i*Y}BQDT#@CDLd9%S^MzYZ|sS>&SSjyxF-H_F4i2cOU~&N(t2s# z&cFT3f13W!)3x(|6#sqV|Ng&zF;R&BcA7%Qzi9mLe_!wY`}+UVmA8Y#xF8}}l literal 0 HcmV?d00001